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/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 {
|
||||||
|
|||||||
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 '../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> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
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 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"))
|
||||||
}
|
}
|
||||||
|
|||||||
72
pubspec.lock
72
pubspec.lock
@ -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
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user