Persistance (FavoriteWidget) - FavoriteWidget.fromJson / toJson ajoutés dans nymea_models.dart - NymeaService : constructeur appelle _loadFavorites() au démarrage - _saveFavorites() appelé après addFavorite / removeFavorite / reorderFavorites - Clé SharedPreferences : 'etm_favorites_v1' - Résistance aux données corrompues (try/catch sur fromJson) Restyle (favorites_screen.dart) - AppTheme → EtmTokens sur toutes les couleurs et typographies - Valeurs en IBM Plex Mono (size 36 pour les grandes métriques) - Cartes avec EtmTokens.cardShadow + border radius 22 - _EmptyState : bouton vert + étoile + message - _AddSheet : DraggableScrollableSheet restyled, sans emoji, items visuellement distingués (ajouté vs disponible) - ReorderableDelayedDragStartListener pour le drag discret - Simulation auto-démarrée si non connecté (comme Dashboard/Things) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
778 lines
27 KiB
Dart
778 lines
27 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 '../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.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<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.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(),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|