import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; import '../../providers/app_settings_provider.dart'; import '../../providers/installer_mode_provider.dart'; import '../../providers/navigation_provider.dart'; import '../../services/connection_manager.dart'; import '../../services/nymea_service.dart'; import '../../theme/etm_tokens.dart'; import 'installer_pin_dialog.dart'; // ───────────────────────────────────────────────────────────────────────────── // MainDrawer — drawer custom qui glisse sur le contenu // // Pas de Drawer Flutter standard. L'overlay est géré dans MainShell via Stack : // 1. Couche sombre (GestureDetector → closeDrawer) // 2. DrawerPanel animé (Transform.translate) // ───────────────────────────────────────────────────────────────────────────── /// Route associée à chaque `AppScreen.id` de [AppSettingsProvider]. /// Un id absent de cette table n'est jamais affiché dans le drawer. const Map kScreenRoutes = { 'dashboard': '/', 'energy': '/energy', 'things': '/things', 'favorites': '/favorites', 'groups': '/groups', 'scenes': '/scenes', 'media': '/media', 'garages': '/garages', 'ac': '/ac', }; /// Couche sombre cliquable qui ferme le drawer. class DrawerScrim extends StatelessWidget { const DrawerScrim({super.key}); @override Widget build(BuildContext context) { return GestureDetector( onTap: () => context.read().closeDrawer(), child: Container(color: Colors.black.withValues(alpha: 0.45)), ); } } /// Panel du drawer (largeur fixe, fond sombre ETM). class DrawerPanel extends StatelessWidget { const DrawerPanel({super.key}); @override Widget build(BuildContext context) { return SizedBox( width: 320, child: Material( color: EtmTokens.navy, child: SafeArea( child: Column( children: [ // ── En-tête ──────────────────────────────────────────────────── _DrawerHeader(), // ── Menu ────────────────────────────────────────────────────── Expanded( child: _DrawerMenu(), ), ], ), ), ), ); } } // ── En-tête du drawer ───────────────────────────────────────────────────────── class _DrawerHeader extends StatelessWidget { @override Widget build(BuildContext context) { final service = context.watch(); final installer = context.watch(); final manager = context.watch(); // Nom de l'installation active : box réelle > identité Hello > démo. final boxName = service.isSimulation ? 'Mode démo' : (manager.active?.name ?? (service.serverName.isNotEmpty ? service.serverName : 'Non connecté')); return Container( padding: const EdgeInsets.fromLTRB(20, 18, 16, 18), decoration: BoxDecoration( gradient: const LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [EtmTokens.navy, EtmTokens.navy2], ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // ── Logo + statut connexion ────────────────────────────────────── Row( children: [ Container( width: 44, height: 44, decoration: BoxDecoration( borderRadius: BorderRadius.circular(13), gradient: const LinearGradient( colors: [EtmTokens.blue, Color(0xFF1F7FB3)], ), ), child: const Icon(Icons.bolt, color: Colors.white, size: 24), ), const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'ETM PowerSync', style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold, fontSize: 15, ), ), Text( service.isSimulation ? 'Mode simulation' : (service.host), style: EtmTokens.sans(size: 12, color: const Color(0xFFA9C4D3)), ), ], ), ), _ConnectionDot( connected: service.connected, simulation: service.isSimulation, ), ], ), const SizedBox(height: 12), // ── Nom de la box active (cliquable → Installations) ───────────── // L'en-tête EST l'accès au gestionnaire multi-HEMS (pas d'entrée de // menu dédiée) : tap → bascule d'installation. GestureDetector( onTap: () { context.read().closeDrawer(); context.go('/installations'); }, behavior: HitTestBehavior.opaque, child: Row( children: [ Flexible( child: Text( boxName, overflow: TextOverflow.ellipsis, style: EtmTokens.sans( size: 15, weight: FontWeight.w600, color: Colors.white), ), ), const SizedBox(width: 6), const Icon(Icons.unfold_more_rounded, size: 16, color: Color(0xFFA9C4D3)), ], ), ), const SizedBox(height: 6), // ── Utilisateur + badge rôle ───────────────────────────────────── Row( children: [ const Icon(Icons.person_outline, size: 16, color: Color(0xFFA9C4D3)), const SizedBox(width: 6), Text( service.username.isNotEmpty ? service.username : 'Utilisateur', style: EtmTokens.sans(size: 13, color: const Color(0xFFCFE6F3)), ), const SizedBox(width: 8), _RoleBadge(isInstaller: installer.isUnlocked), ], ), ], ), ); } } class _ConnectionDot extends StatelessWidget { final bool connected; final bool simulation; const _ConnectionDot( {required this.connected, required this.simulation}); @override Widget build(BuildContext context) { final Color color = simulation ? EtmTokens.amber : connected ? EtmTokens.green : EtmTokens.danger; return Container( width: 9, height: 9, decoration: BoxDecoration(shape: BoxShape.circle, color: color), ); } } class _RoleBadge extends StatelessWidget { final bool isInstaller; const _RoleBadge({required this.isInstaller}); @override Widget build(BuildContext context) { final color = isInstaller ? EtmTokens.orange : EtmTokens.blue; return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration( color: color.withValues(alpha: 0.18), borderRadius: BorderRadius.circular(6), border: Border.all(color: color.withValues(alpha: 0.5)), ), child: Text( isInstaller ? 'INSTALLATEUR' : 'UTILISATEUR', style: EtmTokens.sans(size: 10, weight: FontWeight.w700, color: color, letterSpacing: 0.5), ), ); } } // ── Menu du drawer ──────────────────────────────────────────────────────────── class _DrawerMenu extends StatelessWidget { @override Widget build(BuildContext context) { final installer = context.watch(); final settings = context.watch(); return ListView( padding: const EdgeInsets.symmetric(vertical: 8), children: [ // ── VUE PRINCIPALE ──────────────────────────────────────────────── // Liste pilotée par « App Settings → Écrans actifs » : ordre et // visibilité viennent de AppSettingsProvider. Les écrans smarthome // (Groupes/Scènes/Médias/Garages) sont masqués par défaut — ce HEMS // est un gestionnaire d'énergie. _SectionLabel('VUE PRINCIPALE'), for (final screen in settings.visibleScreens) if (kScreenRoutes[screen.id] != null) _NavItem( icon: screen.icon, label: screen.label, route: kScreenRoutes[screen.id]!, ), const SizedBox(height: 4), _Divider(), // ── CONFIGURATION ───────────────────────────────────────────────── _SectionLabel('CONFIGURATION'), _NavItem(icon: Icons.auto_fix_high_rounded, label: 'Magic / Automatisations', route: '/automations', push: true), // Gestionnaire d'énergie (ExpansionTile) _EnergyManagerExpansion(), // App Settings (ExpansionTile) _AppSettingsExpansion(), const SizedBox(height: 4), _Divider(), // ── MODE INSTALLATEUR ───────────────────────────────────────────── _InstallerSection(isUnlocked: installer.isUnlocked), const SizedBox(height: 4), _Divider(), // ── SUPPORT ─────────────────────────────────────────────────────── _SectionLabel('SUPPORT'), _LinkItem( icon: Icons.menu_book_rounded, label: 'Documentation', url: 'https://docs.etm-powersync.fr/', ), _LinkItem( icon: Icons.telegram_rounded, label: 'Telegram', url: 'https://t.me/etm_powersync_support', ), // Discord désactivé — pas de serveur ETM-PowerSync pour l'instant. // Réactiver ici dès que l'invite permanente existe : // _LinkItem(icon: Icons.discord_rounded, label: 'Discord', url: '...'), _NavItem( icon: Icons.bug_report_rounded, label: 'Rapport de bug', route: '/bug-report', ), const SizedBox(height: 16), ], ); } } // ── Éléments de menu ────────────────────────────────────────────────────────── class _SectionLabel extends StatelessWidget { final String text; const _SectionLabel(this.text); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.fromLTRB(20, 14, 20, 8), child: Text(text, style: EtmTokens.sectionLabel()), ); } } class _Divider extends StatelessWidget { @override Widget build(BuildContext context) { return Divider( color: Colors.white.withValues(alpha: 0.08), height: 16, indent: 20, endIndent: 20, ); } } /// Item de navigation : ferme le drawer et navigue via GoRouter. /// Utilise context.go() pour les routes dans le shell (bottom nav), /// context.push() pour les écrans poussés par-dessus le shell. class _NavItem extends StatelessWidget { final IconData icon; final String label; final String route; /// Si true, utilise context.push() (écrans hors shell). final bool push; const _NavItem({ required this.icon, required this.label, required this.route, this.push = false, }); @override Widget build(BuildContext context) { return _DrawerTile( icon: icon, label: label, onTap: () { context.read().closeDrawer(); if (push) { context.push(route); } else { context.go(route); } }, ); } } /// Item qui ouvre une URL dans le navigateur externe. class _LinkItem extends StatelessWidget { final IconData icon; final String label; final String url; const _LinkItem({ required this.icon, required this.label, required this.url, }); @override Widget build(BuildContext context) { return _DrawerTile( icon: icon, label: label, trailing: Icon(Icons.open_in_new_rounded, size: 14, color: EtmTokens.faintOf(context)), onTap: () async { context.read().closeDrawer(); final uri = Uri.parse(url); if (!await launchUrl(uri, mode: LaunchMode.externalApplication)) { if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Impossible d\'ouvrir : $url')), ); } } }, ); } } class _DrawerTile extends StatelessWidget { final IconData icon; final String label; final Widget? trailing; final VoidCallback onTap; const _DrawerTile({ required this.icon, required this.label, required this.onTap, this.trailing, }); @override Widget build(BuildContext context) { return InkWell( onTap: onTap, borderRadius: BorderRadius.circular(10), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), child: Row( children: [ Icon(icon, size: 20, color: Colors.white.withValues(alpha: 0.92)), const SizedBox(width: 16), Expanded( child: Text( label, style: EtmTokens.sans(size: 14, color: Colors.white.withValues(alpha: 0.92)), ), ), if (trailing != null) trailing!, ], ), ), ); } } // ── ExpansionTile : Gestionnaire d'énergie ──────────────────────────────────── class _EnergyManagerExpansion extends StatelessWidget { @override Widget build(BuildContext context) { return _StyledExpansion( icon: Icons.energy_savings_leaf_rounded, label: 'Gestionnaire d\'énergie', children: [ // 'Rôles & appareils' a migré dans la section MODE INSTALLATEUR : // c'est de la configuration d'installation, pas un réglage utilisateur. _SubItem('Scheduler & stratégie', '/energy/scheduler'), _SubItem('Tarifs & providers', '/energy/tariffs'), _SubItem('Timeline & décisions', '/energy/timeline'), ], ); } } // ── ExpansionTile : App Settings ────────────────────────────────────────────── class _AppSettingsExpansion extends StatelessWidget { @override Widget build(BuildContext context) { return _StyledExpansion( icon: Icons.settings_rounded, label: 'App Settings', children: [ _SubItem('Apparence', '/settings/app/appearance'), _SubItem('Écrans actifs', '/settings/app/screens'), // 'Options développeur' a migré dans la section MODE INSTALLATEUR. _SubItem('À propos PowerSync', '/settings/app/about'), ], ); } } class _StyledExpansion extends StatelessWidget { final IconData icon; final String label; final List children; const _StyledExpansion({ required this.icon, required this.label, required this.children, }); @override Widget build(BuildContext context) { return Theme( data: Theme.of(context).copyWith( dividerColor: Colors.transparent, ), child: ExpansionTile( leading: Icon(icon, size: 20, color: Colors.white.withValues(alpha: 0.92)), title: Text(label, style: EtmTokens.sans(size: 14, color: Colors.white.withValues(alpha: 0.92))), iconColor: EtmTokens.faintOf(context), collapsedIconColor: EtmTokens.faintOf(context), tilePadding: const EdgeInsets.symmetric(horizontal: 20), childrenPadding: EdgeInsets.zero, children: children, ), ); } } class _SubItem extends StatelessWidget { final String label; final String route; const _SubItem(this.label, this.route); @override Widget build(BuildContext context) { return InkWell( onTap: () { context.read().closeDrawer(); context.push(route); }, child: Padding( padding: const EdgeInsets.fromLTRB(56, 10, 16, 10), child: Text(label, style: EtmTokens.sans(size: 13, color: EtmTokens.faintOf(context))), ), ); } } // ── Section Mode Installateur ───────────────────────────────────────────────── class _InstallerSection extends StatelessWidget { final bool isUnlocked; const _InstallerSection({required this.isUnlocked}); @override Widget build(BuildContext context) { if (!isUnlocked) { // Bouton de déverrouillage return Padding( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), child: InkWell( onTap: () => _promptPin(context), child: Container( margin: const EdgeInsets.fromLTRB(16, 4, 16, 4), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), decoration: BoxDecoration( color: EtmTokens.orange.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(12), border: Border.all(color: EtmTokens.orange.withValues(alpha: 0.4)), ), child: Row( children: [ Icon(Icons.build_outlined, size: 18, color: EtmTokens.orange), const SizedBox(width: 10), Expanded( child: Text('Mode installateur', style: EtmTokens.sans(size: 14, weight: FontWeight.w600, color: EtmTokens.orange)), ), Icon(Icons.lock_outline, size: 16, color: EtmTokens.orange), ], ), ), ), ); } // Section déverrouillée — menu installateur complet return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _SectionLabel('MODE INSTALLATEUR'), _NavItem( icon: Icons.account_tree_rounded, label: 'Rôles & appareils', route: '/energy/setup', push: true, ), _NavItem( icon: Icons.tune_rounded, label: 'Configuration Things', route: '/settings/system/things', push: true, ), _NavItem( icon: Icons.router_rounded, label: 'Système & réseau', route: '/settings/system/network', push: true, ), _NavItem( icon: Icons.cable_rounded, label: 'Protocoles', route: '/settings/system/protocols', push: true, ), _NavItem( icon: Icons.cloud_rounded, label: 'MQTT / Web server', route: '/settings/system/mqtt', push: true, ), _NavItem( icon: Icons.extension_rounded, label: 'Plugins', route: '/settings/system/plugins', push: true, ), _NavItem( icon: Icons.system_update_rounded, label: 'Mise à jour système', route: '/settings/system/update', push: true, ), _NavItem( icon: Icons.code_rounded, label: 'Outils développeur', route: '/settings/system/devtools', push: true, ), _NavItem( icon: Icons.developer_mode_rounded, label: 'Options développeur', route: '/settings/app/developer', push: true, ), _NavItem( icon: Icons.info_outline_rounded, label: 'À propos ETM', route: '/settings/system/about', push: true, ), InkWell( onTap: () => context.read().lock(), child: Container( margin: const EdgeInsets.fromLTRB(16, 4, 16, 4), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), decoration: BoxDecoration( color: EtmTokens.danger.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(12), border: Border.all(color: EtmTokens.danger.withValues(alpha: 0.4)), ), child: Row( children: [ Icon(Icons.lock_outline, size: 18, color: EtmTokens.danger), const SizedBox(width: 10), Text('Verrouiller mode installateur', style: EtmTokens.sans(size: 14, weight: FontWeight.w600, color: EtmTokens.danger)), ], ), ), ), ], ); } Future _promptPin(BuildContext context) async { final result = await showDialog( context: context, barrierDismissible: false, builder: (_) => const InstallerPinDialog(), ); if (result == true && context.mounted) { // Le provider a déjà été mis à jour — pas besoin d'action supplémentaire } } }