Elle reste : la retirer aurait laissé un trou dans une liste que le client a composée lui-même. Elle affiche maintenant ce que la box publie, et là où il n'y a rien à lire, elle le dit. Ce qui est lu, et sa source · branché / en charge ← états pluggedIn, charging du Thing · puissance ← état currentPower du Thing · mode ← GetChargingInfos + notification ChargingInfoChanged Absent = « non publié », jamais zéro. Un mode que l'app ne connaît pas se lit « mode non reconnu (X) » plutôt que d'être deviné, et aucun bouton n'est allumé tant que la box n'a pas dit ce qu'elle porte. L'écriture restitue le refus, sinon elle n'existe pas. Les pastilles de 7 à 10 px qui avalaient les codes d'erreur de setChargingInfo passent à 44 px, montrent l'attente, et affichent le refus tel que la box le nomme. Et l'app ne tient plus d'état de mode local : après une écriture acceptée, elle RELIT GetChargingInfos. Elle se souvenait de ce qu'elle avait demandé, ce qui n'est pas ce que la borne porte. La fiction perd sa porte d'entrée. chargingMode, chargingPower (3,6 kW) et solarSourcePercent (82 %) sont SUPPRIMÉS de EnergyData : trois valeurs par défaut que _updateEnergyData ne touchait jamais. Ce que la box publie sur une borne vit désormais dans EvChargerLive, champ par champ, nullable. ⚠️ Et le Sankey du tableau de bord en tirait un flux « Voiture » permanent à 3,6 kW, voiture débranchée. Il est branché sur la mesure de la borne — « non lu » quand il n'y en a pas. Mais la même vue invente encore PAC = 38 % de la maison, Eau chaude = 18 %, Autres = 22 %, dont un seul marqué « estimé » : entrée 🔴 au TODO, avec la source réelle (GetLoadTelemetry publie measuredW par charge : 1500 W et 800 W ce midi). Brancher le Sankey dessus est une refonte de l'écran d'accueil, pas un correctif — à décider. Deux attentes tombent, une reste (relevé Integrations.GetThings sur .75, cet après-midi) · currentL1/L2/L3 est DÉPLOYÉ (5,86 / 5,94 / 6,02 A) — powerL1/L2/L3 a disparu · phaseCount EXISTE sur la Trydan (3) — la note qui le disait absent était en retard · sessionEnergy existe (4,229 kWh) MAIS vaut exactement chargeEnergy : c'est ce qu'on attend d'une session sans interruption, donc ça ne prouve rien. LM-1009 §C1 reste entière tant qu'une remise à zéro de chargeEnergy n'a pas été vue avec un sessionEnergy qui continue. Le brief moteur dit désormais du masquage WRITE_FAILED que ce n'est pas « ne se produit pas » mais « ne s'est pas encore produit » : il attend une charge variable qui perd son Thing, et le banc n'a pas eu ce cas. 154 tests (8 nouveaux sur la tuile, dont les cibles tactiles et la restitution du refus), flutter analyze à 27 remarques, aucune nouvelle. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SQbZKrWqsMFP1Lh2jjjd9f
718 lines
26 KiB
Dart
718 lines
26 KiB
Dart
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 '../widgets/ev_favorite_tile.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<FavoritesScreen> createState() => _FavoritesScreenState();
|
|
}
|
|
|
|
class _FavoritesScreenState extends State<FavoritesScreen> {
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
final s = context.read<NymeaService>();
|
|
if (!s.connected) s.startSimulation();
|
|
});
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Consumer<NymeaService>(
|
|
builder: (context, service, _) {
|
|
final favs = service.favoriteWidgets;
|
|
|
|
return Scaffold(
|
|
backgroundColor: EtmTokens.bgOf(context),
|
|
body: CustomScrollView(
|
|
slivers: [
|
|
SliverAppBar(
|
|
floating: true,
|
|
backgroundColor: EtmTokens.bgOf(context),
|
|
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.surfaceOf(context),
|
|
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: [
|
|
Icon(Icons.drag_handle_rounded,
|
|
color: EtmTokens.faintOf(context), size: 20),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: Text(fav.title,
|
|
style: EtmTokens.sans(
|
|
size: 14,
|
|
weight: FontWeight.w600,
|
|
color: EtmTokens.mutedOf(context))),
|
|
),
|
|
GestureDetector(
|
|
onTap: onRemove,
|
|
child: Icon(Icons.close_rounded,
|
|
color: EtmTokens.faintOf(context), 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),
|
|
// La tuile ne reçoit plus `EnergyData` : ses trois grandeurs de recharge
|
|
// n'étaient jamais lues sur la box. Elle lit la borne, ou dit qu'il n'y a
|
|
// rien à lire.
|
|
FavoriteType.evCharger => EvFavoriteTile(
|
|
live: service.evCharger,
|
|
onSetMode: (m) => service.setChargingInfo(mode: m)),
|
|
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.mutedOf(context))),
|
|
),
|
|
],
|
|
),
|
|
Text(label, style: EtmTokens.sans(size: 12, color: EtmTokens.mutedOf(context))),
|
|
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.mutedOf(context))),
|
|
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.mutedOf(context)),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
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.mutedOf(context)),
|
|
textAlign: TextAlign.center),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
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<HistoryPoint> 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.mutedOf(context))),
|
|
),
|
|
);
|
|
}
|
|
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.mutedOf(context))),
|
|
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, null),
|
|
(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: BoxDecoration(
|
|
color: EtmTokens.surfaceOf(context),
|
|
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.lineOf(context),
|
|
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: Icon(Icons.close_rounded,
|
|
color: EtmTokens.faintOf(context)),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Expanded(
|
|
child: ListView(
|
|
controller: ctrl,
|
|
padding: const EdgeInsets.fromLTRB(18, 12, 18, 24),
|
|
children: _options.map((opt) {
|
|
// `null` = teinte neutre : la table est statique, le gris dépend du
|
|
// thème, il se résout donc ici.
|
|
final (type, title, subtitle, icon, teinte) = opt;
|
|
final color = teinte ?? EtmTokens.mutedOf(context);
|
|
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.bgOf(context) : EtmTokens.surfaceOf(context),
|
|
borderRadius:
|
|
BorderRadius.circular(EtmTokens.radius),
|
|
border: Border.all(
|
|
color: added
|
|
? EtmTokens.lineOf(context)
|
|
: 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.faintOf(context) : 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.faintOf(context)
|
|
: EtmTokens.navy)),
|
|
Text(subtitle,
|
|
style: EtmTokens.sans(
|
|
size: 11,
|
|
color: EtmTokens.mutedOf(context))),
|
|
],
|
|
),
|
|
),
|
|
Icon(
|
|
added
|
|
? Icons.check_circle_rounded
|
|
: Icons.add_circle_outline_rounded,
|
|
color: added ? EtmTokens.green : color,
|
|
size: 22,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}).toList(),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|