edit backgroun and inmclude
This commit is contained in:
parent
1a41a99e20
commit
49a9d2a2a6
@ -1,13 +1,17 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:intl/date_symbol_data_local.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'screens/login_page.dart';
|
||||
import 'state/app_state.dart';
|
||||
|
||||
Future<void> main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await initializeDateFormatting('it_IT');
|
||||
runApp(const MyApp());
|
||||
runApp(
|
||||
ChangeNotifierProvider(create: (_) => AppState(), child: const MyApp()),
|
||||
);
|
||||
}
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
|
||||
68
lib/models/school_settings.dart
Normal file
68
lib/models/school_settings.dart
Normal 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,
|
||||
};
|
||||
}
|
||||
67
lib/models/user_settings.dart
Normal file
67
lib/models/user_settings.dart
Normal 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,
|
||||
};
|
||||
}
|
||||
@ -9,6 +9,9 @@ import 'meditation_page.dart';
|
||||
import '../services/vanguard_api.dart';
|
||||
import 'login_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 {
|
||||
final String token;
|
||||
@ -251,60 +254,14 @@ class _HomePageState extends State<HomePage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: kBg,
|
||||
drawer: Drawer(
|
||||
child: SafeArea(
|
||||
child: ListView(
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
drawer: AppDrawer(
|
||||
token: widget.token,
|
||||
school: widget.school,
|
||||
userFirstName: widget.userFirstName,
|
||||
),
|
||||
|
||||
appBar: AppBar(
|
||||
backgroundColor: kBg,
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
centerTitle: true,
|
||||
title: const Text(
|
||||
@ -329,128 +286,125 @@ class _HomePageState extends State<HomePage> {
|
||||
),
|
||||
],
|
||||
),
|
||||
bottomNavigationBar: BottomNavigationBar(
|
||||
type: BottomNavigationBarType.fixed, // con 4 icone serve
|
||||
backgroundColor: Colors.white,
|
||||
selectedItemColor: const Color(0xFF10B981),
|
||||
unselectedItemColor: Colors.black54,
|
||||
|
||||
bottomNavigationBar: AppBottomNav(
|
||||
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: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 10, 16, 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Ciao + nome
|
||||
Text(
|
||||
_name.isNotEmpty ? 'Ciao, $_name' : 'Ciao',
|
||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w900),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
body: YogibookBackground(
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 10, 16, 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
|
||||
// Nome scuola + indirizzo sotto
|
||||
Text(
|
||||
widget.school.name,
|
||||
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w900),
|
||||
),
|
||||
if (_schoolAddress.isNotEmpty) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
_schoolAddress,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.black54,
|
||||
fontWeight: FontWeight.w600,
|
||||
children: [
|
||||
// Ciao + nome
|
||||
Text(
|
||||
_name.isNotEmpty ? 'Ciao, $_name' : 'Ciao',
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 10),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
// Pulsante mappe
|
||||
OutlinedButton.icon(
|
||||
onPressed: _openMaps,
|
||||
icon: const Icon(Icons.directions, size: 18, color: kGreen),
|
||||
label: const Text(
|
||||
'Apri Mappe',
|
||||
style: TextStyle(fontWeight: FontWeight.w800, color: kGreen),
|
||||
),
|
||||
style: OutlinedButton.styleFrom(
|
||||
side: const BorderSide(color: kGreen, width: 2),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
// Nome scuola + indirizzo sotto
|
||||
Text(
|
||||
widget.school.name,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 14),
|
||||
|
||||
// Tiles
|
||||
_HomeTile(
|
||||
icon: Icons.event_note_rounded,
|
||||
title: 'Vedi lezioni',
|
||||
subtitle: 'Prenotazioni e gestione lezioni',
|
||||
onTap: _goLessons,
|
||||
),
|
||||
_HomeTile(
|
||||
icon: Icons.shopping_bag_rounded,
|
||||
title: 'Vedi ordini',
|
||||
subtitle: 'Storico e stato ordini (TODO)',
|
||||
onTap: () => ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(const SnackBar(content: Text('TODO: Ordini'))),
|
||||
),
|
||||
_HomeTile(
|
||||
icon: Icons.person_rounded,
|
||||
title: 'Account',
|
||||
subtitle: 'Profilo e impostazioni (TODO)',
|
||||
onTap: () => ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(const SnackBar(content: Text('TODO: Account'))),
|
||||
),
|
||||
|
||||
const Spacer(),
|
||||
|
||||
// Logo + frase zen
|
||||
Center(
|
||||
child: Column(
|
||||
children: [
|
||||
if (_schoolAddress.isNotEmpty) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'“$zenQuote”',
|
||||
textAlign: TextAlign.center,
|
||||
_schoolAddress,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
height: 1.3,
|
||||
color: Colors.black87,
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 12,
|
||||
color: Colors.black54,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
Image.asset('assets/images/yogibook_logo.png', height: 78),
|
||||
const SizedBox(height: 6),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
// Pulsante mappe
|
||||
OutlinedButton.icon(
|
||||
onPressed: _openMaps,
|
||||
icon: const Icon(Icons.directions, size: 18, color: kGreen),
|
||||
label: const Text(
|
||||
'Apri Mappe',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w800,
|
||||
color: kGreen,
|
||||
),
|
||||
),
|
||||
style: OutlinedButton.styleFrom(
|
||||
side: const BorderSide(color: kGreen, width: 2),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 14),
|
||||
|
||||
// Tiles
|
||||
_HomeTile(
|
||||
icon: Icons.event_note_rounded,
|
||||
title: 'Vedi lezioni',
|
||||
subtitle: 'Prenotazioni e gestione lezioni',
|
||||
onTap: _goLessons,
|
||||
),
|
||||
_HomeTile(
|
||||
icon: Icons.shopping_bag_rounded,
|
||||
title: 'Vedi ordini',
|
||||
subtitle: 'Storico e stato ordini (TODO)',
|
||||
onTap: () => ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(const SnackBar(content: Text('TODO: Ordini'))),
|
||||
),
|
||||
_HomeTile(
|
||||
icon: Icons.person_rounded,
|
||||
title: 'Account',
|
||||
subtitle: 'Profilo e impostazioni (TODO)',
|
||||
onTap: () => ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('TODO: Account')),
|
||||
),
|
||||
),
|
||||
|
||||
const Spacer(),
|
||||
|
||||
// Logo + frase zen
|
||||
Center(
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
'“$zenQuote”',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
height: 1.3,
|
||||
color: Colors.black87,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
Image.asset(
|
||||
'assets/images/yogibook_logo.png',
|
||||
height: 78,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@ -6,6 +6,9 @@ import '../services/vanguard_api.dart';
|
||||
import 'select_school_page.dart';
|
||||
import 'home_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 {
|
||||
final String token;
|
||||
@ -136,18 +139,14 @@ class _LessonsPageState extends State<LessonsPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF6F6FB),
|
||||
drawer: _AppDrawer(
|
||||
drawer: AppDrawer(
|
||||
token: widget.token,
|
||||
onLogout: () {
|
||||
Navigator.of(context).pop();
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(const SnackBar(content: Text('Logout (TODO)')));
|
||||
},
|
||||
school: widget.school,
|
||||
userFirstName: widget.userFirstName,
|
||||
),
|
||||
|
||||
appBar: AppBar(
|
||||
backgroundColor: const Color(0xFFF6F6FB),
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
centerTitle: true,
|
||||
title: Column(
|
||||
@ -185,127 +184,109 @@ class _LessonsPageState extends State<LessonsPage> {
|
||||
),
|
||||
],
|
||||
),
|
||||
bottomNavigationBar: BottomNavigationBar(
|
||||
type: BottomNavigationBarType.fixed, // con 4 icone serve
|
||||
backgroundColor: Colors.white,
|
||||
selectedItemColor: const Color(0xFF10B981),
|
||||
unselectedItemColor: Colors.black54,
|
||||
bottomNavigationBar: AppBottomNav(
|
||||
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: SafeArea(
|
||||
top: false,
|
||||
child: Column(
|
||||
children: [
|
||||
// Address small (optional)
|
||||
if (schoolAddress != null && schoolAddress!.trim().isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 6, 16, 6),
|
||||
child: Text(
|
||||
schoolAddress!,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.black45,
|
||||
fontWeight: FontWeight.w500,
|
||||
body: YogibookBackground(
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
child: Column(
|
||||
children: [
|
||||
// Address small (optional)
|
||||
if (schoolAddress != null && schoolAddress!.trim().isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 6, 16, 6),
|
||||
child: Text(
|
||||
schoolAddress!,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.black45,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Month selector only
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 6, 16, 10),
|
||||
child: _MonthPillCompact(
|
||||
label: _monthLabel(currentMonth),
|
||||
onPrev: prevMonth == null
|
||||
? null
|
||||
: () {
|
||||
setState(() => currentMonth = prevMonth!);
|
||||
_load();
|
||||
},
|
||||
onNext: nextMonth == null
|
||||
? null
|
||||
: () {
|
||||
setState(() => currentMonth = nextMonth!);
|
||||
_load();
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
// Month selector only
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 6, 16, 10),
|
||||
child: _MonthPillCompact(
|
||||
label: _monthLabel(currentMonth),
|
||||
onPrev: prevMonth == null
|
||||
? null
|
||||
: () {
|
||||
setState(() => currentMonth = prevMonth!);
|
||||
_load();
|
||||
},
|
||||
onNext: nextMonth == null
|
||||
? null
|
||||
: () {
|
||||
setState(() => currentMonth = nextMonth!);
|
||||
_load();
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
Expanded(
|
||||
child: loading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: error.isNotEmpty
|
||||
? _ErrorBox(message: error, onRetry: _load)
|
||||
: lessons.isEmpty
|
||||
? _EmptyState(
|
||||
onBrowse: () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('TODO: vedi corsi')),
|
||||
);
|
||||
},
|
||||
)
|
||||
: ListView.builder(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
|
||||
itemCount: lessons.length + 1,
|
||||
itemBuilder: (_, i) {
|
||||
if (i == lessons.length) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 8, bottom: 6),
|
||||
child: Center(
|
||||
child: Image.asset(
|
||||
'assets/images/yogibook_logo.png',
|
||||
height: 56,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: loading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: error.isNotEmpty
|
||||
? _ErrorBox(message: error, onRetry: _load)
|
||||
: lessons.isEmpty
|
||||
? _EmptyState(
|
||||
onBrowse: () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('TODO: vedi corsi')),
|
||||
);
|
||||
}
|
||||
},
|
||||
)
|
||||
: ListView.builder(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
|
||||
itemCount: lessons.length + 1,
|
||||
itemBuilder: (_, i) {
|
||||
if (i == lessons.length) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 8, bottom: 6),
|
||||
child: Center(
|
||||
child: Image.asset(
|
||||
'assets/images/yogibook_logo.png',
|
||||
height: 56,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final l = lessons[i];
|
||||
return _LessonGreenCardCompact(
|
||||
lesson: l,
|
||||
weekday: _weekdayLabel(l.date),
|
||||
dayNum: _dayNum(l.date),
|
||||
onReschedule: l.canModify
|
||||
? () =>
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Riprogramma: API dopo'),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
onCancel: l.canModify
|
||||
? () =>
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Cancella: API dopo'),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
final l = lessons[i];
|
||||
return _LessonGreenCardCompact(
|
||||
lesson: l,
|
||||
weekday: _weekdayLabel(l.date),
|
||||
dayNum: _dayNum(l.date),
|
||||
onReschedule: l.canModify
|
||||
? () => ScaffoldMessenger.of(context)
|
||||
.showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text(
|
||||
'Riprogramma: API dopo',
|
||||
),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
onCancel: l.canModify
|
||||
? () => ScaffoldMessenger.of(context)
|
||||
.showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Cancella: API dopo'),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
// lib/screens/medical_certificates_page.dart
|
||||
import 'dart:io';
|
||||
|
||||
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:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../models/school.dart';
|
||||
import '../services/medical_certificates_api.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 {
|
||||
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
|
||||
State<MedicalCertificatesPage> createState() =>
|
||||
@ -19,6 +36,8 @@ class MedicalCertificatesPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _MedicalCertificatesPageState extends State<MedicalCertificatesPage> {
|
||||
static const Color kBg = Color(0xFFF6F6FB);
|
||||
|
||||
bool loading = true;
|
||||
String error = '';
|
||||
List<Map<String, dynamic>> certs = [];
|
||||
@ -31,6 +50,9 @@ class _MedicalCertificatesPageState extends State<MedicalCertificatesPage> {
|
||||
DateTime? expiryDate;
|
||||
bool uploading = false;
|
||||
|
||||
// ✅ questa pagina è "sezione account"
|
||||
int bottomIndex = 2;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@ -60,7 +82,6 @@ class _MedicalCertificatesPageState extends State<MedicalCertificatesPage> {
|
||||
}
|
||||
|
||||
Future<void> _pickFromChooser() async {
|
||||
// 1 solo bottone -> bottom sheet scelta
|
||||
final choice = await showModalBottomSheet<String>(
|
||||
context: context,
|
||||
showDragHandle: true,
|
||||
@ -123,7 +144,6 @@ class _MedicalCertificatesPageState extends State<MedicalCertificatesPage> {
|
||||
setState(() {
|
||||
pickedFile = f;
|
||||
pickedLabel = label;
|
||||
// reset campi per nuovo upload
|
||||
docNameCtrl.text = 'certificato';
|
||||
notesCtrl.clear();
|
||||
expiryDate = null;
|
||||
@ -182,7 +202,6 @@ class _MedicalCertificatesPageState extends State<MedicalCertificatesPage> {
|
||||
notes: notesCtrl.text,
|
||||
);
|
||||
|
||||
// reset form
|
||||
setState(() {
|
||||
pickedFile = null;
|
||||
pickedLabel = '';
|
||||
@ -205,10 +224,21 @@ class _MedicalCertificatesPageState extends State<MedicalCertificatesPage> {
|
||||
}
|
||||
|
||||
Future<void> _openFileUrl(String url) async {
|
||||
// url arriva tipo "/userarea/certificate/xxx"
|
||||
// per aprirlo serve url assoluto
|
||||
final abs = '${ApiConfig.scheme}://${ApiConfig.host}$url';
|
||||
// url tipico: "/userarea/certificate/xxx"
|
||||
// fix plural errato se presente
|
||||
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 ok = await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||
if (!ok && mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
@ -256,10 +286,67 @@ class _MedicalCertificatesPageState extends State<MedicalCertificatesPage> {
|
||||
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
// ✅ Drawer standard (come MeditationPage)
|
||||
drawer: AppDrawer(
|
||||
token: widget.token,
|
||||
school: widget.school,
|
||||
userFirstName: widget.userFirstName,
|
||||
),
|
||||
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
title: const Text(
|
||||
'Certificati medici',
|
||||
style: TextStyle(fontWeight: FontWeight.w900),
|
||||
@ -271,206 +358,223 @@ class _MedicalCertificatesPageState extends State<MedicalCertificatesPage> {
|
||||
),
|
||||
],
|
||||
),
|
||||
body: RefreshIndicator(
|
||||
onRefresh: _reload,
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.fromLTRB(16, 14, 16, 22),
|
||||
children: [
|
||||
// 1 SOLO bottone in alto
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: uploading ? null : _pickFromChooser,
|
||||
icon: const Icon(Icons.cloud_upload),
|
||||
label: const Text('Carica certificato'),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
// ✅ Bottom nav standard (come MeditationPage)
|
||||
bottomNavigationBar: AppBottomNav(
|
||||
currentIndex: bottomIndex,
|
||||
onTap: _handleBottomNav,
|
||||
),
|
||||
|
||||
// Preview + form (solo se file selezionato)
|
||||
if (pickedFile != null) ...[
|
||||
_UploadCard(
|
||||
filename: pickedLabel.isNotEmpty
|
||||
? pickedLabel
|
||||
: pickedFile!.path.split(Platform.pathSeparator).last,
|
||||
isImage: _isImagePath(pickedFile!.path),
|
||||
isPdf: _isPdfPath(pickedFile!.path),
|
||||
file: pickedFile!,
|
||||
documentNameController: docNameCtrl,
|
||||
notesController: notesCtrl,
|
||||
expiryDate: expiryDate,
|
||||
onPickExpiry: _pickExpiry,
|
||||
onUpload: uploading ? null : _doUpload,
|
||||
uploading: uploading,
|
||||
expiryLabel: expiryDate == null
|
||||
? 'Seleziona data'
|
||||
: _fmtDDMMYYYY(expiryDate!),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
],
|
||||
|
||||
if (error.isNotEmpty) ...[
|
||||
Text(error, style: const TextStyle(color: Colors.red)),
|
||||
const SizedBox(height: 10),
|
||||
],
|
||||
|
||||
Text(
|
||||
'Certificati caricati (${certs.length})',
|
||||
style: const TextStyle(fontWeight: FontWeight.w900, fontSize: 16),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
if (loading)
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(top: 30),
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
)
|
||||
else if (certs.isEmpty)
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(top: 26),
|
||||
child: Center(child: Text('Nessun certificato caricato')),
|
||||
)
|
||||
else
|
||||
...certs.map((c) {
|
||||
final expired = c['is_expired'] == true;
|
||||
final docName = (c['document_name'] ?? '').toString();
|
||||
final filename = (c['filename'] ?? '').toString();
|
||||
final uploadedAt = (c['uploaded_at'] ?? '').toString();
|
||||
final expiry = (c['expiry_date'] ?? '').toString();
|
||||
final notes = (c['notes'] ?? '').toString();
|
||||
final fileUrl = (c['file_url'] ?? '').toString();
|
||||
|
||||
return Card(
|
||||
elevation: 0,
|
||||
color: expired ? const Color(0xFFFFEBEE) : Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
body: YogibookBackground(
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
child: RefreshIndicator(
|
||||
onRefresh: _reload,
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.fromLTRB(16, 14, 16, 22),
|
||||
children: [
|
||||
// 1 SOLO bottone in alto
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: uploading ? null : _pickFromChooser,
|
||||
icon: const Icon(Icons.cloud_upload),
|
||||
label: const Text('Carica certificato'),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(14, 12, 14, 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Preview + form (solo se file selezionato)
|
||||
if (pickedFile != null) ...[
|
||||
_UploadCard(
|
||||
filename: pickedLabel.isNotEmpty
|
||||
? pickedLabel
|
||||
: pickedFile!.path.split(Platform.pathSeparator).last,
|
||||
isImage: _isImagePath(pickedFile!.path),
|
||||
isPdf: _isPdfPath(pickedFile!.path),
|
||||
file: pickedFile!,
|
||||
documentNameController: docNameCtrl,
|
||||
notesController: notesCtrl,
|
||||
expiryDate: expiryDate,
|
||||
onPickExpiry: _pickExpiry,
|
||||
onUpload: uploading ? null : _doUpload,
|
||||
uploading: uploading,
|
||||
expiryLabel: expiryDate == null
|
||||
? 'Seleziona data'
|
||||
: _fmtDDMMYYYY(expiryDate!),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
],
|
||||
|
||||
if (error.isNotEmpty) ...[
|
||||
Text(error, style: const TextStyle(color: Colors.red)),
|
||||
const SizedBox(height: 10),
|
||||
],
|
||||
|
||||
Text(
|
||||
'Certificati caricati (${certs.length})',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w900,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
if (loading)
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(top: 30),
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
)
|
||||
else if (certs.isEmpty)
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(top: 26),
|
||||
child: Center(child: Text('Nessun certificato caricato')),
|
||||
)
|
||||
else
|
||||
...certs.map((c) {
|
||||
final expired = c['is_expired'] == true;
|
||||
final docName = (c['document_name'] ?? '').toString();
|
||||
final filename = (c['filename'] ?? '').toString();
|
||||
final uploadedAt = (c['uploaded_at'] ?? '').toString();
|
||||
final expiry = (c['expiry_date'] ?? '').toString();
|
||||
final notes = (c['notes'] ?? '').toString();
|
||||
final fileUrl = (c['file_url'] ?? '').toString();
|
||||
|
||||
return Card(
|
||||
elevation: 0,
|
||||
color: expired ? const Color(0xFFFFEBEE) : Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(14, 12, 14, 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
docName.isNotEmpty ? docName : 'certificato',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w900,
|
||||
fontSize: 15,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (expired)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFDC3545),
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
),
|
||||
child: const Text(
|
||||
'SCADUTO',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w900,
|
||||
fontSize: 11,
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
docName.isNotEmpty
|
||||
? docName
|
||||
: 'certificato',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w900,
|
||||
fontSize: 15,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
filename,
|
||||
style: const TextStyle(
|
||||
color: Colors.black54,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.event,
|
||||
size: 16,
|
||||
color: Colors.black54,
|
||||
if (expired)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFDC3545),
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
),
|
||||
child: const Text(
|
||||
'SCADUTO',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w900,
|
||||
fontSize: 11,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Caricato: $uploadedAt',
|
||||
style: const TextStyle(
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
filename,
|
||||
style: const TextStyle(
|
||||
color: Colors.black54,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.event,
|
||||
size: 16,
|
||||
color: Colors.black54,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.timer,
|
||||
size: 16,
|
||||
color: Colors.black54,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Scadenza: ${expiry.isEmpty ? '—' : expiry}',
|
||||
style: TextStyle(
|
||||
color: expired
|
||||
? const Color(0xFFDC3545)
|
||||
: Colors.black54,
|
||||
fontSize: 12,
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Caricato: $uploadedAt',
|
||||
style: const TextStyle(
|
||||
color: Colors.black54,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.timer,
|
||||
size: 16,
|
||||
color: Colors.black54,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Scadenza: ${expiry.isEmpty ? '—' : expiry}',
|
||||
style: TextStyle(
|
||||
color: expired
|
||||
? const Color(0xFFDC3545)
|
||||
: Colors.black54,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
if (notes.trim().isNotEmpty) ...[
|
||||
const SizedBox(height: 10),
|
||||
Text(notes, style: const TextStyle(fontSize: 12)),
|
||||
],
|
||||
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
children: [
|
||||
TextButton.icon(
|
||||
onPressed: () => _openFileUrl(fileUrl),
|
||||
icon: const Icon(Icons.open_in_new),
|
||||
label: const Text('Apri'),
|
||||
),
|
||||
const Spacer(),
|
||||
TextButton.icon(
|
||||
onPressed: () =>
|
||||
_deleteCert((c['id'] as num).toInt()),
|
||||
icon: const Icon(
|
||||
Icons.delete_outline,
|
||||
color: Colors.red,
|
||||
),
|
||||
label: const Text(
|
||||
'Elimina',
|
||||
style: TextStyle(color: Colors.red),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
if (notes.trim().isNotEmpty) ...[
|
||||
const SizedBox(height: 10),
|
||||
Text(notes, style: const TextStyle(fontSize: 12)),
|
||||
],
|
||||
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
children: [
|
||||
TextButton.icon(
|
||||
onPressed: () => _openFileUrl(fileUrl),
|
||||
icon: const Icon(Icons.open_in_new),
|
||||
label: const Text('Apri'),
|
||||
),
|
||||
const Spacer(),
|
||||
TextButton.icon(
|
||||
onPressed: () =>
|
||||
_deleteCert((c['id'] as num).toInt()),
|
||||
icon: const Icon(
|
||||
Icons.delete_outline,
|
||||
color: Colors.red,
|
||||
),
|
||||
label: const Text(
|
||||
'Elimina',
|
||||
style: TextStyle(color: Colors.red),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -567,17 +671,12 @@ class _UploadCard extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: onPickExpiry,
|
||||
icon: const Icon(Icons.date_range),
|
||||
label: Text(expiryLabel),
|
||||
),
|
||||
),
|
||||
],
|
||||
OutlinedButton.icon(
|
||||
onPressed: onPickExpiry,
|
||||
icon: const Icon(Icons.date_range),
|
||||
label: Text(expiryLabel),
|
||||
),
|
||||
|
||||
const SizedBox(height: 10),
|
||||
|
||||
TextField(
|
||||
|
||||
@ -8,6 +8,9 @@ import 'home_page.dart';
|
||||
import 'lessons_page.dart';
|
||||
import 'select_school_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 {
|
||||
final String token;
|
||||
@ -288,41 +291,14 @@ class _MeditationPageState extends State<MeditationPage>
|
||||
).animate(CurvedAnimation(parent: _pulseCtrl, curve: Curves.easeInOut));
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: kBg,
|
||||
drawer: Drawer(
|
||||
child: SafeArea(
|
||||
child: ListView(
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
drawer: AppDrawer(
|
||||
token: widget.token,
|
||||
school: widget.school,
|
||||
userFirstName: widget.userFirstName,
|
||||
),
|
||||
|
||||
appBar: AppBar(
|
||||
backgroundColor: kBg,
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
centerTitle: true,
|
||||
title: const Text(
|
||||
@ -381,203 +357,205 @@ class _MeditationPageState extends State<MeditationPage>
|
||||
),
|
||||
],
|
||||
),
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 10, 16, 16),
|
||||
child: Column(
|
||||
children: [
|
||||
// Top card with progress
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.fromLTRB(14, 12, 14, 14),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
blurRadius: 18,
|
||||
color: Color(0x12000000),
|
||||
offset: Offset(0, 10),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Respiro consapevole',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w900,
|
||||
body: YogibookBackground(
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 10, 16, 16),
|
||||
child: Column(
|
||||
children: [
|
||||
// Top card with progress
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.fromLTRB(14, 12, 14, 14),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
blurRadius: 18,
|
||||
color: Color(0x12000000),
|
||||
offset: Offset(0, 10),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
child: LinearProgressIndicator(
|
||||
minHeight: 8,
|
||||
value: _progress,
|
||||
backgroundColor: const Color(0xFFE9E9EF),
|
||||
valueColor: const AlwaysStoppedAnimation(
|
||||
Color(0xFFF59E0B),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Respiro consapevole',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 8),
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
child: LinearProgressIndicator(
|
||||
minHeight: 8,
|
||||
value: _progress,
|
||||
backgroundColor: const Color(0xFFE9E9EF),
|
||||
valueColor: const AlwaysStoppedAnimation(
|
||||
Color(0xFFF59E0B),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Square + countdown
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 8, bottom: 8),
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
AnimatedBuilder(
|
||||
animation: Listenable.merge([
|
||||
_squareCtrl,
|
||||
bubbleScale,
|
||||
]),
|
||||
builder: (_, __) {
|
||||
final scale = bubbleScale.value;
|
||||
// Square + countdown
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 8, bottom: 8),
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
AnimatedBuilder(
|
||||
animation: Listenable.merge([
|
||||
_squareCtrl,
|
||||
bubbleScale,
|
||||
]),
|
||||
builder: (_, __) {
|
||||
final scale = bubbleScale.value;
|
||||
|
||||
return Transform.scale(
|
||||
scale: scale,
|
||||
child: CustomPaint(
|
||||
painter: _SquareBreathPainter(
|
||||
t: _squareCtrl.value,
|
||||
color: kGreen,
|
||||
),
|
||||
child: SizedBox(
|
||||
width: 240,
|
||||
height: 240,
|
||||
child: Center(
|
||||
child: Text(
|
||||
_breathLabel,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w800,
|
||||
fontSize: 18,
|
||||
return Transform.scale(
|
||||
scale: scale,
|
||||
child: CustomPaint(
|
||||
painter: _SquareBreathPainter(
|
||||
t: _squareCtrl.value,
|
||||
color: kGreen,
|
||||
),
|
||||
child: SizedBox(
|
||||
width: 240,
|
||||
height: 240,
|
||||
child: Center(
|
||||
child: Text(
|
||||
_breathLabel,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w800,
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
Text(
|
||||
_mmss(remainingSeconds),
|
||||
style: const TextStyle(
|
||||
fontSize: 44,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: Color(0xFF1F1F1F),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
const Text(
|
||||
'Tempo rimanente',
|
||||
style: TextStyle(
|
||||
color: Colors.black45,
|
||||
fontWeight: FontWeight.w600,
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
Text(
|
||||
_mmss(remainingSeconds),
|
||||
style: const TextStyle(
|
||||
fontSize: 44,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: Color(0xFF1F1F1F),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 18),
|
||||
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 44,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: running ? _pause : _start,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: kGreen,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
),
|
||||
icon: Icon(
|
||||
running ? Icons.pause : Icons.play_arrow,
|
||||
),
|
||||
label: Text(running ? 'Pausa' : 'Avvia'),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
const Text(
|
||||
'Tempo rimanente',
|
||||
style: TextStyle(
|
||||
color: Colors.black45,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
SizedBox(
|
||||
height: 44,
|
||||
child: OutlinedButton(
|
||||
onPressed: _reset,
|
||||
style: OutlinedButton.styleFrom(
|
||||
side: const BorderSide(
|
||||
color: kGreen,
|
||||
width: 2,
|
||||
),
|
||||
|
||||
const SizedBox(height: 18),
|
||||
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 44,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: running ? _pause : _start,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: kGreen,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
icon: Icon(
|
||||
running ? Icons.pause : Icons.play_arrow,
|
||||
),
|
||||
label: Text(running ? 'Pausa' : 'Avvia'),
|
||||
),
|
||||
child: const Text(
|
||||
'Reset',
|
||||
style: TextStyle(
|
||||
color: Colors.black87,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
SizedBox(
|
||||
height: 44,
|
||||
child: OutlinedButton(
|
||||
onPressed: _reset,
|
||||
style: OutlinedButton.styleFrom(
|
||||
side: const BorderSide(
|
||||
color: kGreen,
|
||||
width: 2,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
'Reset',
|
||||
style: TextStyle(
|
||||
color: Colors.black87,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 52,
|
||||
child: ElevatedButton(
|
||||
onPressed: () async {
|
||||
_pause();
|
||||
await _stopMusic();
|
||||
if (!mounted) return;
|
||||
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => HomePage(
|
||||
token: widget.token,
|
||||
school: widget.school,
|
||||
userFirstName: widget.userFirstName,
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: kGreen,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
'TERMINA SESSIONE',
|
||||
style: TextStyle(fontWeight: FontWeight.w900),
|
||||
),
|
||||
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 52,
|
||||
child: ElevatedButton(
|
||||
onPressed: () async {
|
||||
_pause();
|
||||
await _stopMusic();
|
||||
if (!mounted) return;
|
||||
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => HomePage(
|
||||
token: widget.token,
|
||||
school: widget.school,
|
||||
userFirstName: widget.userFirstName,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: kGreen,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
'TERMINA SESSIONE',
|
||||
style: TextStyle(fontWeight: FontWeight.w900),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@ -1,10 +1,14 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../models/school.dart';
|
||||
import '../services/vanguard_api.dart';
|
||||
import 'lessons_page.dart';
|
||||
import 'home_page.dart';
|
||||
import 'meditation_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 {
|
||||
final String token;
|
||||
@ -15,7 +19,6 @@ class SelectSchoolPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _SelectSchoolPageState extends State<SelectSchoolPage> {
|
||||
static const Color kBg = Color(0xFFF6F6FB);
|
||||
static const Color kGreen = Color(0xFF10B981);
|
||||
|
||||
bool loading = true;
|
||||
@ -23,8 +26,6 @@ class _SelectSchoolPageState extends State<SelectSchoolPage> {
|
||||
String? firstName;
|
||||
List<School> schools = [];
|
||||
|
||||
int bottomIndex = 1; // 0=Home, 1=Lezioni, 2=Account (qui siamo in "Lezioni")
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@ -51,26 +52,50 @@ class _SelectSchoolPageState extends State<SelectSchoolPage> {
|
||||
// Auto-select if API says so
|
||||
final autoSelect = data['auto_select'] == true;
|
||||
final selectedId = data['selected_school_id'];
|
||||
|
||||
if (autoSelect && selectedId != null && schools.isNotEmpty) {
|
||||
final id = (selectedId as num).toInt();
|
||||
final s = schools.firstWhere((x) => x.id == id);
|
||||
_enterSchool(s);
|
||||
await _enterSchool(s); // ✅ ora async
|
||||
}
|
||||
} catch (e) {
|
||||
setState(() => error = 'Errore: $e');
|
||||
} finally {
|
||||
setState(() => loading = false);
|
||||
if (mounted) setState(() => loading = false);
|
||||
}
|
||||
}
|
||||
|
||||
void _enterSchool(School s) {
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) =>
|
||||
HomePage(token: widget.token, school: s, userFirstName: firstName),
|
||||
),
|
||||
);
|
||||
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(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => HomePage(
|
||||
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();
|
||||
@ -97,31 +122,20 @@ class _SelectSchoolPageState extends State<SelectSchoolPage> {
|
||||
|
||||
if (ok != true) return;
|
||||
|
||||
// chiude il drawer se aperto
|
||||
Navigator.of(context).pop();
|
||||
|
||||
// chiama API logout (se fallisce usciamo lo stesso)
|
||||
try {
|
||||
await VanguardApi.logout(token: widget.token);
|
||||
} catch (_) {}
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
// torna a Login e cancella tutta la stack
|
||||
Navigator.of(context).pushAndRemoveUntil(
|
||||
MaterialPageRoute(builder: (_) => const LoginPage()),
|
||||
(route) => false,
|
||||
);
|
||||
}
|
||||
|
||||
void _handleBottomNav(int i) {
|
||||
setState(() => bottomIndex = i);
|
||||
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(const SnackBar(content: Text('Prima seleziona la scuola')));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
@ -153,84 +167,20 @@ 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(
|
||||
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 della pagina (quello che avevi prima)
|
||||
SafeArea(
|
||||
top: false,
|
||||
child: loading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: error.isNotEmpty
|
||||
? _ErrorBox(message: error, onRetry: _load)
|
||||
: _SchoolsList(
|
||||
firstName: firstName,
|
||||
schools: schools,
|
||||
onSelect: _enterSchool,
|
||||
),
|
||||
),
|
||||
],
|
||||
body: YogibookBackground(
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
child: loading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: error.isNotEmpty
|
||||
? _ErrorBox(message: error, onRetry: _load)
|
||||
: _SchoolsList(
|
||||
firstName: firstName,
|
||||
schools: schools,
|
||||
onSelect: (s) => _enterSchool(s),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -261,7 +211,7 @@ class _AppDrawer extends StatelessWidget {
|
||||
ListTile(
|
||||
leading: const Icon(Icons.swap_horiz),
|
||||
title: const Text('Cambia scuola'),
|
||||
onTap: () => Navigator.of(context).pop(), // sei già qui
|
||||
onTap: () => Navigator.of(context).pop(),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
ListTile(
|
||||
@ -296,7 +246,6 @@ class _SchoolsList extends StatelessWidget {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// header compatto (stesso stile “soft” delle lezioni)
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 10),
|
||||
child: Column(
|
||||
@ -334,6 +283,7 @@ class _SchoolsList extends StatelessWidget {
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 8),
|
||||
Center(
|
||||
child: Padding(
|
||||
|
||||
46
lib/services/settings_api.dart
Normal file
46
lib/services/settings_api.dart
Normal 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>(),
|
||||
);
|
||||
}
|
||||
}
|
||||
56
lib/state/app_settings_store.dart
Normal file
56
lib/state/app_settings_store.dart
Normal 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
104
lib/state/app_state.dart
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
39
lib/widgets/app_bottom_nav.dart
Normal file
39
lib/widgets/app_bottom_nav.dart
Normal 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
271
lib/widgets/app_drawer.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
88
lib/widgets/yogibook_background.dart
Normal file
88
lib/widgets/yogibook_background.dart
Normal 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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -10,6 +10,7 @@ import file_picker
|
||||
import file_selector_macos
|
||||
import google_sign_in_ios
|
||||
import path_provider_foundation
|
||||
import shared_preferences_foundation
|
||||
import url_launcher_macos
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
@ -18,5 +19,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
||||
FLTGoogleSignInPlugin.register(with: registry.registrar(forPlugin: "FLTGoogleSignInPlugin"))
|
||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||
}
|
||||
|
||||
72
pubspec.lock
72
pubspec.lock
@ -429,6 +429,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
nested:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: nested
|
||||
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -501,6 +509,70 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
|
||||
@ -42,6 +42,8 @@ dependencies:
|
||||
file_picker: ^8.0.7
|
||||
image_picker: ^1.1.2
|
||||
mime: ^2.0.0
|
||||
provider: ^6.1.5+1
|
||||
shared_preferences: ^2.2.3
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user