feat(config): l'arbitrage descend en bas, et les blocs se replient
Trois changements demandés après le passage sur téléphone. 1. L'ARBITRAGE EN DIRECT PASSE TOUT EN BAS. Il était en tête avec cet argument : « Alloué 3 000 W ne veut rien dire tant qu'on ne sait pas s'il y avait 3 100 ou 12 000 W à distribuer ». L'argument reste juste pour LIRE une allocation — mais cet écran sert à CONFIGURER, et faire descendre l'installateur sous un pavé de télémétrie à chaque ouverture met la lecture devant le geste. La note de rangs ex æquo, elle, RESTE EN TÊTE : elle prévient que l'ordre affiché juste en dessous n'en est pas un, et la lire après les cartes arriverait trop tard. 2. LES DEUX FLÈCHES DE DOMAINE DEVIENNENT UN CHEVRON DE REPLI. Elles faisaient doublon avec la poignée de glissement, désormais vérifiée au doigt sur appareil (48 px, réordonnancement mesuré, 0 px de défilement parasite). Deux chemins pour le même geste, dont l'un sans repli, valaient moins qu'un chemin sûr plus une commande qui manquait. Replié, le bloc dit COMBIEN de charges il contient : sinon replier reviendrait à faire disparaître des charges, et une charge qu'on ne voit plus se lit comme une charge perdue. Le repli est clé par CODE de domaine, pas par index — un domaine déplacé emporte son repli au lieu de replier son voisin. 3. LA CARTE RÉSERVE BATTERIE SE REPLIE AUSSI, avec une réserve qui n'est pas cosmétique : repliée, elle garde le pourcentage ET l'alerte de seuil inatteignable. Replier abrège, ça ne fait pas taire — c'est le réglage qui peut geler toute l'installation sans erreur ni panne, et le cacher ramènerait le paramètre d'usine invisible que cette carte existe pour supprimer. Jamais repliée d'office. Aucun repli n'est persisté, délibérément : c'est une commodité de lecture, pas une configuration. La rendre durable ferait retrouver un écran à moitié caché des semaines plus tard, sans savoir pourquoi. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016vrTVifar2GN5rtUh89Wkq
This commit is contained in:
parent
01fdad73c5
commit
2701a9c0b3
@ -45,6 +45,14 @@ class _BatteryReserveCardState extends State<BatteryReserveCard> {
|
||||
bool _charge = true;
|
||||
String? _erreur;
|
||||
|
||||
/// Carte repliée — l'en-tête et sa valeur restent, le réglage se retire.
|
||||
///
|
||||
/// **Jamais repliée d'office**, et jamais retenue d'une session à l'autre : c'est le
|
||||
/// réglage qui peut geler toute l'installation sans erreur ni panne, et le cacher par
|
||||
/// défaut ramènerait le paramètre d'usine invisible que cette carte existe pour
|
||||
/// supprimer. Se replier reste un geste de l'utilisateur, pour la session en cours.
|
||||
bool _replie = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@ -121,7 +129,40 @@ class _BatteryReserveCardState extends State<BatteryReserveCard> {
|
||||
fontWeight: FontWeight.w700,
|
||||
fontFamily: 'monospace',
|
||||
color: EtmTokens.inkOf(context))),
|
||||
IconButton(
|
||||
key: const ValueKey('replier-reserve-batterie'),
|
||||
visualDensity: VisualDensity.compact,
|
||||
icon: Icon(
|
||||
_replie
|
||||
? Icons.keyboard_arrow_down_rounded
|
||||
: Icons.keyboard_arrow_up_rounded,
|
||||
size: 22),
|
||||
tooltip: _replie ? 'Déplier la réserve' : 'Replier la réserve',
|
||||
onPressed: () => setState(() => _replie = !_replie),
|
||||
),
|
||||
]),
|
||||
// Repliée, la carte garde le POURCENTAGE et, s'il y a lieu, l'alerte. Un seuil
|
||||
// qui gèle l'installation ne doit pas pouvoir être caché : replier abrège, ça
|
||||
// ne fait pas taire.
|
||||
if (_replie && alerte) ...[
|
||||
const SizedBox(height: 4),
|
||||
Row(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
const Icon(Icons.warning_amber_rounded,
|
||||
size: 16, color: EtmTokens.orange),
|
||||
const SizedBox(width: 7),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Seuil au-dessus de la charge actuelle (${soc.round()} %) — rien ne '
|
||||
'sera piloté.',
|
||||
style: TextStyle(
|
||||
fontSize: 11.5,
|
||||
height: 1.35,
|
||||
color: EtmTokens.inkOf(context)),
|
||||
),
|
||||
),
|
||||
]),
|
||||
],
|
||||
if (!_replie) ...[
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
'Sous ce niveau de charge, le budget de surplus est ANNULÉ — pas réduit. '
|
||||
@ -182,6 +223,7 @@ class _BatteryReserveCardState extends State<BatteryReserveCard> {
|
||||
'régler « pour la voiture » couperait alors le reste.',
|
||||
style: TextStyle(fontSize: 10.5, height: 1.35, color: muted),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@ -42,7 +42,7 @@ import 'load_cards.dart';
|
||||
/// Depuis le lot plugin B-bis, toute charge publiée en télémétrie a une entrée en
|
||||
/// configuration. L'inverse reste faux : une charge configurée absente de la télémétrie
|
||||
/// est **désactivée**, jamais perdue — et « non arbitrée » n'est donc pas une erreur.
|
||||
class ControllableLoadsSection extends StatelessWidget {
|
||||
class ControllableLoadsSection extends StatefulWidget {
|
||||
/// Ouvre l'écran de mécanisme d'une charge. Fourni par l'écran hôte, qui connaît la
|
||||
/// navigation ; les trois contrats ne se ressemblent pas, donc il n'y a pas de
|
||||
/// formulaire unique à ouvrir.
|
||||
@ -50,6 +50,22 @@ class ControllableLoadsSection extends StatelessWidget {
|
||||
|
||||
const ControllableLoadsSection({super.key, required this.onConfigure});
|
||||
|
||||
@override
|
||||
State<ControllableLoadsSection> createState() =>
|
||||
_ControllableLoadsSectionState();
|
||||
}
|
||||
|
||||
class _ControllableLoadsSectionState extends State<ControllableLoadsSection> {
|
||||
/// Domaines repliés, par leur code.
|
||||
///
|
||||
/// Clé par **code de domaine** et non par index : un domaine qui se déplace emporte
|
||||
/// son repli, ce qu'un index ne saurait pas faire — il replierait le voisin.
|
||||
///
|
||||
/// **Non persisté**, délibérément : c'est une commodité de lecture, pas une
|
||||
/// configuration. La rendre durable ferait retrouver un écran à moitié caché des
|
||||
/// semaines plus tard, sans savoir pourquoi.
|
||||
final Set<String> _replies = <String>{};
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final provider = context.watch<LoadConfigProvider>();
|
||||
@ -70,9 +86,8 @@ class ControllableLoadsSection extends StatelessWidget {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Le budget du cycle AVANT les charges : « Alloué 3 000 W » ne veut rien dire
|
||||
// tant qu'on ne sait pas s'il y avait 3 100 ou 12 000 W à distribuer.
|
||||
TelemetryHeader(telemetry: telemetry),
|
||||
// La note de rang reste EN TÊTE : elle prévient que l'ordre affiché juste en
|
||||
// dessous n'en est pas un. La lire après les cartes arriverait trop tard.
|
||||
_NoteRangsExAequo(entries: provider.entries),
|
||||
// Le niveau glissable est celui des DOMAINES, pas des cartes.
|
||||
//
|
||||
@ -96,7 +111,12 @@ class ControllableLoadsSection extends StatelessWidget {
|
||||
total: groups.length,
|
||||
provider: provider,
|
||||
telemetry: telemetry,
|
||||
onConfigure: onConfigure,
|
||||
onConfigure: widget.onConfigure,
|
||||
replie: _replies.contains(groups[i].domain),
|
||||
onReplier: () => setState(() {
|
||||
final d = groups[i].domain;
|
||||
_replies.contains(d) ? _replies.remove(d) : _replies.add(d);
|
||||
}),
|
||||
),
|
||||
),
|
||||
// Plus de section séparée pour les bornes : depuis `+etm23` elles ONT une entrée
|
||||
@ -108,6 +128,16 @@ class ControllableLoadsSection extends StatelessWidget {
|
||||
const SizedBox(height: 4),
|
||||
ImpactSaveBar(provider: provider),
|
||||
],
|
||||
// L'arbitrage en direct EN BAS.
|
||||
//
|
||||
// Il était en tête, avec cet argument : « Alloué 3 000 W ne veut rien dire tant
|
||||
// qu'on ne sait pas s'il y avait 3 100 ou 12 000 W à distribuer ». L'argument
|
||||
// reste juste pour LIRE une allocation — mais cet écran sert à CONFIGURER, et
|
||||
// faire descendre l'installateur sous un pavé de télémétrie à chaque ouverture
|
||||
// met la lecture devant le geste. Le budget reste là, sous les charges qu'il
|
||||
// explique, atteignable quand on le cherche.
|
||||
const SizedBox(height: 4),
|
||||
TelemetryHeader(telemetry: telemetry),
|
||||
],
|
||||
);
|
||||
}
|
||||
@ -179,6 +209,10 @@ class _DomainBlock extends StatelessWidget {
|
||||
final LoadTelemetryProvider telemetry;
|
||||
final void Function(BuildContext, LoadConfigEntry) onConfigure;
|
||||
|
||||
/// Bloc replié : l'en-tête reste, les cartes sont retirées de l'arbre.
|
||||
final bool replie;
|
||||
final VoidCallback onReplier;
|
||||
|
||||
const _DomainBlock({
|
||||
super.key,
|
||||
required this.group,
|
||||
@ -187,6 +221,8 @@ class _DomainBlock extends StatelessWidget {
|
||||
required this.provider,
|
||||
required this.telemetry,
|
||||
required this.onConfigure,
|
||||
required this.replie,
|
||||
required this.onReplier,
|
||||
});
|
||||
|
||||
@override
|
||||
@ -233,23 +269,39 @@ class _DomainBlock extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
// Quand le bloc est replié, dire ce qu'il contient — sinon replier revient à
|
||||
// faire disparaître des charges, et une charge qu'on ne voit plus se lit
|
||||
// comme une charge perdue.
|
||||
if (replie)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 4),
|
||||
child: Text(
|
||||
group.loads.length > 1
|
||||
? '${group.loads.length} charges'
|
||||
: '${group.loads.length} charge',
|
||||
style: TextStyle(fontSize: 11.5, color: muted),
|
||||
),
|
||||
),
|
||||
// Un SEUL chevron, qui replie — à la place des deux flèches monter/descendre.
|
||||
//
|
||||
// Elles faisaient doublon avec la poignée de glissement, désormais vérifiée
|
||||
// au doigt sur appareil (48 px, réordonnancement mesuré). Deux chemins pour
|
||||
// le même geste, dont l'un n'a pas de repli, valaient moins qu'un chemin sûr
|
||||
// plus une commande qui manquait.
|
||||
IconButton(
|
||||
key: ValueKey('replier-${group.domain}'),
|
||||
visualDensity: VisualDensity.compact,
|
||||
icon: const Icon(Icons.keyboard_arrow_up_rounded, size: 20),
|
||||
tooltip: 'Monter ce domaine',
|
||||
onPressed:
|
||||
index == 0 ? null : () => provider.reorderDomains(index, index - 1),
|
||||
),
|
||||
IconButton(
|
||||
visualDensity: VisualDensity.compact,
|
||||
icon: const Icon(Icons.keyboard_arrow_down_rounded, size: 20),
|
||||
tooltip: 'Descendre ce domaine',
|
||||
onPressed: index >= total - 1
|
||||
? null
|
||||
: () => provider.reorderDomains(index, index + 2),
|
||||
icon: Icon(
|
||||
replie
|
||||
? Icons.keyboard_arrow_down_rounded
|
||||
: Icons.keyboard_arrow_up_rounded,
|
||||
size: 22),
|
||||
tooltip: replie ? 'Déplier ce domaine' : 'Replier ce domaine',
|
||||
onPressed: onReplier,
|
||||
),
|
||||
]),
|
||||
const SizedBox(height: 6),
|
||||
if (!replie) const SizedBox(height: 6),
|
||||
if (!replie)
|
||||
for (var i = 0; i < group.loads.length; i++)
|
||||
Padding(
|
||||
key: ValueKey('load-${group.loads[i].id}'),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user