edit backgroun and inmclude

This commit is contained in:
Claudio 2026-01-18 21:33:01 +01:00
parent 1a41a99e20
commit 49a9d2a2a6
17 changed files with 1575 additions and 794 deletions

View File

@ -1,13 +1,17 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:intl/date_symbol_data_local.dart'; import 'package:intl/date_symbol_data_local.dart';
import 'package:provider/provider.dart';
import 'screens/login_page.dart'; import 'screens/login_page.dart';
import 'state/app_state.dart';
Future<void> main() async { Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
await initializeDateFormatting('it_IT'); await initializeDateFormatting('it_IT');
runApp(const MyApp()); runApp(
ChangeNotifierProvider(create: (_) => AppState(), child: const MyApp()),
);
} }
class MyApp extends StatelessWidget { class MyApp extends StatelessWidget {

View File

@ -0,0 +1,68 @@
class SchoolSettings {
final int portalPurchasesEnabled;
final String allowedProductTypes;
final String paymentMethods;
final String currencyCode;
final int enableNotifications;
final int allowFreezeGlobal;
final int freezeMaxDaysGlobal;
final int autoPropagateOnPurchase;
final int allowFullAccessRebooking;
final List<String> paymentMethodsArray;
final List<String> allowedProductTypesArray;
const SchoolSettings({
required this.portalPurchasesEnabled,
required this.allowedProductTypes,
required this.paymentMethods,
required this.currencyCode,
required this.enableNotifications,
required this.allowFreezeGlobal,
required this.freezeMaxDaysGlobal,
required this.autoPropagateOnPurchase,
required this.allowFullAccessRebooking,
required this.paymentMethodsArray,
required this.allowedProductTypesArray,
});
factory SchoolSettings.fromMap(Map<String, dynamic> m) {
int i(dynamic v) => (v is num) ? v.toInt() : int.tryParse('$v') ?? 0;
String s(dynamic v, [String def = '']) => (v ?? def).toString();
List<String> list(dynamic v) {
if (v is List) return v.map((e) => e.toString()).toList();
return [];
}
return SchoolSettings(
portalPurchasesEnabled: i(m['portal_purchases_enabled']),
allowedProductTypes: s(m['allowed_product_types']),
paymentMethods: s(m['payment_methods']),
currencyCode: s(m['currency_code'], 'EUR'),
enableNotifications: i(m['enable_notifications']),
allowFreezeGlobal: i(m['allow_freeze_global']),
freezeMaxDaysGlobal: i(m['freeze_max_days_global']),
autoPropagateOnPurchase: i(m['auto_propagate_on_purchase']),
allowFullAccessRebooking: i(m['allow_full_access_rebooking']),
paymentMethodsArray: list(m['payment_methods_array']),
allowedProductTypesArray: list(m['allowed_product_types_array']),
);
}
Map<String, dynamic> toMap() => {
'portal_purchases_enabled': portalPurchasesEnabled,
'allowed_product_types': allowedProductTypes,
'payment_methods': paymentMethods,
'currency_code': currencyCode,
'enable_notifications': enableNotifications,
'allow_freeze_global': allowFreezeGlobal,
'freeze_max_days_global': freezeMaxDaysGlobal,
'auto_propagate_on_purchase': autoPropagateOnPurchase,
'allow_full_access_rebooking': allowFullAccessRebooking,
'payment_methods_array': paymentMethodsArray,
'allowed_product_types_array': allowedProductTypesArray,
};
}

View File

@ -0,0 +1,67 @@
class UserSettings {
final int notifyEmail;
final int notifyWhatsapp;
final int notifyPush;
final int notifyBookingConfirm;
final int notifyBookingCancel;
final int notifySessionCancel;
final int notifyPaymentReceipt;
final int notifyExpirationReminder;
final int newsletterOptIn;
final int marketingOptIn;
final String locale;
final String timezone;
const UserSettings({
required this.notifyEmail,
required this.notifyWhatsapp,
required this.notifyPush,
required this.notifyBookingConfirm,
required this.notifyBookingCancel,
required this.notifySessionCancel,
required this.notifyPaymentReceipt,
required this.notifyExpirationReminder,
required this.newsletterOptIn,
required this.marketingOptIn,
required this.locale,
required this.timezone,
});
factory UserSettings.fromMap(Map<String, dynamic> m) {
int i(dynamic v) => (v is num) ? v.toInt() : int.tryParse('$v') ?? 0;
String s(dynamic v, [String def = '']) => (v ?? def).toString();
return UserSettings(
notifyEmail: i(m['notify_email']),
notifyWhatsapp: i(m['notify_whatsapp']),
notifyPush: i(m['notify_push']),
notifyBookingConfirm: i(m['notify_booking_confirm']),
notifyBookingCancel: i(m['notify_booking_cancel']),
notifySessionCancel: i(m['notify_session_cancel']),
notifyPaymentReceipt: i(m['notify_payment_receipt']),
notifyExpirationReminder: i(m['notify_expiration_reminder']),
newsletterOptIn: i(m['newsletter_opt_in']),
marketingOptIn: i(m['marketing_opt_in']),
locale: s(m['locale'], 'it'),
timezone: s(m['timezone'], 'Europe/Rome'),
);
}
Map<String, dynamic> toMap() => {
'notify_email': notifyEmail,
'notify_whatsapp': notifyWhatsapp,
'notify_push': notifyPush,
'notify_booking_confirm': notifyBookingConfirm,
'notify_booking_cancel': notifyBookingCancel,
'notify_session_cancel': notifySessionCancel,
'notify_payment_receipt': notifyPaymentReceipt,
'notify_expiration_reminder': notifyExpirationReminder,
'newsletter_opt_in': newsletterOptIn,
'marketing_opt_in': marketingOptIn,
'locale': locale,
'timezone': timezone,
};
}

View File

@ -9,6 +9,9 @@ import 'meditation_page.dart';
import '../services/vanguard_api.dart'; import '../services/vanguard_api.dart';
import 'login_page.dart'; import 'login_page.dart';
import 'medical_certificates_page.dart'; import 'medical_certificates_page.dart';
import '../widgets/app_drawer.dart';
import '../widgets/app_bottom_nav.dart';
import '../widgets/yogibook_background.dart';
class HomePage extends StatefulWidget { class HomePage extends StatefulWidget {
final String token; final String token;
@ -251,60 +254,14 @@ class _HomePageState extends State<HomePage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
backgroundColor: kBg, drawer: AppDrawer(
drawer: Drawer( token: widget.token,
child: SafeArea( school: widget.school,
child: ListView( userFirstName: widget.userFirstName,
padding: EdgeInsets.zero,
children: [
const DrawerHeader(
child: Align(
alignment: Alignment.bottomLeft,
child: Text(
'Menu',
style: TextStyle(fontSize: 22, fontWeight: FontWeight.w900),
),
),
),
ListTile(
leading: const Icon(Icons.swap_horiz),
title: const Text('Cambia scuola'),
onTap: () {
Navigator.of(context).pop();
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (_) => SelectSchoolPage(token: widget.token),
),
);
},
),
ListTile(
leading: const Icon(Icons.medical_information),
title: const Text('Certificati medici'),
onTap: () {
Navigator.of(context).pop(); // chiude drawer
Navigator.push(
context,
MaterialPageRoute(
builder: (_) =>
MedicalCertificatesPage(token: widget.token),
),
);
},
),
const Divider(height: 1),
ListTile(
leading: const Icon(Icons.logout),
title: const Text('Logout'),
onTap: _logout,
),
],
),
),
), ),
appBar: AppBar( appBar: AppBar(
backgroundColor: kBg, backgroundColor: Colors.transparent,
elevation: 0, elevation: 0,
centerTitle: true, centerTitle: true,
title: const Text( title: const Text(
@ -329,49 +286,38 @@ class _HomePageState extends State<HomePage> {
), ),
], ],
), ),
bottomNavigationBar: BottomNavigationBar(
type: BottomNavigationBarType.fixed, // con 4 icone serve bottomNavigationBar: AppBottomNav(
backgroundColor: Colors.white,
selectedItemColor: const Color(0xFF10B981),
unselectedItemColor: Colors.black54,
currentIndex: bottomIndex, currentIndex: bottomIndex,
onTap: _handleBottomNav, onTap: _handleBottomNav,
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.home_rounded),
label: 'Home',
),
BottomNavigationBarItem(
icon: Icon(Icons.event_note_rounded),
label: 'Lezioni',
),
BottomNavigationBarItem(
icon: Icon(Icons.person_rounded),
label: 'Account',
),
BottomNavigationBarItem(
icon: Icon(Icons.self_improvement_rounded),
label: 'Meditazione',
),
],
), ),
body: Padding( body: YogibookBackground(
child: SafeArea(
top: false,
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 10, 16, 16), padding: const EdgeInsets.fromLTRB(16, 10, 16, 16),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Ciao + nome // Ciao + nome
Text( Text(
_name.isNotEmpty ? 'Ciao, $_name' : 'Ciao', _name.isNotEmpty ? 'Ciao, $_name' : 'Ciao',
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w900), style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w900,
),
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
// Nome scuola + indirizzo sotto // Nome scuola + indirizzo sotto
Text( Text(
widget.school.name, widget.school.name,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w900), style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w900,
),
), ),
if (_schoolAddress.isNotEmpty) ...[ if (_schoolAddress.isNotEmpty) ...[
const SizedBox(height: 4), const SizedBox(height: 4),
@ -392,7 +338,10 @@ class _HomePageState extends State<HomePage> {
icon: const Icon(Icons.directions, size: 18, color: kGreen), icon: const Icon(Icons.directions, size: 18, color: kGreen),
label: const Text( label: const Text(
'Apri Mappe', 'Apri Mappe',
style: TextStyle(fontWeight: FontWeight.w800, color: kGreen), style: TextStyle(
fontWeight: FontWeight.w800,
color: kGreen,
),
), ),
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
side: const BorderSide(color: kGreen, width: 2), side: const BorderSide(color: kGreen, width: 2),
@ -423,9 +372,9 @@ class _HomePageState extends State<HomePage> {
icon: Icons.person_rounded, icon: Icons.person_rounded,
title: 'Account', title: 'Account',
subtitle: 'Profilo e impostazioni (TODO)', subtitle: 'Profilo e impostazioni (TODO)',
onTap: () => ScaffoldMessenger.of( onTap: () => ScaffoldMessenger.of(context).showSnackBar(
context, const SnackBar(content: Text('TODO: Account')),
).showSnackBar(const SnackBar(content: Text('TODO: Account'))), ),
), ),
const Spacer(), const Spacer(),
@ -445,7 +394,10 @@ class _HomePageState extends State<HomePage> {
), ),
), ),
const SizedBox(height: 14), const SizedBox(height: 14),
Image.asset('assets/images/yogibook_logo.png', height: 78), Image.asset(
'assets/images/yogibook_logo.png',
height: 78,
),
const SizedBox(height: 6), const SizedBox(height: 6),
], ],
), ),
@ -453,6 +405,8 @@ class _HomePageState extends State<HomePage> {
], ],
), ),
), ),
),
),
); );
} }
} }

