import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:go_router/go_router.dart'; import 'package:provider/provider.dart'; import 'package:flutter/foundation.dart' show kIsWeb; import 'providers/app_settings_provider.dart'; import 'providers/energy_setup_provider.dart'; import 'providers/installer_mode_provider.dart'; import 'providers/navigation_provider.dart'; import 'providers/scheduler_provider.dart'; import 'providers/tariff_provider.dart'; import 'screens/ac_screen.dart'; import 'screens/dashboard_screen.dart'; import 'screens/drawer/main_drawer.dart'; import 'screens/energy/energy_setup_screen.dart'; import 'screens/energy/scheduler_screen.dart'; import 'screens/energy/tariff_screen.dart'; import 'screens/energy/timeline_screen.dart'; import 'screens/energy_screen.dart'; import 'screens/favorites_screen.dart'; import 'screens/settings/app_settings_screen.dart'; import 'screens/settings/appearance_screen.dart'; import 'screens/settings/screens_settings_screen.dart'; import 'screens/things_screen.dart'; import 'services/nymea_service.dart'; import 'theme/app_theme.dart'; import 'theme/etm_tokens.dart'; // ───────────────────────────────────────────────────────────────────────────── // Router GoRouter // ───────────────────────────────────────────────────────────────────────────── final _router = GoRouter( initialLocation: '/', routes: [ // ── Shell principal (bottom nav + drawer overlay) ───────────────────── ShellRoute( builder: (context, state, child) => MainShell(child: child), routes: [ GoRoute(path: '/', builder: (c, s) => const DashboardScreen()), GoRoute(path: '/energy', builder: (c, s) => const EnergyScreen()), GoRoute(path: '/things', builder: (c, s) => const ThingsScreen()), GoRoute(path: '/ac', builder: (c, s) => const ACScreen()), GoRoute(path: '/favorites', builder: (c, s) => const FavoritesScreen()), // Routes stubées — à implémenter plus tard GoRoute(path: '/groups', builder: (c, s) => _StubScreen('Groupes')), GoRoute(path: '/scenes', builder: (c, s) => _StubScreen('Scènes')), GoRoute(path: '/media', builder: (c, s) => _StubScreen('Médias')), GoRoute(path: '/garages', builder: (c, s) => _StubScreen('Garages')), GoRoute(path: '/automations', builder: (c, s) => _StubScreen('Magic / Automatisations')), GoRoute(path: '/bug-report', builder: (c, s) => _StubScreen('Rapport de bug')), ], ), // ── Écrans énergie (push par-dessus le shell) ───────────────────────── GoRoute( path: '/energy/setup', builder: (c, s) => const EnergySetupScreen(), ), GoRoute( path: '/energy/scheduler', builder: (c, s) => const SchedulerScreen(), ), GoRoute( path: '/energy/tariffs', builder: (c, s) => const TariffScreen(), ), GoRoute( path: '/energy/timeline', builder: (c, s) => const TimelineScreen(), ), // ── Écrans settings (push par-dessus le shell) ──────────────────────── GoRoute( path: '/settings/app', builder: (c, s) => const AppSettingsScreen(), ), GoRoute( path: '/settings/app/appearance', builder: (c, s) => const AppearanceScreen(), ), GoRoute( path: '/settings/app/screens', builder: (c, s) => const ScreensSettingsScreen(), ), GoRoute( path: '/settings/app/developer', builder: (c, s) => _StubScreen('Options développeur'), ), GoRoute( path: '/settings/app/about', builder: (c, s) => _StubScreen('À propos PowerSync'), ), // Routes installateur stubées GoRoute(path: '/settings/system', builder: (c, s) => _StubScreen('Configuration système')), GoRoute(path: '/settings/system/things', builder: (c, s) => _StubScreen('Configuration Things')), GoRoute(path: '/settings/system/network', builder: (c, s) => _StubScreen('Système & réseau')), GoRoute(path: '/settings/system/protocols',builder: (c, s) => _StubScreen('Protocoles')), GoRoute(path: '/settings/system/mqtt', builder: (c, s) => _StubScreen('MQTT / Web server')), GoRoute(path: '/settings/system/plugins', builder: (c, s) => _StubScreen('Plugins')), GoRoute(path: '/settings/system/update', builder: (c, s) => _StubScreen('Mise à jour système')), GoRoute(path: '/settings/system/devtools', builder: (c, s) => _StubScreen('Outils développeur')), GoRoute(path: '/settings/system/about', builder: (c, s) => _StubScreen('À propos ETM')), ], ); // ───────────────────────────────────────────────────────────────────────────── // main() // ───────────────────────────────────────────────────────────────────────────── void main() async { WidgetsFlutterBinding.ensureInitialized(); SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); SystemChrome.setSystemUIOverlayStyle( const SystemUiOverlayStyle( statusBarColor: Colors.transparent, statusBarIconBrightness: Brightness.dark, ), ); // Initialisation des providers qui ont besoin d'I/O asynchrone final appSettings = AppSettingsProvider(); final installerMode = InstallerModeProvider(); await Future.wait([ appSettings.load(), installerMode.init(), ]); runApp( MultiProvider( providers: [ ChangeNotifierProvider(create: (_) => NymeaService()), ChangeNotifierProvider(create: (_) => NavigationProvider()), ChangeNotifierProvider.value(value: installerMode), ChangeNotifierProvider.value(value: appSettings), ChangeNotifierProvider(create: (_) => EnergySetupProvider()), ChangeNotifierProvider(create: (_) => SchedulerProvider()), ChangeNotifierProvider(create: (_) => TariffProvider()), ], child: const NymeaEnergyApp(), ), ); } // ───────────────────────────────────────────────────────────────────────────── // Application root // ───────────────────────────────────────────────────────────────────────────── class NymeaEnergyApp extends StatelessWidget { const NymeaEnergyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp.router( title: 'ETM PowerSync', debugShowCheckedModeBanner: false, theme: AppTheme.theme, routerConfig: _router, builder: (context, child) { // Sur mobile natif, on rend tel quel if (!kIsWeb) return child!; // Sur web : si la fenêtre est déjà étroite (smartphone qui visite la PWA), // on rend plein écran. Sur desktop, on encadre dans un viewport "mobile". final width = MediaQuery.sizeOf(context).width; if (width < 600) return child!; return Container( color: const Color(0xFF0a1f2b), // navy ETM en arrière-plan alignment: Alignment.center, child: Container( constraints: const BoxConstraints(maxWidth: 440, maxHeight: 900), decoration: BoxDecoration( borderRadius: BorderRadius.circular(24), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.5), blurRadius: 40, ), ], ), child: ClipRRect( borderRadius: BorderRadius.circular(24), child: child, ), ), ); }, ); } } // ───────────────────────────────────────────────────────────────────────────── // MainShell — Scaffold principal avec bottom nav + drawer overlay // ───────────────────────────────────────────────────────────────────────────── class MainShell extends StatefulWidget { final Widget child; const MainShell({super.key, required this.child}); @override State createState() => _MainShellState(); } class _MainShellState extends State with SingleTickerProviderStateMixin { late AnimationController _animCtrl; late Animation _slideAnim; late Animation _overlayAnim; // Mapping route → index onglet static const _routeToTab = { '/': 0, '/energy': 1, '/things': 2, '/ac': 3, '/favorites': 4, }; static const _tabToRoute = ['/', '/energy', '/things', '/ac', '/favorites']; static const _navItems = [ _NavItem(icon: Icons.home_rounded, label: 'Dashboard'), _NavItem(icon: Icons.bar_chart_rounded, label: 'Énergie'), _NavItem(icon: Icons.device_hub_rounded, label: 'Things'), _NavItem(icon: Icons.ac_unit_rounded, label: 'A/C'), _NavItem(icon: Icons.star_rounded, label: 'Favoris'), ]; int _currentTab = 0; // Listener GoRouter pour syncer _currentTab sur les navigations du drawer late final GoRouter _goRouter; late final NavigationProvider _navProvider; @override void initState() { super.initState(); _animCtrl = AnimationController( vsync: this, duration: const Duration(milliseconds: 260), ); _slideAnim = CurvedAnimation(parent: _animCtrl, curve: Curves.easeOutCubic); _overlayAnim = CurvedAnimation(parent: _animCtrl, curve: Curves.easeOut); } @override void didChangeDependencies() { super.didChangeDependencies(); // Récupère GoRouter et NavigationProvider une seule fois _goRouter = GoRouter.of(context); _navProvider = context.read(); // Listener GoRouter → sync onglet actif (navigation depuis le drawer) _goRouter.routerDelegate.addListener(_syncTabFromRoute); // Listener NavigationProvider → animation drawer UNIQUEMENT _navProvider.addListener(_onDrawerStateChanged); } @override void dispose() { _goRouter.routerDelegate.removeListener(_syncTabFromRoute); _navProvider.removeListener(_onDrawerStateChanged); _animCtrl.dispose(); super.dispose(); } /// Détecte le changement de route GoRouter et met à jour l'onglet actif. void _syncTabFromRoute() { if (!mounted) return; final path = _goRouter.routerDelegate.currentConfiguration.uri.path; final tab = _routeToTab[path] ?? _currentTab; if (tab != _currentTab) { setState(() => _currentTab = tab); } } /// Déclenche l'animation du drawer. NE touche pas à la navigation. void _onDrawerStateChanged() { if (!mounted) return; if (_navProvider.isDrawerOpen) { _animCtrl.forward(); } else { _animCtrl.reverse(); } } void _onTabTap(int index) { if (index == _currentTab) return; setState(() => _currentTab = index); context.go(_tabToRoute[index]); } @override Widget build(BuildContext context) { return Scaffold( body: Stack( children: [ // ── Contenu principal + bottom nav ─────────────────────────────── Column( children: [ Expanded(child: widget.child), _BottomNav( currentIndex: _currentTab, items: _navItems, onTap: _onTabTap, ), ], ), // ── Overlay sombre ──────────────────────────────────────────────── AnimatedBuilder( animation: _overlayAnim, builder: (context, _) { if (_overlayAnim.value == 0) return const SizedBox.shrink(); return Opacity( opacity: _overlayAnim.value, child: const DrawerScrim(), ); }, ), // ── Drawer panneau ──────────────────────────────────────────────── AnimatedBuilder( animation: _slideAnim, builder: (context, _) { final offset = 320.0 * (_slideAnim.value - 1); return Transform.translate( offset: Offset(offset, 0), child: const Align( alignment: Alignment.centerLeft, child: DrawerPanel(), ), ); }, ), ], ), ); } } // ───────────────────────────────────────────────────────────────────────────── // Bottom navigation bar // ───────────────────────────────────────────────────────────────────────────── class _BottomNav extends StatelessWidget { final int currentIndex; final List<_NavItem> items; final ValueChanged onTap; const _BottomNav({ required this.currentIndex, required this.items, required this.onTap, }); @override Widget build(BuildContext context) { return Container( decoration: BoxDecoration( color: Colors.white, boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.08), blurRadius: 12, offset: const Offset(0, -2), ), ], ), child: SafeArea( child: SizedBox( height: 62, child: Row( children: items.asMap().entries.map((e) { final i = e.key; final item = e.value; final selected = currentIndex == i; return Expanded( child: GestureDetector( onTap: () => onTap(i), behavior: HitTestBehavior.opaque, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ AnimatedContainer( duration: const Duration(milliseconds: 200), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 4), decoration: BoxDecoration( color: selected ? EtmTokens.green.withValues(alpha: 0.12) : Colors.transparent, borderRadius: BorderRadius.circular(12), ), child: Icon( item.icon, color: selected ? EtmTokens.green : EtmTokens.muted, size: 22, ), ), const SizedBox(height: 2), Text( item.label, style: EtmTokens.sans( size: 10, color: selected ? EtmTokens.green : EtmTokens.muted, weight: selected ? FontWeight.w600 : FontWeight.w400, ), ), ], ), ), ); }).toList(), ), ), ), ); } } class _NavItem { final IconData icon; final String label; const _NavItem({required this.icon, required this.label}); } // ───────────────────────────────────────────────────────────────────────────── // Widget bouton d'ouverture du drawer (à placer dans l'AppBar de chaque écran) // ───────────────────────────────────────────────────────────────────────────── /// Bouton hamburger/logo ETM qui ouvre le drawer depuis n'importe quel écran. class DrawerMenuButton extends StatelessWidget { const DrawerMenuButton({super.key}); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.only(left: 12), child: GestureDetector( onTap: () => context.read().openDrawer(), child: Container( width: 38, height: 38, decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), gradient: const LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [EtmTokens.green, EtmTokens.greenDark], ), boxShadow: [ BoxShadow( color: EtmTokens.green.withValues(alpha: 0.35), blurRadius: 12, offset: const Offset(0, 4), ), ], ), child: const Icon(Icons.bolt, color: Colors.white, size: 22), ), ), ); } } // ───────────────────────────────────────────────────────────────────────────── // Écran stub (pour les routes non encore implémentées) // ───────────────────────────────────────────────────────────────────────────── class _StubScreen extends StatelessWidget { final String title; const _StubScreen(this.title); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(title), backgroundColor: Colors.white, foregroundColor: const Color(0xFF1A1A2E), elevation: 0, ), backgroundColor: const Color(0xFFF0F2F5), body: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.construction_rounded, size: 48, color: Colors.grey.shade400), const SizedBox(height: 16), Text( title, style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold), ), const SizedBox(height: 8), const Text( 'Cet écran sera disponible prochainement.', style: TextStyle(color: Color(0xFF6B7280)), ), ], ), ), ); } }