import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../models/energy_data.dart'; import '../models/nymea_models.dart'; import '../services/nymea_service.dart'; import '../theme/etm_tokens.dart'; import '../main.dart' show DrawerMenuButton; // ───────────────────────────────────────────────────────────────────────────── // FavoritesScreen — widgets personnalisables, réordonnables, persistés // ───────────────────────────────────────────────────────────────────────────── class FavoritesScreen extends StatefulWidget { const FavoritesScreen({super.key}); @override State createState() => _FavoritesScreenState(); } class _FavoritesScreenState extends State { @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { final s = context.read(); if (!s.connected) s.startSimulation(); }); } @override Widget build(BuildContext context) { return Consumer( builder: (context, service, _) { final favs = service.favoriteWidgets; return Scaffold( backgroundColor: EtmTokens.bg, body: CustomScrollView( slivers: [ SliverAppBar( floating: true, backgroundColor: EtmTokens.bg, elevation: 0, leading: const DrawerMenuButton(), leadingWidth: 64, title: Text('Favoris', style: EtmTokens.sans(size: 20, weight: FontWeight.w600)), actions: [ Padding( padding: const EdgeInsets.only(right: 12), child: GestureDetector( onTap: () => _showAddSheet(context, service), child: Container( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 7), decoration: BoxDecoration( color: EtmTokens.greenSoft, borderRadius: BorderRadius.circular(10), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.add_rounded, color: EtmTokens.green, size: 18), const SizedBox(width: 4), Text('Ajouter', style: EtmTokens.sans( size: 13, weight: FontWeight.w600, color: EtmTokens.green)), ], ), ), ), ), ], ), if (favs.isEmpty) SliverFillRemaining( child: _EmptyState( onAdd: () => _showAddSheet(context, service)), ) else SliverPadding( padding: const EdgeInsets.fromLTRB(18, 4, 18, 32), sliver: SliverReorderableList( itemCount: favs.length, onReorder: service.reorderFavorites, proxyDecorator: (child, _, animation) => Material( elevation: 8, borderRadius: BorderRadius.circular(EtmTokens.radiusLg), shadowColor: EtmTokens.navy.withValues(alpha: 0.15), child: child, ), itemBuilder: (context, index) { final fav = favs[index]; return ReorderableDelayedDragStartListener( key: ValueKey(fav.id), index: index, child: Padding( padding: const EdgeInsets.only(bottom: 14), child: _FavoriteCard( fav: fav, service: service, onRemove: () => service.removeFavorite(fav.id), ), ), ); }, ), ), ], ), ); }, ); } void _showAddSheet(BuildContext context, NymeaService service) { showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (_) => _AddSheet(service: service), ); } } // ─────────────────────────── Carte favori ────────────────────────────────────── class _FavoriteCard extends StatelessWidget { final FavoriteWidget fav; final NymeaService service; final VoidCallback onRemove; const _FavoriteCard({ required this.fav, required this.service, required this.onRemove, }); @override Widget build(BuildContext context) { return Container( decoration: BoxDecoration( color: EtmTokens.card, borderRadius: BorderRadius.circular(EtmTokens.radiusLg), boxShadow: EtmTokens.cardShadow, ), padding: const EdgeInsets.fromLTRB(18, 16, 14, 18), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // En-tête : poignée + titre + supprimer Row( children: [ const Icon(Icons.drag_handle_rounded, color: EtmTokens.faint, size: 20), const SizedBox(width: 8), Expanded( child: Text(fav.title, style: EtmTokens.sans( size: 14, weight: FontWeight.w600, color: EtmTokens.muted)), ), GestureDetector( onTap: onRemove, child: const Icon(Icons.close_rounded, color: EtmTokens.faint, size: 18), ), ], ), const SizedBox(height: 14), _buildContent(), ], ), ); } Widget _buildContent() { final d = service.energyData; return switch (fav.type) { FavoriteType.pvPower => _BigMetric( value: _fmtVal(d.pvPower), unit: _fmtUnit(d.pvPower), label: 'Production solaire', icon: Icons.wb_sunny_rounded, color: EtmTokens.amber, sub: d.pvPower > 100 ? 'En production' : 'Pas de production', ), FavoriteType.homePower => _BigMetric( value: _fmtVal(d.homePower), unit: _fmtUnit(d.homePower), label: 'Consommation maison', icon: Icons.home_rounded, color: EtmTokens.blue, sub: 'Dont ${d.selfConsumptionRate.toStringAsFixed(0)}% solaire', ), FavoriteType.batterySOC => _BatteryWidget(data: d), FavoriteType.gridPower => _BigMetric( value: _fmtVal(d.gridPower.abs()), unit: _fmtUnit(d.gridPower.abs()), label: d.gridPower > 0 ? 'Soutirage réseau' : 'Injection réseau', icon: Icons.electrical_services_rounded, color: d.gridPower > 0 ? EtmTokens.orange : EtmTokens.green, sub: d.gridPower > 0 ? 'Achat réseau' : 'Vente réseau', ), FavoriteType.rates => _RatesWidget(data: d), FavoriteType.evCharger => _EVWidget(data: d, service: service), FavoriteType.thingState => _ThingStateWidget(fav: fav, service: service), FavoriteType.chart => _MiniChart(history: service.historyPoints), }; } static String _fmtVal(double w) => w >= 1000 ? (w / 1000).toStringAsFixed(2) : w.toStringAsFixed(0); static String _fmtUnit(double w) => w >= 1000 ? 'kW' : 'W'; } // ─────────────────────────── Widgets de contenu ──────────────────────────────── class _BigMetric extends StatelessWidget { final String value; final String unit; final String label; final IconData icon; final Color color; final String sub; const _BigMetric({ required this.value, required this.unit, required this.label, required this.icon, required this.color, required this.sub, }); @override Widget build(BuildContext context) { return Row( children: [ Container( width: 56, height: 56, decoration: BoxDecoration( color: color.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(16), ), child: Icon(icon, color: color, size: 30), ), const SizedBox(width: 18), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ Text(value, style: EtmTokens.mono( size: 36, weight: FontWeight.w700, color: color)), Padding( padding: const EdgeInsets.only(bottom: 5, left: 4), child: Text(unit, style: EtmTokens.sans(size: 15, color: EtmTokens.muted)), ), ], ), Text(label, style: EtmTokens.sans(size: 12, color: EtmTokens.muted)), const SizedBox(height: 2), Text(sub, style: EtmTokens.sans( size: 11, weight: FontWeight.w500, color: color)), ], ), ], ); } } class _BatteryWidget extends StatelessWidget { final EnergyData data; const _BatteryWidget({required this.data}); @override Widget build(BuildContext context) { final soc = data.batterySOC; final isCharging = data.batteryPower > 0; final color = soc > 50 ? EtmTokens.green : soc > 20 ? EtmTokens.amber : EtmTokens.danger; return Row( children: [ Stack( alignment: Alignment.center, children: [ SizedBox( width: 80, height: 80, child: CircularProgressIndicator( value: soc / 100, strokeWidth: 7, backgroundColor: color.withValues(alpha: 0.12), valueColor: AlwaysStoppedAnimation(color), ), ), Column( mainAxisSize: MainAxisSize.min, children: [ Text('${soc.toStringAsFixed(0)}%', style: EtmTokens.mono( size: 18, weight: FontWeight.w700, color: color)), Icon( isCharging ? Icons.bolt_rounded : Icons.battery_std_rounded, size: 14, color: color, ), ], ), ], ), const SizedBox(width: 20), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('État de charge', style: EtmTokens.sans(size: 13, color: EtmTokens.muted)), const SizedBox(height: 6), Text( isCharging ? 'En charge ${_fmt(data.batteryPower)}' : data.batteryPower < -10 ? 'Décharge ${_fmt(data.batteryPower.abs())}' : 'Standby', style: EtmTokens.mono( size: 14, weight: FontWeight.w700, color: color), ), const SizedBox(height: 4), Text( '${data.batteryPower.abs().toStringAsFixed(0)} W', style: EtmTokens.sans(size: 12, color: EtmTokens.muted), ), ], ), ], ); } static String _fmt(double w) => w >= 1000 ? '${(w / 1000).toStringAsFixed(1)} kW' : '${w.toStringAsFixed(0)} W'; } class _RatesWidget extends StatelessWidget { final EnergyData data; const _RatesWidget({required this.data}); @override Widget build(BuildContext context) { return Row( children: [ Expanded( child: _RateTile( label: 'Autoconsommation', value: data.selfConsumptionRate, color: EtmTokens.amber, ), ), const SizedBox(width: 16), Expanded( child: _RateTile( label: 'Autonomie', value: data.autonomyRate, color: EtmTokens.blue, ), ), ], ); } } class _RateTile extends StatelessWidget { final String label; final double value; final Color color; const _RateTile( {required this.label, required this.value, required this.color}); @override Widget build(BuildContext context) { return Column( children: [ Text('${value.toStringAsFixed(1)}%', style: EtmTokens.mono(size: 30, weight: FontWeight.w700, color: color)), const SizedBox(height: 6), ClipRRect( borderRadius: BorderRadius.circular(99), child: LinearProgressIndicator( value: (value / 100).clamp(0.0, 1.0), backgroundColor: color.withValues(alpha: 0.12), valueColor: AlwaysStoppedAnimation(color), minHeight: 7, ), ), const SizedBox(height: 6), Text(label, style: EtmTokens.sans(size: 11, color: EtmTokens.muted), textAlign: TextAlign.center), ], ); } } class _EVWidget extends StatelessWidget { final EnergyData data; final NymeaService service; const _EVWidget({required this.data, required this.service}); @override Widget build(BuildContext context) { final (color, label) = switch (data.chargingMode) { ChargingMode.pv => (EtmTokens.amber, 'PV'), ChargingMode.minPv => (EtmTokens.blue, 'Min+PV'), ChargingMode.boost => (EtmTokens.green, 'Boost'), }; return Row( children: [ Container( width: 52, height: 52, decoration: BoxDecoration( color: color.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(14), ), child: Icon(Icons.ev_station_rounded, color: color, size: 28), ), const SizedBox(width: 14), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('${(data.chargingPower * 1000).toStringAsFixed(0)} W', style: EtmTokens.mono( size: 26, weight: FontWeight.w700, color: color)), Text('Mode $label', style: EtmTokens.sans(size: 12, color: color)), Text( '${data.solarSourcePercent.toStringAsFixed(0)}% solaire', style: EtmTokens.sans(size: 11, color: EtmTokens.muted)), ], ), ), // Sélecteur de mode rapide Column( mainAxisSize: MainAxisSize.min, children: ChargingMode.values.map((m) { final mc = switch (m) { ChargingMode.pv => EtmTokens.amber, ChargingMode.minPv => EtmTokens.blue, ChargingMode.boost => EtmTokens.green, }; final sel = data.chargingMode == m; return GestureDetector( onTap: () => service.setChargingInfo(mode: m), child: AnimatedContainer( duration: const Duration(milliseconds: 150), width: sel ? 10 : 7, height: sel ? 10 : 7, margin: const EdgeInsets.symmetric(vertical: 3), decoration: BoxDecoration( color: sel ? mc : mc.withValues(alpha: 0.3), shape: BoxShape.circle, ), ), ); }).toList(), ), ], ); } } class _ThingStateWidget extends StatelessWidget { final FavoriteWidget fav; final NymeaService service; const _ThingStateWidget({required this.fav, required this.service}); @override Widget build(BuildContext context) { final thing = service.things.firstWhere( (t) => t.id == fav.thingId, orElse: () => const NymeaThing( id: '', name: 'Introuvable', thingClassId: '', setupStatus: '', paramValues: []), ); final value = fav.stateTypeId != null ? thing.stateValue(fav.stateTypeId!) : null; return Row( children: [ Container( width: 48, height: 48, decoration: BoxDecoration( color: EtmTokens.green.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(12), ), child: const Icon(Icons.device_hub_rounded, color: EtmTokens.green, size: 26), ), const SizedBox(width: 14), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(thing.name, style: EtmTokens.sans(size: 14, weight: FontWeight.w600)), const SizedBox(height: 4), Text( value?.toString() ?? '—', style: EtmTokens.mono( size: 24, weight: FontWeight.w700, color: EtmTokens.green), ), ], ), ], ); } } class _MiniChart extends StatelessWidget { final List history; const _MiniChart({required this.history}); @override Widget build(BuildContext context) { if (history.isEmpty) { return SizedBox( height: 64, child: Center( child: Text('Pas de données', style: EtmTokens.sans(size: 12, color: EtmTokens.muted)), ), ); } final max = history.map((p) => p.pvWh).fold(0.0, (a, b) => a > b ? a : b); return SizedBox( height: 64, child: Row( crossAxisAlignment: CrossAxisAlignment.end, children: history.map((p) { final h = max > 0 ? (p.pvWh / max) : 0.0; return Expanded( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 1), child: FractionallySizedBox( alignment: Alignment.bottomCenter, heightFactor: h.clamp(0.04, 1.0), child: DecoratedBox( decoration: BoxDecoration( color: EtmTokens.amber.withValues(alpha: 0.8), borderRadius: const BorderRadius.vertical(top: Radius.circular(3)), ), ), ), ), ); }).toList(), ), ); } } // ─────────────────────────── État vide ───────────────────────────────────────── class _EmptyState extends StatelessWidget { final VoidCallback onAdd; const _EmptyState({required this.onAdd}); @override Widget build(BuildContext context) { return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Container( width: 88, height: 88, decoration: BoxDecoration( color: EtmTokens.green.withValues(alpha: 0.10), shape: BoxShape.circle, ), child: const Icon(Icons.star_outline_rounded, color: EtmTokens.green, size: 44), ), const SizedBox(height: 18), Text('Aucun favori', style: EtmTokens.sans( size: 18, weight: FontWeight.w600)), const SizedBox(height: 8), Text('Ajoutez des widgets pour un accès rapide', style: EtmTokens.sans(size: 14, color: EtmTokens.muted)), const SizedBox(height: 24), GestureDetector( onTap: onAdd, child: Container( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), decoration: BoxDecoration( color: EtmTokens.green, borderRadius: BorderRadius.circular(12), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.add_rounded, color: Colors.white, size: 18), const SizedBox(width: 8), Text('Ajouter un widget', style: EtmTokens.sans( size: 14, weight: FontWeight.w600, color: Colors.white)), ], ), ), ), ], ), ); } } // ─────────────────────────── Feuille d'ajout ─────────────────────────────────── class _AddSheet extends StatelessWidget { final NymeaService service; const _AddSheet({required this.service}); static const _options = [ (FavoriteType.pvPower, 'Production PV', 'Puissance solaire instantanée', Icons.wb_sunny_rounded, EtmTokens.amber), (FavoriteType.homePower, 'Consommation', 'Puissance consommée', Icons.home_rounded, EtmTokens.blue), (FavoriteType.batterySOC,'Batterie', 'État de charge', Icons.battery_charging_full_rounded,EtmTokens.green), (FavoriteType.gridPower, 'Réseau', 'Soutirage / injection réseau', Icons.electrical_services_rounded, EtmTokens.muted), (FavoriteType.rates, 'Taux', 'Autoconsommation & autonomie', Icons.pie_chart_rounded, EtmTokens.amber), (FavoriteType.evCharger, 'Borne EV', 'Recharge véhicule', Icons.ev_station_rounded, EtmTokens.blue), (FavoriteType.chart, 'Graphique PV', 'Production journalière (barres)', Icons.bar_chart_rounded, EtmTokens.green), ]; @override Widget build(BuildContext context) { return Container( decoration: const BoxDecoration( color: EtmTokens.card, borderRadius: BorderRadius.vertical(top: Radius.circular(24)), ), child: DraggableScrollableSheet( initialChildSize: 0.6, maxChildSize: 0.9, minChildSize: 0.4, expand: false, builder: (_, ctrl) => Column( children: [ // Poignée Container( margin: const EdgeInsets.only(top: 12, bottom: 18), width: 40, height: 4, decoration: BoxDecoration( color: EtmTokens.line, borderRadius: BorderRadius.circular(2)), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('Ajouter un widget', style: EtmTokens.sans( size: 18, weight: FontWeight.w600)), GestureDetector( onTap: () => Navigator.pop(context), child: const Icon(Icons.close_rounded, color: EtmTokens.faint), ), ], ), ), const SizedBox(height: 4), Expanded( child: ListView( controller: ctrl, padding: const EdgeInsets.fromLTRB(18, 12, 18, 24), children: _options.map((opt) { final (type, title, subtitle, icon, color) = opt; final added = service.favoriteWidgets .any((f) => f.type == type); return Padding( padding: const EdgeInsets.only(bottom: 10), child: GestureDetector( onTap: added ? null : () { service.addFavorite(FavoriteWidget( id: 'fw_${type.name}_${DateTime.now().millisecondsSinceEpoch}', type: type, title: title, )); Navigator.pop(context); }, child: Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: added ? EtmTokens.bg : EtmTokens.card, borderRadius: BorderRadius.circular(EtmTokens.radius), border: Border.all( color: added ? EtmTokens.line : color.withValues(alpha: 0.3), ), boxShadow: added ? null : EtmTokens.cardShadow, ), child: Row( children: [ Container( width: 42, height: 42, decoration: BoxDecoration( color: color.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(12), ), child: Icon(icon, color: added ? EtmTokens.faint : color, size: 22), ), const SizedBox(width: 14), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(title, style: EtmTokens.sans( size: 14, weight: FontWeight.w600, color: added ? EtmTokens.faint : EtmTokens.navy)), Text(subtitle, style: EtmTokens.sans( size: 11, color: EtmTokens.muted)), ], ), ), Icon( added ? Icons.check_circle_rounded : Icons.add_circle_outline_rounded, color: added ? EtmTokens.green : color, size: 22, ), ], ), ), ), ); }).toList(), ), ), ], ), ), ); } }