View File

@ -6,6 +6,9 @@ import '../services/vanguard_api.dart';
import 'select_school_page.dart'; import 'select_school_page.dart';
import 'home_page.dart'; import 'home_page.dart';
import 'meditation_page.dart'; import 'meditation_page.dart';
import '../widgets/app_drawer.dart';
import '../widgets/app_bottom_nav.dart';
import '../widgets/yogibook_background.dart';
class LessonsPage extends StatefulWidget { class LessonsPage extends StatefulWidget {
final String token; final String token;
@ -136,18 +139,14 @@ class _LessonsPageState extends State<LessonsPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
backgroundColor: const Color(0xFFF6F6FB), drawer: AppDrawer(
drawer: _AppDrawer(
token: widget.token, token: widget.token,
onLogout: () { school: widget.school,
Navigator.of(context).pop(); userFirstName: widget.userFirstName,
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('Logout (TODO)')));
},
), ),
appBar: AppBar( appBar: AppBar(
backgroundColor: const Color(0xFFF6F6FB), backgroundColor: Colors.transparent,
elevation: 0, elevation: 0,
centerTitle: true, centerTitle: true,
title: Column( title: Column(
@ -185,34 +184,13 @@ class _LessonsPageState extends State<LessonsPage> {
), ),
], ],
), ),
bottomNavigationBar: BottomNavigationBar( bottomNavigationBar: AppBottomNav(
type: BottomNavigationBarType.fixed, // con 4 icone serve
backgroundColor: Colors.white,
selectedItemColor: const Color(0xFF10B981),
unselectedItemColor: Colors.black54,
currentIndex: bottomIndex, currentIndex: bottomIndex,
onTap: _handleBottomNav, onTap: _handleBottomNav,
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.home_rounded),
label: 'Home',
),
BottomNavigationBarItem(
icon: Icon(Icons.event_note_rounded),
label: 'Lezioni',
),
BottomNavigationBarItem(
icon: Icon(Icons.person_rounded),
label: 'Account',
),
BottomNavigationBarItem(
icon: Icon(Icons.self_improvement_rounded),
label: 'Meditazione',
),
],
), ),
body: SafeArea( body: YogibookBackground(
child: SafeArea(
top: false, top: false,
child: Column( child: Column(
children: [ children: [
@ -286,16 +264,18 @@ class _LessonsPageState extends State<LessonsPage> {
weekday: _weekdayLabel(l.date), weekday: _weekdayLabel(l.date),
dayNum: _dayNum(l.date), dayNum: _dayNum(l.date),
onReschedule: l.canModify onReschedule: l.canModify
? () => ? () => ScaffoldMessenger.of(context)
ScaffoldMessenger.of(context).showSnackBar( .showSnackBar(
const SnackBar( const SnackBar(
content: Text('Riprogramma: API dopo'), content: Text(
'Riprogramma: API dopo',
),
), ),
) )
: null, : null,
onCancel: l.canModify onCancel: l.canModify
? () => ? () => ScaffoldMessenger.of(context)
ScaffoldMessenger.of(context).showSnackBar( .showSnackBar(
const SnackBar( const SnackBar(
content: Text('Cancella: API dopo'), content: Text('Cancella: API dopo'),
), ),
@ -308,6 +288,7 @@ class _LessonsPageState extends State<LessonsPage> {
], ],
), ),
), ),
),
); );
} }
} }

