import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flux/core/utils/version_check_service.dart'; import 'package:flux/features/attachments/data/attachments_repository.dart'; import 'package:flux/features/auth/bloc/auth_cubit.dart'; import 'package:flux/features/company/data/company_repository.dart'; import 'package:flux/features/notes/blocs/notes_bloc.dart'; import 'package:flux/features/notes/data/notes_repository.dart'; import 'package:flux/features/settings/data/settings_repository.dart'; import 'package:flux/features/tasks/data/task_repository.dart'; import 'package:flux/features/tickets/data/tickets_shipping_repository.dart'; import 'package:flux/features/master_data/providers/blocs/provider_list_cubit.dart'; import 'package:flux/features/operations/blocs/operation_list_cubit.dart'; import 'package:flux/features/operations/data/operations_repository.dart'; import 'package:flux/features/settings/blocs/settings_cubit.dart'; import 'package:flux/features/settings/document_sequence/data/document_sequence_repository.dart'; import 'package:flux/features/tickets/blocs/ticket_list_cubit.dart'; import 'package:flux/features/tickets/data/ticket_repository.dart'; import 'package:flux/features/tracking/blocs/tracking_cubit.dart'; import 'package:flux/features/tracking/data/tracking_repository.dart'; import 'package:flux/l10n/app_localizations.dart'; import 'package:get_it/get_it.dart'; import 'package:go_router/go_router.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:flux/core/data/core_repository.dart'; import 'package:flux/core/routes/app_router.dart'; import 'package:flux/core/theme/theme.dart'; import 'package:flux/core/theme/bloc/theme_bloc.dart'; import 'package:flux/features/customers/blocs/customers_list_cubit.dart'; import 'package:flux/features/customers/data/customer_repository.dart'; import 'package:flux/features/master_data/products/blocs/product_cubit.dart'; import 'package:flux/features/master_data/products/data/product_repository.dart'; import 'package:flux/features/master_data/providers/data/provider_repository.dart'; import 'package:flux/features/master_data/staff/blocs/staff_cubit.dart'; import 'package:flux/features/master_data/staff/data/staff_repository.dart'; import 'package:flux/features/master_data/store/bloc/store_cubit.dart'; import 'package:flux/features/master_data/store/data/store_repository.dart'; import 'package:flux/features/settings/ui/settings.dart'; import 'package:flutter_web_plugins/url_strategy.dart'; import 'package:url_launcher/url_launcher.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); await dotenv.load(fileName: ".env"); // Inizializza le dipendenze PRIMA di lanciare l'app await setupLocator(); // RIMUOVE IL CARATTERE # DAGLI URL WEB! usePathUrlStrategy(); runApp( MultiBlocProvider( providers: [ BlocProvider(create: (context) => AuthCubit()), BlocProvider( create: (context) => ThemeBloc()..add(LoadThemeEvent()), ), // Il Vigile Urbano viene inizializzato! BlocProvider(create: (_) => GetIt.I()), // Cubit delle feature BlocProvider(create: (_) => StoreCubit()), BlocProvider(create: (_) => CustomersListCubit()), BlocProvider(create: (_) => ProductsCubit()), BlocProvider( create: (_) => StaffCubit() ..loadStaffForStore( GetIt.I.get().state.currentStore!.id!, ), ), BlocProvider(create: (_) => OperationListCubit()), BlocProvider(create: (_) => ProviderListCubit()), BlocProvider(create: (_) => SettingsCubit()), BlocProvider(create: (_) => TicketListCubit()), BlocProvider(create: (_) => OperationListCubit()), BlocProvider(create: (_) => TrackingCubit()), BlocProvider(create: (_) => NotesBloc()), ], child: const FluxApp(), ), ); } Future setupLocator() async { final GetIt getIt = GetIt.instance; getIt.registerSingleton( await SharedPreferences.getInstance(), ); await Supabase.initialize( url: dotenv.env['SUPABASE_URL'] ?? '', anonKey: dotenv.env['SUPABASE_ANON_KEY'] ?? '', ); //await Supabase.instance.client.auth.signOut(); getIt.registerSingleton(Supabase.instance.client); // Settings getIt.registerLazySingleton(() => AppSettings()); // Repositories getIt.registerLazySingleton( () => CoreRepository(), ); // <-- NUOVO getIt.registerLazySingleton(() => StoreRepository()); getIt.registerLazySingleton(() => CustomerRepository()); getIt.registerLazySingleton(() => ProductRepository()); getIt.registerLazySingleton(() => StaffRepository()); getIt.registerLazySingleton( () => OperationsRepository(), ); getIt.registerLazySingleton(() => ProviderRepository()); getIt.registerLazySingleton( () => AttachmentsRepository(), ); getIt.registerLazySingleton(() => TicketRepository()); getIt.registerLazySingleton( () => DocumentSequenceRepository(), ); // NOTA: CompanyRepository l'ho tolto perché la logica della Company // ora è gestita dal CoreRepository durante l'Onboarding. // Se ti serve per altro, rimettilo pure! // Inizializziamo il SessionCubit (che prende CoreRepository e SharedPreferences) // Usiamo registerSingleton così viene creato subito e inizia ad ascoltare Supabase Auth. getIt.registerSingleton( SessionCubit(getIt(), getIt()), ); getIt.registerLazySingleton(() => CompanyRepository()); getIt.registerLazySingleton(() => TrackingRepository()); getIt.registerLazySingleton( () => TicketsShippingRepository(), ); getIt.registerLazySingleton(() => NotesRepository()); getIt.registerLazySingleton(() => TaskRepository()); getIt.registerLazySingleton(() => SettingsRepository()); } class FluxApp extends StatefulWidget { const FluxApp({super.key}); @override State createState() => _FluxAppState(); } class _FluxAppState extends State { late final GoRouter _router; @override void initState() { super.initState(); // Creiamo il router passandogli il Cubit per i redirect _router = AppRouter.createRouter(context.read()); GetIt.I.get().setIsMobileDevice(isMobileDevice(context)); } bool isMobileDevice(BuildContext context) { if (kIsWeb) { return false; // Il web non lo consideriamo "mobile nativo" per i deep link } return Platform.isAndroid || Platform.isIOS; } @override Widget build(BuildContext context) { return BlocConsumer( listenWhen: (previous, current) => previous.status != SessionStatus.authenticated && current.status == SessionStatus.authenticated, listener: (context, state) { context.read().loadStores(); context.read().loadAllStaff(); }, builder: (context, sessionState) { if (sessionState.status == SessionStatus.initial) { return _buildLoadingScreen(); } return BlocBuilder( builder: (context, themeState) { return MaterialApp.router( title: 'FLUX Gestionale', debugShowCheckedModeBanner: false, theme: fluxLightTheme, darkTheme: fluxDarkTheme, themeMode: themeState.currentTheme.themeMode, routerConfig: _router, localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, locale: const Locale('it'), // 🥷 ECCO LA MAGIA: Avvolgiamo tutta l'app nel nostro checker! builder: (context, child) { return GlobalUpdateChecker(child: child!); }, ); }, ); }, ); } Widget _buildLoadingScreen() { return MaterialApp( debugShowCheckedModeBanner: false, home: Scaffold( body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.bolt, size: 64, color: Colors.blue), const SizedBox(height: 24), const CircularProgressIndicator(), const SizedBox(height: 16), const Text( "Inizializzazione sessione...", style: TextStyle( fontWeight: FontWeight.bold, color: Colors.grey, ), ), ], ), ), ), ); } } // --- IL WIDGET GUARDIANO DEGLI AGGIORNAMENTI --- class GlobalUpdateChecker extends StatefulWidget { final Widget child; const GlobalUpdateChecker({super.key, required this.child}); @override State createState() => _GlobalUpdateCheckerState(); } class _GlobalUpdateCheckerState extends State { bool _mustUpdate = false; String? _updateUrl; @override void initState() { super.initState(); _checkVersionAndBlock(); } Future _checkVersionAndBlock() async { final updateUrl = await VersionCheckService().checkForceUpdate(); if (updateUrl != null && mounted) { // Invece di aprire un dialog, cambiamo lo stato e attiviamo lo "Scudo" setState(() { _mustUpdate = true; _updateUrl = updateUrl; }); } } @override Widget build(BuildContext context) { // 1. Se l'app è aggiornata, mostriamo solo l'app normale if (!_mustUpdate) return widget.child; // 2. Se l'app è vecchia, sovrapponiamo il blocco con uno Stack return Stack( children: [ // L'app sotto continua ad esistere, ma è inaccessibile widget.child, // IL BLOCCO INVALICABILE SOPRA A TUTTO Positioned.fill( child: Container( color: Colors.black.withValues(alpha: 0.85), // Sfondo oscurante child: Center( // Usiamo Material per ereditare correttamente temi, font e colori child: Material( color: Colors.transparent, child: Container( margin: const EdgeInsets.all(24), padding: const EdgeInsets.all(24), constraints: const BoxConstraints(maxWidth: 400), decoration: BoxDecoration( color: Theme.of(context).scaffoldBackgroundColor, borderRadius: BorderRadius.circular(16), boxShadow: const [ BoxShadow( color: Colors.black54, blurRadius: 20, offset: Offset(0, 10), ), ], ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( kIsWeb ? Icons.cached : Icons.system_update, color: Colors.orange, size: 32, ), const SizedBox(width: 12), Flexible( child: Text( kIsWeb ? "Aggiornamento" : "Aggiornamento Obbligatorio", style: Theme.of(context).textTheme.titleLarge ?.copyWith(fontWeight: FontWeight.bold), ), ), ], ), const SizedBox(height: 16), Text( kIsWeb ? "È stata rilasciata una nuova versione dell'applicazione. Ricarica la pagina per continuare." : "Per continuare ad utilizzare l'applicazione è necessario scaricare e installare l'ultimo aggiornamento.", textAlign: TextAlign.center, style: const TextStyle(fontSize: 16), ), const SizedBox(height: 24), if (kIsWeb) FilledButton.icon( icon: const Icon(Icons.refresh), label: const Text("RICARICA ORA"), style: FilledButton.styleFrom( minimumSize: const Size(double.infinity, 50), ), onPressed: () async { // Trick cross-platform per fare il reload await launchUrl( Uri.parse(Uri.base.toString()), webOnlyWindowName: '_self', ); }, ) else FilledButton.icon( icon: const Icon(Icons.download), label: const Text("SCARICA AGGIORNAMENTO"), style: FilledButton.styleFrom( minimumSize: const Size(double.infinity, 50), backgroundColor: Colors.blue, ), onPressed: () async { if (_updateUrl != null) { final url = Uri.parse(_updateUrl!); await launchUrl( url, mode: LaunchMode.externalApplication, ); } }, ), ], ), ), ), ), ), ), ], ); } }