Je n'avais pas ouvert docs/mockups/ : les écrans ont été construits depuis le texte du brief seul, et l'écart est franc. Les maquettes du 2026-08-25 font autorité, elles sont désormais suivies. ÉCRANS DE MÉCANISME, refaits : - sélecteur de Things PAR NOM, filtré sur l'interface nymea (power / etmvariableload). Taper un UUID au doigt est une erreur garantie ET muette : la box accepte un UUID valide qui ne désigne rien, et la charge ne commande jamais rien. Le sélecteur grise les Things déjà pris — la box refuse en bloc un même Thing sur deux lignes — et signale en rouge un ThingId configuré qui n'existe plus sur la box ; - routeur de relais : ajout/retrait de contacteurs, puissance par ligne, et les paliers atteignables AFFICHÉS. Ils sont déduits (2^N combinaisons, doublons écartés), jamais enregistrés — la maquette est explicite là-dessus, ma note précédente disant l'inverse était une sur-interprétation du brief ; - SG-Ready : SG1/SG2 se choisissent EN HAUT, une fois, et la table des quatre états devient éditable — ce qui s'y coche est fermé/ouvert, et c'est ça qui devient relays[]. Les étiquettes SG1/SG2 sont une convention de l'écran (SG1 = fermé en état 1, SG2 = en état 3) et elles TOMBENT sur un encodage non standard, où les contacts s'affichent tels quels ; - modulable : plafond requis > 0, et l'avertissement que changer d'appareil crée une autre charge puisque l'identifiant EST le ThingId ; - temporisations minOnS/minOffS (et minStateHoldS pour la PAC) en pas de 30/60 s ; - « Compteur dédié » présent et GRISÉ, avec sa raison : aucun champ n'existe côté box, et inventer meterThingId ferait rejeter l'enregistrement de TOUTES les charges ; - « Inclure dans l'arbitrage » avec rang et domaine, et ce que la désactivation fait vraiment selon le mécanisme ; - validation AVANT envoi : relays[] vide, powerW ≤ 0, Thing en double, état 2 absent, maxPowerW ≤ 0. SetLoadConfig refuse en bloc et sans motif — laisser partir une config invalide condamne l'installateur à un refus muet qui perd tout l'appel. LISTE : - l'en-tête d'arbitrage et le tableau de budget, PERDUS à la fusion, sont rétablis. Sans eux « Alloué 3 000 W » ne se rattache à rien ; - le rang passe en pastille près de la poignée, et le sous-titre devient lisible : « Routeur de relais · 3 relais · min ON 60 s » au lieu de « Routeur de relais · rang 1 » ; - section « Recharge véhicule » : la borne n'est pas une LoadConfig, elle n'apparaissait donc NULLE PART — ce qu'un installateur qui vient de la régler lit comme une perte. MENU : l'entrée « Ordre de service des charges » est retirée ; elle pointait sur une redirection, donc sur le même écran sous deux noms. Tests 80/80, analyze 0 erreur. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HajXLUczEyZd22JeewRfff
664 lines
22 KiB
Dart
664 lines
22 KiB
Dart
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<String, String> 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<NavigationProvider>().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<NymeaService>();
|
|
final installer = context.watch<InstallerModeProvider>();
|
|
final manager = context.watch<ConnectionManager>();
|
|
|
|
// 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<NavigationProvider>().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<InstallerModeProvider>();
|
|
final settings = context.watch<AppSettingsProvider>();
|
|
|
|
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<NavigationProvider>().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<NavigationProvider>().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<Widget> 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<NavigationProvider>().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<InstallerModeProvider>().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<void> _promptPin(BuildContext context) async {
|
|
final result = await showDialog<bool>(
|
|
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
|
|
}
|
|
}
|
|
}
|