View File

@ -1,3 +1,4 @@
// lib/screens/medical_certificates_page.dart
import 'dart:io'; import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -5,13 +6,29 @@ import 'package:image_picker/image_picker.dart';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import '../models/school.dart';
import '../services/medical_certificates_api.dart'; import '../services/medical_certificates_api.dart';
import '../config/api_config.dart'; import '../config/api_config.dart';
import '../widgets/app_drawer.dart';
import '../widgets/app_bottom_nav.dart';
import 'home_page.dart';
import 'lessons_page.dart';
import 'meditation_page.dart';
import '../widgets/yogibook_background.dart';
class MedicalCertificatesPage extends StatefulWidget { class MedicalCertificatesPage extends StatefulWidget {
final String token; final String token;
final School school;
final String? userFirstName;
const MedicalCertificatesPage({super.key, required this.token}); const MedicalCertificatesPage({
super.key,
required this.token,
required this.school,
this.userFirstName,
});
@override @override
State<MedicalCertificatesPage> createState() => State<MedicalCertificatesPage> createState() =>
@ -19,6 +36,8 @@ class MedicalCertificatesPage extends StatefulWidget {
} }
class _MedicalCertificatesPageState extends State<MedicalCertificatesPage> { class _MedicalCertificatesPageState extends State<MedicalCertificatesPage> {
static const Color kBg = Color(0xFFF6F6FB);
bool loading = true; bool loading = true;
String error = ''; String error = '';
List<Map<String, dynamic>> certs = []; List<Map<String, dynamic>> certs = [];
@ -31,6 +50,9 @@ class _MedicalCertificatesPageState extends State<MedicalCertificatesPage> {
DateTime? expiryDate; DateTime? expiryDate;
bool uploading = false; bool uploading = false;
// questa pagina è "sezione account"
int bottomIndex = 2;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -60,7 +82,6 @@ class _MedicalCertificatesPageState extends State<MedicalCertificatesPage> {
} }
Future<void> _pickFromChooser() async { Future<void> _pickFromChooser() async {
// 1 solo bottone -> bottom sheet scelta
final choice = await showModalBottomSheet<String>( final choice = await showModalBottomSheet<String>(
context: context, context: context,
showDragHandle: true, showDragHandle: true,
@ -123,7 +144,6 @@ class _MedicalCertificatesPageState extends State<MedicalCertificatesPage> {
setState(() { setState(() {
pickedFile = f; pickedFile = f;
pickedLabel = label; pickedLabel = label;
// reset campi per nuovo upload
docNameCtrl.text = 'certificato'; docNameCtrl.text = 'certificato';
notesCtrl.clear(); notesCtrl.clear();
expiryDate = null; expiryDate = null;
@ -182,7 +202,6 @@ class _MedicalCertificatesPageState extends State<MedicalCertificatesPage> {
notes: notesCtrl.text, notes: notesCtrl.text,
); );
// reset form
setState(() { setState(() {
pickedFile = null; pickedFile = null;
pickedLabel = ''; pickedLabel = '';
@ -205,10 +224,21 @@ class _MedicalCertificatesPageState extends State<MedicalCertificatesPage> {
} }
Future<void> _openFileUrl(String url) async { Future<void> _openFileUrl(String url) async {
// url arriva tipo "/userarea/certificate/xxx" // url tipico: "/userarea/certificate/xxx"
// per aprirlo serve url assoluto // fix plural errato se presente
final abs = '${ApiConfig.scheme}://${ApiConfig.host}$url'; var fixed = url.replaceFirst(
'/userarea/certificates/',
'/userarea/certificate/',
);
// se non ha /public davanti, lo aggiungiamo
if (!fixed.startsWith('/public/')) {
fixed = '/public${fixed.startsWith('/') ? '' : '/'}$fixed';
}
final abs = '${ApiConfig.scheme}://${ApiConfig.host}$fixed';
final uri = Uri.parse(abs); final uri = Uri.parse(abs);
final ok = await launchUrl(uri, mode: LaunchMode.externalApplication); final ok = await launchUrl(uri, mode: LaunchMode.externalApplication);
if (!ok && mounted) { if (!ok && mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
@ -256,10 +286,67 @@ class _MedicalCertificatesPageState extends State<MedicalCertificatesPage> {
bool _isPdfPath(String p) => p.toLowerCase().endsWith('.pdf'); bool _isPdfPath(String p) => p.toLowerCase().endsWith('.pdf');
Future<void> _goTo(Widget page) async {
if (!mounted) return;
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => page));
}
void _handleBottomNav(int i) {
if (i == bottomIndex) return;
setState(() => bottomIndex = i);
if (i == 0) {
_goTo(
HomePage(
token: widget.token,
school: widget.school,
userFirstName: widget.userFirstName,
),
);
return;
}
if (i == 1) {
_goTo(
LessonsPage(
token: widget.token,
school: widget.school,
userFirstName: widget.userFirstName,
),
);
return;
}
if (i == 2) {
// siamo già in "area account", non facciamo nulla (o TODO)
return;
}
if (i == 3) {
_goTo(
MeditationPage(
token: widget.token,
school: widget.school,
userFirstName: widget.userFirstName,
),
);
return;
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
// Drawer standard (come MeditationPage)
drawer: AppDrawer(
token: widget.token,
school: widget.school,
userFirstName: widget.userFirstName,
),
appBar: AppBar( appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
title: const Text( title: const Text(
'Certificati medici', 'Certificati medici',
style: TextStyle(fontWeight: FontWeight.w900), style: TextStyle(fontWeight: FontWeight.w900),
@ -271,7 +358,17 @@ class _MedicalCertificatesPageState extends State<MedicalCertificatesPage> {
), ),
], ],
), ),
body: RefreshIndicator(
// Bottom nav standard (come MeditationPage)
bottomNavigationBar: AppBottomNav(
currentIndex: bottomIndex,
onTap: _handleBottomNav,
),
body: YogibookBackground(
child: SafeArea(
top: false,
child: RefreshIndicator(
onRefresh: _reload, onRefresh: _reload,
child: ListView( child: ListView(
padding: const EdgeInsets.fromLTRB(16, 14, 16, 22), padding: const EdgeInsets.fromLTRB(16, 14, 16, 22),
@ -317,7 +414,10 @@ class _MedicalCertificatesPageState extends State<MedicalCertificatesPage> {
Text( Text(
'Certificati caricati (${certs.length})', 'Certificati caricati (${certs.length})',
style: const TextStyle(fontWeight: FontWeight.w900, fontSize: 16), style: const TextStyle(
fontWeight: FontWeight.w900,
fontSize: 16,
),
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
@ -356,7 +456,9 @@ class _MedicalCertificatesPageState extends State<MedicalCertificatesPage> {
children: [ children: [
Expanded( Expanded(
child: Text( child: Text(
docName.isNotEmpty ? docName : 'certificato', docName.isNotEmpty
? docName
: 'certificato',
style: const TextStyle( style: const TextStyle(
fontWeight: FontWeight.w900, fontWeight: FontWeight.w900,
fontSize: 15, fontSize: 15,
@ -473,6 +575,8 @@ class _MedicalCertificatesPageState extends State<MedicalCertificatesPage> {
], ],
), ),
), ),
),
),
); );
} }
} }
@ -567,17 +671,12 @@ class _UploadCard extends StatelessWidget {
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
Row( OutlinedButton.icon(
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: onPickExpiry, onPressed: onPickExpiry,
icon: const Icon(Icons.date_range), icon: const Icon(Icons.date_range),
label: Text(expiryLabel), label: Text(expiryLabel),
), ),
),
],
),
const SizedBox(height: 10), const SizedBox(height: 10),
TextField( TextField(

View File

@ -8,6 +8,9 @@ import 'home_page.dart';
import 'lessons_page.dart'; import 'lessons_page.dart';
import 'select_school_page.dart'; import 'select_school_page.dart';
import 'login_page.dart'; import 'login_page.dart';
import '../widgets/app_drawer.dart';
import '../widgets/app_bottom_nav.dart';
import '../widgets/yogibook_background.dart';
class MeditationPage extends StatefulWidget { class MeditationPage extends StatefulWidget {
final String token; final String token;
@ -288,41 +291,14 @@ class _MeditationPageState extends State<MeditationPage>
).animate(CurvedAnimation(parent: _pulseCtrl, curve: Curves.easeInOut)); ).animate(CurvedAnimation(parent: _pulseCtrl, curve: Curves.easeInOut));
return Scaffold( return Scaffold(
backgroundColor: kBg, drawer: AppDrawer(
drawer: Drawer( token: widget.token,
child: SafeArea( school: widget.school,
child: ListView( userFirstName: widget.userFirstName,
padding: EdgeInsets.zero,
children: [
const DrawerHeader(
child: Align(
alignment: Alignment.bottomLeft,
child: Text(
'Menu',
style: TextStyle(fontSize: 22, fontWeight: FontWeight.w900),
),
),
),
ListTile(
leading: const Icon(Icons.swap_horiz),
title: const Text('Cambia scuola'),
onTap: () async {
Navigator.of(context).pop(); // chiude drawer
await _goTo(SelectSchoolPage(token: widget.token));
},
),
const Divider(height: 1),
ListTile(
leading: const Icon(Icons.logout),
title: const Text('Logout'),
onTap: _logout,
),
],
),
),
), ),
appBar: AppBar( appBar: AppBar(
backgroundColor: kBg, backgroundColor: Colors.transparent,
elevation: 0, elevation: 0,
centerTitle: true, centerTitle: true,
title: const Text( title: const Text(
@ -381,7 +357,8 @@ class _MeditationPageState extends State<MeditationPage>
), ),
], ],
), ),
body: SafeArea( body: YogibookBackground(
child: SafeArea(
top: false, top: false,
child: Padding( child: Padding(
padding: const EdgeInsets.fromLTRB(16, 10, 16, 16), padding: const EdgeInsets.fromLTRB(16, 10, 16, 16),
@ -581,6 +558,7 @@ class _MeditationPageState extends State<MeditationPage>
), ),
), ),
), ),
),
); );
} }
} }

View File

@ -1,10 +1,14 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../models/school.dart'; import '../models/school.dart';
import '../services/vanguard_api.dart'; import '../services/vanguard_api.dart';
import 'lessons_page.dart';
import 'home_page.dart'; import 'home_page.dart';
import 'meditation_page.dart';
import 'login_page.dart'; import 'login_page.dart';
import 'package:provider/provider.dart';
import '../state/app_state.dart';
import '../widgets/yogibook_background.dart';
// TODO (step successivo): creeremo questi due file
// import '../services/settings_bootstrap.dart';
class SelectSchoolPage extends StatefulWidget { class SelectSchoolPage extends StatefulWidget {
final String token; final String token;
@ -15,7 +19,6 @@ class SelectSchoolPage extends StatefulWidget {
} }
class _SelectSchoolPageState extends State<SelectSchoolPage> { class _SelectSchoolPageState extends State<SelectSchoolPage> {
static const Color kBg = Color(0xFFF6F6FB);
static const Color kGreen = Color(0xFF10B981); static const Color kGreen = Color(0xFF10B981);
bool loading = true; bool loading = true;
@ -23,8 +26,6 @@ class _SelectSchoolPageState extends State<SelectSchoolPage> {
String? firstName; String? firstName;
List<School> schools = []; List<School> schools = [];
int bottomIndex = 1; // 0=Home, 1=Lezioni, 2=Account (qui siamo in "Lezioni")
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -51,26 +52,50 @@ class _SelectSchoolPageState extends State<SelectSchoolPage> {
// Auto-select if API says so // Auto-select if API says so
final autoSelect = data['auto_select'] == true; final autoSelect = data['auto_select'] == true;
final selectedId = data['selected_school_id']; final selectedId = data['selected_school_id'];
if (autoSelect && selectedId != null && schools.isNotEmpty) { if (autoSelect && selectedId != null && schools.isNotEmpty) {
final id = (selectedId as num).toInt(); final id = (selectedId as num).toInt();
final s = schools.firstWhere((x) => x.id == id); final s = schools.firstWhere((x) => x.id == id);
_enterSchool(s); await _enterSchool(s); // ora async
} }
} catch (e) { } catch (e) {
setState(() => error = 'Errore: $e'); setState(() => error = 'Errore: $e');
} finally { } finally {
setState(() => loading = false); if (mounted) setState(() => loading = false);
} }
} }
void _enterSchool(School s) { Future<void> _enterSchool(School s) async {
setState(() {
loading = true;
error = '';
});
try {
// CARICA settings (user + school) nello store globale AppState
await context.read<AppState>().bootstrap(
token: widget.token,
school: s,
userFirstName: firstName,
);
if (!mounted) return;
Navigator.pushReplacement( Navigator.pushReplacement(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (_) => builder: (_) => HomePage(
HomePage(token: widget.token, school: s, userFirstName: firstName), token: widget.token,
school: s,
userFirstName: firstName,
),
), ),
); );
} catch (e) {
if (mounted) setState(() => error = 'Errore caricando impostazioni: $e');
} finally {
if (mounted) setState(() => loading = false);
}
} }
String get _name => (firstName ?? '').trim(); String get _name => (firstName ?? '').trim();
@ -97,31 +122,20 @@ class _SelectSchoolPageState extends State<SelectSchoolPage> {
if (ok != true) return; if (ok != true) return;
// chiude il drawer se aperto
Navigator.of(context).pop(); Navigator.of(context).pop();
// chiama API logout (se fallisce usciamo lo stesso)
try { try {
await VanguardApi.logout(token: widget.token); await VanguardApi.logout(token: widget.token);
} catch (_) {} } catch (_) {}
if (!mounted) return; if (!mounted) return;
// torna a Login e cancella tutta la stack
Navigator.of(context).pushAndRemoveUntil( Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(builder: (_) => const LoginPage()), MaterialPageRoute(builder: (_) => const LoginPage()),
(route) => false, (route) => false,
); );
} }
void _handleBottomNav(int i) {
setState(() => bottomIndex = i);
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('Prima seleziona la scuola')));
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@ -153,72 +167,9 @@ class _SelectSchoolPageState extends State<SelectSchoolPage> {
), ),
], ],
), ),
bottomNavigationBar: BottomNavigationBar(
type: BottomNavigationBarType.fixed, // con 4 icone serve
backgroundColor: Colors.white,
selectedItemColor: const Color(0xFF10B981),
unselectedItemColor: Colors.black54,
currentIndex: bottomIndex,
onTap: _handleBottomNav,
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.home_rounded),
label: 'Home',
),
BottomNavigationBarItem(
icon: Icon(Icons.event_note_rounded),
label: 'Lezioni',
),
BottomNavigationBarItem(
icon: Icon(Icons.person_rounded),
label: 'Account',
),
BottomNavigationBarItem(
icon: Icon(Icons.self_improvement_rounded),
label: 'Meditazione',
),
],
),
body: Stack( body: YogibookBackground(
children: [ child: SafeArea(
// --- SFONDO (gradient)
Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Color(0xFFF4F6FA),
Color(0xFFEFF7F3),
Color(0xFFF4F6FA),
],
),
),
),
// --- BOLLE decorative
const _BgBlob(
color: Color(0x3310B981),
size: 280,
top: -70,
left: -90,
),
const _BgBlob(
color: Color(0x22F59E0B),
size: 230,
bottom: 80,
right: -70,
),
const _BgBlob(
color: Color(0x227C3AED),
size: 190,
bottom: -55,
left: 35,
),
// --- CONTENUTO della pagina (quello che avevi prima)
SafeArea(
top: false, top: false,
child: loading child: loading
? const Center(child: CircularProgressIndicator()) ? const Center(child: CircularProgressIndicator())
@ -227,10 +178,9 @@ class _SelectSchoolPageState extends State<SelectSchoolPage> {
: _SchoolsList( : _SchoolsList(
firstName: firstName, firstName: firstName,
schools: schools, schools: schools,
onSelect: _enterSchool, onSelect: (s) => _enterSchool(s),
), ),
), ),
],
), ),
); );
} }
@ -261,7 +211,7 @@ class _AppDrawer extends StatelessWidget {
ListTile( ListTile(
leading: const Icon(Icons.swap_horiz), leading: const Icon(Icons.swap_horiz),
title: const Text('Cambia scuola'), title: const Text('Cambia scuola'),
onTap: () => Navigator.of(context).pop(), // sei già qui onTap: () => Navigator.of(context).pop(),
), ),
const Divider(height: 1), const Divider(height: 1),
ListTile( ListTile(
@ -296,7 +246,6 @@ class _SchoolsList extends StatelessWidget {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// header compatto (stesso stile soft delle lezioni)
Padding( Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 10), padding: const EdgeInsets.fromLTRB(16, 8, 16, 10),
child: Column( child: Column(
@ -334,6 +283,7 @@ class _SchoolsList extends StatelessWidget {
}, },
), ),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Center( Center(
child: Padding( child: Padding(

View File

@ -0,0 +1,46 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../config/api_config.dart';
import '../models/user_settings.dart';
import '../models/school_settings.dart';
class SettingsApi {
static Map<String, String> _headers(String token) => {
'Accept': 'application/json',
'Authorization': 'Bearer $token',
};
static Future<UserSettings> fetchUserSettings({required String token}) async {
final uri = Uri.parse('${ApiConfig.phpApiBase}/api_user_settings.php');
final res = await http.get(uri, headers: _headers(token));
final data = jsonDecode(res.body) as Map<String, dynamic>;
if (res.statusCode != 200 || data['success'] != true) {
throw Exception(data['message'] ?? 'User settings error');
}
return UserSettings.fromMap(
(data['settings'] as Map).cast<String, dynamic>(),
);
}
static Future<SchoolSettings> fetchSchoolSettings({
required String token,
required int schoolId,
}) async {
final uri = Uri.parse(
'${ApiConfig.phpApiBase}/api_school_settings.php?school_id=$schoolId',
);
final res = await http.get(uri, headers: _headers(token));
final data = jsonDecode(res.body) as Map<String, dynamic>;
if (res.statusCode != 200 || data['success'] != true) {
throw Exception(data['message'] ?? 'School settings error');
}
return SchoolSettings.fromMap(
(data['settings'] as Map).cast<String, dynamic>(),
);
}
}

View File

@ -0,0 +1,56 @@
import 'package:flutter/foundation.dart';
import '../models/user_settings.dart';
import '../models/school_settings.dart';
import '../services/settings_api.dart';
class AppSettingsStore extends ChangeNotifier {
bool loading = false;
String error = '';
int? schoolId;
UserSettings? userSettings;
SchoolSettings? schoolSettings;
bool get isReady => userSettings != null && schoolSettings != null;
Future<void> loadForSchool({
required String token,
required int schoolId,
}) async {
loading = true;
error = '';
notifyListeners();
try {
// puoi farle in parallelo
final results = await Future.wait([
SettingsApi.fetchUserSettings(token: token),
SettingsApi.fetchSchoolSettings(token: token, schoolId: schoolId),
]);
userSettings = results[0] as UserSettings;
schoolSettings = results[1] as SchoolSettings;
this.schoolId = schoolId;
} catch (e) {
error = e.toString();
// se fallisce, meglio lasciare null per evitare dati mezzi vecchi
userSettings = null;
schoolSettings = null;
this.schoolId = null;
} finally {
loading = false;
notifyListeners();
}
}
void clear() {
loading = false;
error = '';
schoolId = null;
userSettings = null;
schoolSettings = null;
notifyListeners();
}
}

104
lib/state/app_state.dart Normal file
View File

@ -0,0 +1,104 @@
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../models/school.dart';
import '../models/user_settings.dart';
import '../models/school_settings.dart';
import '../services/settings_api.dart';
class AppState extends ChangeNotifier {
String? token;
School? school;
String? userFirstName;
UserSettings? userSettings;
SchoolSettings? schoolSettings;
bool loadingSettings = false;
String settingsError = '';
// Call this after login + school selection (not in SelectSchoolPage UI itself)
Future<void> bootstrap({
required String token,
required School school,
String? userFirstName,
}) async {
this.token = token;
this.school = school;
this.userFirstName = userFirstName;
loadingSettings = true;
settingsError = '';
notifyListeners();
// Try cache first
await _loadCache();
try {
// Fetch both in parallel
final results = await Future.wait([
SettingsApi.fetchUserSettings(token: token),
SettingsApi.fetchSchoolSettings(token: token, schoolId: school.id),
]);
userSettings = results[0] as UserSettings;
schoolSettings = results[1] as SchoolSettings;
await _saveCache();
} catch (e) {
settingsError = e.toString();
// If cache exists, we keep it and don't crash the app
} finally {
loadingSettings = false;
notifyListeners();
}
}
// If you want manual refresh from any page
Future<void> refreshSettings() async {
if (token == null || school == null) return;
await bootstrap(
token: token!,
school: school!,
userFirstName: userFirstName,
);
}
String get _cacheKey {
final sid = school?.id ?? 0;
// Cache per school (user is implicit in token/session)
return 'settings_cache_school_$sid';
}
Future<void> _saveCache() async {
if (userSettings == null || schoolSettings == null) return;
final sp = await SharedPreferences.getInstance();
final payload = jsonEncode({
'user': userSettings!.toMap(),
'school': schoolSettings!.toMap(),
});
await sp.setString(_cacheKey, payload);
}
Future<void> _loadCache() async {
final sp = await SharedPreferences.getInstance();
final raw = sp.getString(_cacheKey);
if (raw == null) return;
try {
final decoded = jsonDecode(raw) as Map<String, dynamic>;
userSettings = UserSettings.fromMap(
(decoded['user'] as Map).cast<String, dynamic>(),
);
schoolSettings = SchoolSettings.fromMap(
(decoded['school'] as Map).cast<String, dynamic>(),
);
} catch (_) {
// Ignore cache errors
}
}
}

View File

@ -0,0 +1,39 @@
import 'package:flutter/material.dart';
class AppBottomNav extends StatelessWidget {
final int currentIndex;
final ValueChanged<int> onTap;
const AppBottomNav({
super.key,
required this.currentIndex,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return BottomNavigationBar(
type: BottomNavigationBarType.fixed,
backgroundColor: Colors.white,
selectedItemColor: const Color(0xFF10B981),
unselectedItemColor: Colors.black54,
currentIndex: currentIndex,
onTap: onTap,
items: const [
BottomNavigationBarItem(icon: Icon(Icons.home_rounded), label: 'Home'),
BottomNavigationBarItem(
icon: Icon(Icons.event_note_rounded),
label: 'Lezioni',
),
BottomNavigationBarItem(
icon: Icon(Icons.person_rounded),
label: 'Account',
),
BottomNavigationBarItem(
icon: Icon(Icons.self_improvement_rounded),
label: 'Meditazione',
),
],
);
}
}

271
lib/widgets/app_drawer.dart Normal file
View File

@ -0,0 +1,271 @@
import 'package:flutter/material.dart';
import '../models/school.dart';
import '../services/vanguard_api.dart';
import '../config/api_config.dart';
import '../screens/select_school_page.dart';
import '../screens/login_page.dart';
import '../screens/medical_certificates_page.dart';
class AppDrawer extends StatelessWidget {
final String token;
final School school;
final String? userFirstName;
const AppDrawer({
super.key,
required this.token,
required this.school,
this.userFirstName,
});
String get _name => (userFirstName ?? '').trim();
String get _avatarLetter => _name.isNotEmpty ? _name[0].toUpperCase() : 'U';
String? get _schoolLogoUrl {
final raw = (school.logo ?? '').toString().trim();
if (raw.isEmpty) return null;
// base: https://app.yogibook.com/public/userarea/
return '${ApiConfig.scheme}://${ApiConfig.host}/public/userarea/$raw';
}
Future<void> _logout(BuildContext context) async {
final ok = await showDialog<bool>(
context: context,
builder: (_) => AlertDialog(
title: const Text('Conferma logout'),
content: const Text('Vuoi uscire dal tuo account?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('Annulla'),
),
ElevatedButton(
onPressed: () => Navigator.pop(context, true),
child: const Text('Logout'),
),
],
),
);
if (ok != true) return;
Navigator.of(context).pop(); // chiude drawer
try {
await VanguardApi.logout(token: token);
} catch (_) {}
if (!context.mounted) return;
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(builder: (_) => const LoginPage()),
(route) => false,
);
}
@override
Widget build(BuildContext context) {
final logoUrl = _schoolLogoUrl;
return Drawer(
child: SafeArea(
child: ListView(
padding: EdgeInsets.zero,
children: [
_DrawerHeaderWide(
schoolName: school.name,
logoUrl: logoUrl,
fallbackLetter: _avatarLetter,
),
ListTile(
leading: const Icon(Icons.swap_horiz),
title: const Text('Cambia scuola'),
onTap: () {
Navigator.of(context).pop();
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (_) => SelectSchoolPage(token: token),
),
);
},
),
// solo nel drawer
ListTile(
leading: const Icon(Icons.medical_information),
title: const Text('Certificati medici'),
onTap: () {
Navigator.of(context).pop();
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => MedicalCertificatesPage(
token: token,
school: school,
userFirstName: userFirstName,
),
),
);
},
),
const Divider(height: 1),
ListTile(
leading: const Icon(Icons.logout),
title: const Text('Logout'),
onTap: () => _logout(context),
),
],
),
),
);
}
}
class _DrawerHeaderWide extends StatelessWidget {
final String schoolName;
final String? logoUrl;
final String fallbackLetter;
const _DrawerHeaderWide({
required this.schoolName,
required this.logoUrl,
required this.fallbackLetter,
});
@override
Widget build(BuildContext context) {
const green = Color(0xFF10B981);
return Container(
// Header largo quanto il drawer
padding: const EdgeInsets.fromLTRB(16, 14, 16, 14),
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Color(0xFFF4F6FA), Color(0xFFEFF7F3)],
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Menu',
style: TextStyle(fontSize: 22, fontWeight: FontWeight.w900),
),
const SizedBox(height: 12),
// BOX LOGO LARGO + PIÙ ALTO + IN RATIO (non taglia)
Container(
width: double.infinity,
height: 140, // <-- aumenta qui (es. 140/160/180)
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: const [
BoxShadow(
blurRadius: 18,
color: Color(0x12000000),
offset: Offset(0, 10),
),
],
border: Border.all(color: const Color(0x14000000)),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: logoUrl == null
? _LogoFallback(letter: fallbackLetter)
: Padding(
padding: const EdgeInsets.all(10), // respiro per logo
child: Image.network(
logoUrl!,
width: double.infinity, // solo larghezza imposta
fit: BoxFit.contain, // mantiene ratio, non taglia
errorBuilder: (_, __, ___) =>
_LogoFallback(letter: fallbackLetter),
),
),
),
),
const SizedBox(height: 10),
// Nome scuola sotto
Text(
schoolName,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: Colors.black87,
fontWeight: FontWeight.w900,
fontSize: 22,
),
),
const SizedBox(height: 6),
// badge piccolo opzionale, fa design
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
decoration: BoxDecoration(
color: green.withOpacity(0.12),
borderRadius: BorderRadius.circular(999),
),
child: const Text(
'Scuola selezionata',
style: TextStyle(
color: green,
fontWeight: FontWeight.w900,
fontSize: 11,
),
),
),
],
),
);
}
}
class _LogoFallback extends StatelessWidget {
final String letter;
const _LogoFallback({required this.letter});
@override
Widget build(BuildContext context) {
const green = Color(0xFF10B981);
return Container(
width: double.infinity,
height: double.infinity,
alignment: Alignment.center,
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Color(0xFFE7F8F1), Color(0xFFF4F6FA)],
),
),
child: Container(
width: 44,
height: 44,
decoration: const BoxDecoration(color: green, shape: BoxShape.circle),
alignment: Alignment.center,
child: Text(
letter,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.w900,
fontSize: 16,
),
),
),
);
}
}

