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/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) // ───────────────────────────────────────────────────────────────────────────── /// 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(); return ListView( padding: const EdgeInsets.symmetric(vertical: 8), children: [ // ── VUE PRINCIPALE ──────────────────────────────────────────────── _SectionLabel('VUE PRINCIPALE'), _NavItem(icon: Icons.home_rounded, label: 'Dashboard', route: '/'), _NavItem(icon: Icons.bar_chart_rounded, label: 'Énergie', route: '/energy'), _NavItem(icon: Icons.device_hub_rounded, label: 'Things', route: '/things'), _NavItem(icon: Icons.star_rounded, label: 'Favoris', route: '/favorites'), _NavItem(icon: Icons.group_work_rounded, label: 'Groupes', route: '/groups'), _NavItem(icon: Icons.auto_awesome_rounded, label: 'Scènes', route: '/scenes'), _NavItem(icon: Icons.music_note_rounded, label: 'Médias', route: '/media'), _NavItem(icon: Icons.garage_rounded, label: 'Garages', route: '/garages'), _NavItem(icon: Icons.ac_unit_rounded, label: 'AC / Climatisation', route: '/ac'), 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://etm.at/powersync/docs', ), _LinkItem( icon: Icons.telegram_rounded, label: 'Telegram', url: 'https://t.me/etm_support', ), _LinkItem( icon: Icons.discord_rounded, label: 'Discord', url: 'https://discord.gg/etm', ), _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: const Icon(Icons.open_in_new_rounded, size: 14, color: EtmTokens.faint), 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: [ _SubItem('Rôles & appareils', '/energy/setup'), _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'), _SubItem('Options développeur', '/settings/app/developer'), _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.faint, collapsedIconColor: EtmTokens.faint, 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.faint)), ), ); } } // ── 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.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.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 } } }