Le geste sans repli de cet écran — déplacer une plage — puis le geste inverse laissaient un brouillon qui rend exactement la configuration de la box. L'écran le traitait comme n'importe quel brouillon, et n'offrait plus pour en sortir que des portes mal nommées. Quatre messages, tous faux au même instant : la barre « Modifications en attente » la sortie d'écran « vos changements n'ont pas été envoyés » adopter un rang « enregistrez ou annulez vos modifications » retirer une charge idem Il n'y avait plus rien à enregistrer, ni rien à perdre. `draftIsNoOp` nomme l'état : un brouillon dont les charges utiles brutes, clés triées, sont identiques à celles de la box — ce qui couvre l'ordre ET un patch de champ. `hasSubstantiveChanges` garde désormais les trois portes ; la barre, elle, continue de s'afficher, parce que c'est là qu'est la sortie. Elle dit maintenant « L'ordre est revenu à celui de la box — il n'y a rien à enregistrer » et n'offre qu'un geste, « Terminer ». Pas « Enregistrer » : réécrire tout l'ensemble pour n'y rien changer reconstruirait les charges et réarmerait les verrous, au prix de zéro. ⚠️ Le remède n'est PAS d'effacer le brouillon dès qu'il devient sans effet : la barre disparaîtrait sous le doigt à l'instant où l'utilisateur repose la plage, et « j'ai décidé » redeviendrait « il ne s'est rien passé ». Un état se conclut, il ne s'évapore pas. Un test le fige. Et la note sur le glissement est REPRISE. J'y affirmais un défaut ; le test au doigt montre que le réordonnancement fonctionne, liste dépliée comprise. Le mécanisme du shrinkWrap reste écrit — il est probablement juste — mais comme une LIMITE CONNUE non reproduite, avec le protocole en trois gestes (A témoin, B discriminant, C cause), le contournement (replier les blocs) et le remède nommé. On ne réécrit pas un écran qui marche sur la foi d'une lecture de code. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
88 lines
3.6 KiB
Dart
88 lines
3.6 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:provider/provider.dart';
|
|
|
|
import '../providers/load_config_provider.dart';
|
|
import '../theme/etm_tokens.dart';
|
|
|
|
/// **Quitter un écran avec un brouillon en attente** — demander, jamais décider seul.
|
|
///
|
|
/// ## Pourquoi une alerte, et pas un abandon silencieux
|
|
///
|
|
/// Le brouillon vit dans `LoadConfigProvider`, pas dans l'écran : il **survit** à la
|
|
/// navigation. Quitter sans rien dire laissait donc une configuration modifiée en attente,
|
|
/// invisible, qui réapparaissait au retour — ou disparaissait à la première relecture de la
|
|
/// box (`LoadConfigChanged`, reconnexion), sans que personne ait choisi.
|
|
///
|
|
/// Les deux issues sont mauvaises pour la même raison : **l'utilisateur ne sait pas ce
|
|
/// qu'il laisse derrière lui.** L'alerte rend le choix explicite, et le fait porter à qui
|
|
/// de droit.
|
|
///
|
|
/// ## Ce que chaque bouton fait vraiment
|
|
///
|
|
/// - **Rester** : rien ne bouge, le brouillon reste, l'écran non plus.
|
|
/// - **Quitter sans enregistrer** : le brouillon est **abandonné** (`discardDraft`), et
|
|
/// l'écran se ferme. C'est délibérément destructeur — un brouillon fantôme qui ressort
|
|
/// trois écrans plus loin est pire qu'une modification perdue qu'on a acceptée de perdre.
|
|
///
|
|
/// Aucune écriture n'est proposée ici : enregistrer depuis une boîte de dialogue de sortie
|
|
/// ferait partir une configuration que l'utilisateur n'a pas relue.
|
|
class GardeBrouillon extends StatelessWidget {
|
|
final Widget child;
|
|
|
|
const GardeBrouillon({super.key, required this.child});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final provider = context.watch<LoadConfigProvider>();
|
|
|
|
return PopScope(
|
|
// **`hasSubstantiveChanges`, pas `hasPendingChanges`.**
|
|
//
|
|
// Un brouillon revenu à l'ordre de la box ne fait rien perdre : bloquer la sortie
|
|
// pour annoncer que « vos changements n'ont pas été envoyés » nommait une perte
|
|
// inexistante, et laissait l'écran sans porte honnête. On sort librement, et on
|
|
// ramasse le brouillon vide au passage pour qu'il ne ressorte pas ailleurs.
|
|
canPop: !provider.hasSubstantiveChanges,
|
|
onPopInvokedWithResult: (didPop, _) async {
|
|
if (didPop) {
|
|
if (provider.hasPendingChanges) {
|
|
// Après la frame : on est en train d'être démonté, et notifier pendant
|
|
// un build ferait tomber l'arbre au lieu de fermer proprement.
|
|
WidgetsBinding.instance
|
|
.addPostFrameCallback((_) => provider.discardDraft());
|
|
}
|
|
return;
|
|
}
|
|
final partir = await _demander(context);
|
|
if (partir != true || !context.mounted) return;
|
|
provider.discardDraft();
|
|
Navigator.of(context).pop();
|
|
},
|
|
child: child,
|
|
);
|
|
}
|
|
|
|
Future<bool?> _demander(BuildContext context) => showDialog<bool>(
|
|
context: context,
|
|
builder: (ctx) => AlertDialog(
|
|
title: const Text('Modifications non enregistrées'),
|
|
content: const Text(
|
|
'Vos changements n\'ont pas été envoyés à la box : elle porte toujours la '
|
|
'configuration d\'avant.\n\n'
|
|
'Quitter maintenant les abandonne.',
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(ctx, false),
|
|
child: const Text('Rester'),
|
|
),
|
|
FilledButton(
|
|
style: FilledButton.styleFrom(backgroundColor: EtmTokens.danger),
|
|
onPressed: () => Navigator.pop(ctx, true),
|
|
child: const Text('Quitter sans enregistrer'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|