View File

@ -0,0 +1,88 @@
import 'package:flutter/material.dart';
class YogibookBackground extends StatelessWidget {
final Widget child;
const YogibookBackground({super.key, required this.child});
@override
Widget build(BuildContext context) {
return Stack(
children: [
// --- SFONDO (gradient)
Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Color(0xFFF4F6FA), Color(0xFFEFF7F3), Color(0xFFF4F6FA)],
),
),
),
// --- BOLLE decorative
const _BgBlob(color: Color(0x3310B981), size: 280, top: -70, left: -90),
const _BgBlob(
color: Color(0x22F59E0B),
size: 230,
bottom: 80,
right: -70,
),
const _BgBlob(
color: Color(0x227C3AED),
size: 190,
bottom: -55,
left: 35,
),
// contenuto pagina
child,
],
);
}
}
class _BgBlob extends StatelessWidget {
final Color color;
final double size;
final double? top;
final double? left;
final double? right;
final double? bottom;
const _BgBlob({
required this.color,
required this.size,
this.top,
this.left,
this.right,
this.bottom,
});
@override
Widget build(BuildContext context) {
return Positioned(
top: top,
left: left,
right: right,
bottom: bottom,
child: IgnorePointer(
child: Container(
width: size,
height: size,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
blurRadius: 70,
color: color,
offset: const Offset(0, 18),
),
],
),
),
),
);
}
}

View File

@ -10,6 +10,7 @@ import file_picker
import file_selector_macos import file_selector_macos
import google_sign_in_ios import google_sign_in_ios
import path_provider_foundation import path_provider_foundation
import shared_preferences_foundation
import url_launcher_macos import url_launcher_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
@ -18,5 +19,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
FLTGoogleSignInPlugin.register(with: registry.registrar(forPlugin: "FLTGoogleSignInPlugin")) FLTGoogleSignInPlugin.register(with: registry.registrar(forPlugin: "FLTGoogleSignInPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
} }

View File

@ -429,6 +429,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.0" version: "2.0.0"
nested:
dependency: transitive
description:
name: nested
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
path: path:
dependency: transitive dependency: transitive
description: description:
@ -501,6 +509,70 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.8" version: "2.1.8"
provider:
dependency: "direct main"
description:
name: provider
sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272"
url: "https://pub.dev"
source: hosted
version: "6.1.5+1"
shared_preferences:
dependency: "direct main"
description:
name: shared_preferences
sha256: "2939ae520c9024cb197fc20dee269cd8cdbf564c8b5746374ec6cacdc5169e64"
url: "https://pub.dev"
source: hosted
version: "2.5.4"
shared_preferences_android:
dependency: transitive
description:
name: shared_preferences_android
sha256: "83af5c682796c0f7719c2bbf74792d113e40ae97981b8f266fa84574573556bc"
url: "https://pub.dev"
source: hosted
version: "2.4.18"
shared_preferences_foundation:
dependency: transitive
description:
name: shared_preferences_foundation
sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f"
url: "https://pub.dev"
source: hosted
version: "2.5.6"
shared_preferences_linux:
dependency: transitive
description:
name: shared_preferences_linux
sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shared_preferences_platform_interface:
dependency: transitive
description:
name: shared_preferences_platform_interface
sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shared_preferences_web:
dependency: transitive
description:
name: shared_preferences_web
sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019
url: "https://pub.dev"
source: hosted
version: "2.4.3"
shared_preferences_windows:
dependency: transitive
description:
name: shared_preferences_windows
sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
sky_engine: sky_engine:
dependency: transitive dependency: transitive
description: flutter description: flutter

View File

@ -42,6 +42,8 @@ dependencies:
file_picker: ^8.0.7 file_picker: ^8.0.7
image_picker: ^1.1.2 image_picker: ^1.1.2
mime: ^2.0.0 mime: ^2.0.0
provider: ^6.1.5+1
shared_preferences: ^2.2.3
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: