Regroupe l'état de travail en cours réalisé dans un même worktree sur plusieurs tickets/sprints (#85, #136, #145, #155-160, #162-164), mélangeant des tickets QA et inProgress. Ne constitue pas une feature terminée : commit de sauvegarde avant triage/split par ticket en branches feature/* dédiées. Exclut les dossiers d'environnement de build locaux et le heap dump parasite (.gitignore mis à jour). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
11 KiB
issueRef, version, updatedBy, updatedAt
| issueRef | version | updatedBy | updatedAt | ||||
|---|---|---|---|---|---|---|---|
| #94 | 8 |
|
1784936913063 |
Architect — Cadrage point 2 : regroupement des étapes sans chrono (ticket #94)
Périmètre : uniquement le point 2 de l'analyse UX ci-dessus (regroupement/auto-complétion silencieuse des étapes sans chrono). Les points 1/3/4/5 restent des ajustements UI purs, DevFrontend les traite directement sans ce cadrage.
Ce qui existe déjà et se réutilise tel quel
Code lu : lib/application/use_cases.dart (ActiveExerciseStepUseCases, ~L3087-3653) et lib/domain/entities.dart (ActiveExerciseStepProgressState, ActiveExerciseStepResult, ExerciseStep, ActiveExerciseStepProgressStatus).
_StepSequenceContextporte déjàsteps,expectedPassages,autoStartNextTimedStep(résolu depuis #73/#76) — tout ce qu'il faut pour scanner la timeline à venir sans lecture DB supplémentaire._advanceState(context, state, now, {completedStep})fait déjà avancer d'une étape en traversant les frontières de passage (nextStep >= steps.length → nextPassage++) et sait déjà produiresequenceCompleteen fin de dernier passage. C'est exactement la mécanique de traversée qu'il faut réutiliser en boucle pour le regroupement — pas besoin de réécrire la logique de frontière de passage, juste de l'appeler plusieurs fois d'affilée avant l'action déclenchante, comme_autoAdvanceElapsedTimersle fait déjà pour les chronos qui s'enchaînent._stepResult(...)sait déjà construire unActiveExerciseStepResultcompletedavecactualReps ?? step.defaultTargetValue(cf.completeCurrentStep, L3301-3303) — c'est la convention à réutiliser pour l'auto-complétion silencieuse (valeur cible enregistrée comme valeur réalisée, jamaisnull, cohérent avecgametime-architecture-exercise-stepsqui laissait ce choix à l'implémentation).ActiveExerciseStepProgressStatus.waitingManualreste le statut correct pour une étape reps individuelle ; on ne touche pas à l'enum.
Ce qui doit changer : nouvelle logique de calcul, pas de nouveau champ persisté
Pas de changement de modèle de données. ActiveExerciseStepProgressState et ActiveExerciseStepResult restent inchangés dans leur forme. La notion de « suite silencieuse » est dérivée à la volée depuis context.steps + position courante, exactement comme _isNextTimedStepReady (présentation, ~L2354-2368) dérive déjà Chrono suivant prêt sans champ dédié. Raison : la suite n'est qu'une lecture en avance sur une liste déjà chargée en mémoire (max 8 étapes, cf. gametime-architecture-exercise-steps), aucune raison de la persister.
Règle d'éligibilité au regroupement silencieux
Une étape est éligible au regroupement silencieux si et seulement si :
bool _isSilentStep(ExerciseStep step) {
if (step.type == ExerciseStepType.time) return false;
if (step.hasScore && step.scoreInputMode == ScoreInputMode.stopwatch) return false;
return true; // reps sans score, ou reps avec score manuel
}
C'est la traduction directe du cas limite déjà identifié par UX : une étape reps à score chrono a son propre chrono actif et reste un point d'interaction à part entière.
Calcul de la suite courante (nouvelle fonction application, pure, sans I/O)
_SilentStepGroup _resolveSilentGroup(
_StepSequenceContext context,
ActiveExerciseStepProgressState state,
) {
final steps = <ExerciseStep>[];
var passage = state.currentPassageIndex;
var index = state.currentStepIndex;
while (true) {
final step = context.steps[index];
if (!_isSilentStep(step)) {
return _SilentStepGroup(steps: steps, blockingStep: step, blockingPassageIndex: passage, reachesSequenceEnd: false);
}
steps.add(step);
index++;
if (index >= context.steps.length) {
index = 0;
passage++;
if (passage >= context.expectedPassages) {
return _SilentStepGroup(steps: steps, blockingStep: null, blockingPassageIndex: passage, reachesSequenceEnd: true);
}
}
}
}
Appelée uniquement quand l'étape courante (_currentStep(context, state)) est elle-même silencieuse — sinon la vue reste le mode _CurrentStepPane classique existant (étape chronométrée, ou étape reps isolée). En pratique une étape reps isolée forme un groupe à 1 élément : le layout groupé et le layout actuel convergent visuellement pour ce cas trivial. DevFrontend affiche le layout groupé dès qu'il y a ≥1 étape silencieuse en tête, pas seulement ≥2, pour ne pas avoir deux présentations différentes selon la longueur de la suite.
Port/contrat exposé à la présentation
Étendre ActiveExerciseStepProgressView (pas de nouveau port séparé — c'est le même port ActiveExerciseStepUseCases/vue déjà consommé par l'écran) avec :
final class ActiveExerciseStepProgressView {
// ... champs existants inchangés (state, steps, expectedPassages, results)
final List<ExerciseStep> silentPendingSteps; // vide si l'étape courante n'est pas silencieuse
final ExerciseStep? silentGroupBlockingStep; // étape chronométrée qui suit la suite, null si fin de séquence
final bool silentGroupReachesSequenceEnd; // true si la suite va jusqu'à sequenceComplete
}
Calculée dans startOrResumeProgress/readProgress à partir de _resolveSilentGroup, en lecture seule — aucune écriture tant que l'utilisateur n'a pas déclenché l'action de clôture. silentPendingSteps est la liste à afficher (« Pompes 10 reps / Squats 15 reps / Fentes 10 reps ») ; silentGroupBlockingStep alimente la ligne « Chrono suivant : Gainage · 20 s » ; silentGroupReachesSequenceEnd sert à afficher le message de fin de série au lieu du bouton Lancer.
Nouvelle méthode d'auto-complétion silencieuse (écriture)
Un seul point d'entrée, appelé par les deux actions déclenchantes :
Future<ActiveExerciseStepProgressState> completePendingSilentSteps({
required String sessionId,
required int programIndex,
required int exerciseIndex,
required int setIndex,
}) async {
final context = await _stepContext(...);
var state = await _requiredStepProgressState(...);
final now = clock.now();
while (_isSilentStep(_currentStep(context, state))) {
final step = _currentStep(context, state);
final result = _stepResult(
context: context, state: state, step: step,
status: SetResultStatus.completed, now: now,
actualReps: step.type == ExerciseStepType.reps ? step.defaultTargetValue : null,
);
await sessionRepository.saveExerciseStepResult(result);
state = _advanceState(context, state, now, completedStep: step);
}
await sessionRepository.saveExerciseStepProgressState(state);
return state;
}
Réutilise _advanceState tel quel pour la traversée multi-passages (aucune modification de cette méthode nécessaire : elle gère déjà nextPassage++ et sequenceComplete).
Intégration côté « Lancer » (chrono suivant) : startTimer(...) doit appeler completePendingSilentSteps(...) en tout premier (si l'étape courante est silencieuse) avant sa logique actuelle — après quoi l'étape courante est bien l'étape chronométrée bloquante, et le reste de startTimer fonctionne sans modification.
Intégration côté « Terminer la série » : le use case derrière _finishSet côté présentation (recordCurrentSetResult/upsertSetResultAtPosition, lib/application/use_cases.dart ~L2435-2487) doit, quand une séquence d'étapes existe pour la série (context résoluble sans exception) et que l'état n'est pas déjà sequenceComplete, appeler completePendingSilentSteps(...) avant d'enregistrer le résultat de série — silencieusement, sans passer par un dialogue supplémentaire (contrairement au dialogue existant Chrono non lancé qui gère un autre cas). Point d'attention DevBackend : ne déclencher cet appel que si l'étape courante est silencieuse ; si elle est déjà chronométrée/bloquante, ne rien faire (comportement actuel inchangé, l'utilisateur clôture une série avec une étape chrono en cours comme aujourd'hui).
Cas limites couverts par ce calcul
- Traversée de plusieurs passages : couverte nativement par la boucle
whilede_resolveSilentGroup/completePendingSilentSteps, qui réutilise_advanceState— aucune règle spéciale à écrire, c'est la même mécanique que_autoAdvanceElapsedTimersapplique déjà pour les chronos. - Étape reps à score chrono : exclue par
_isSilentStep, casse le regroupement comme demandé — elle redevient un point d'arrêt classique (waitingManual), sans changement de comportement pour elle. - Séquence entièrement sans chrono :
silentGroupReachesSequenceEnd == truedèsstartOrResumeProgress, dès la première étape ;completePendingSilentStepssera appelée uniquement àTerminer la série, donc aucune écriture avant cette action (conforme à « l'utilisateur ne touche le téléphone qu'une fois, à la fin »). - Reprise après kill/pause : rien à faire de spécial,
silentPendingStepsest recalculé à chaquestartOrResumeProgress/readProgressdepuis l'état persistant réel (pas de champ volatile en mémoire à perdre).
Pas de réglage configurable
Confirmé avec la recommandation UX : comportement toujours actif, pas d'override façon autoStartNextTimedStep (#73). Contrairement à #73, il n'existe ici aucune fenêtre de temps réel entre étapes silencieuses (pas de chrono qui tourne), donc aucun cas d'usage produit ne justifie une confirmation étape par étape. Ne pas ajouter de champ domaine Exercise/ProgramExercise/WorkoutTemplateExerciseOverride pour ce point. Si un besoin réel émerge après usage, rouvrir un ticket dédié plutôt que d'anticiper.
Découpage en lots
- B1 — DevBackend :
_isSilentStep,_resolveSilentGroup, extension deActiveExerciseStepProgressView(3 nouveaux champs),completePendingSilentSteps, intégration dansstartTimeret dans le use case de clôture de série (recordCurrentSetResult/équivalent derrière_finishSet). Tests ciblés : suite silencieuse simple, suite traversant 2+ passages, suite bloquée par étape à score chrono, suite qui va jusqu'àsequenceComplete, reprise après kill au milieu d'une suite silencieuse. - F — DevFrontend (en plus des points 1/3/4/5 déjà cadrés UI par UX) : nouveau layout du panneau séquence quand
silentPendingSteps.isNotEmpty(liste « À enchaîner sans interruption » + ligneChrono suivant : nom · cible+ boutonLancer, ou message de fin de série sans bouton dédié sisilentGroupReachesSequenceEnd), réutilisant le format déjà défini par UX dans son cadrage ci-dessus. Dépend de B1.
Étape suivante
- Git : créer la branche de travail pour #94.
- DevBackend : lot B1 ci-dessus (point 2 uniquement).
- DevFrontend : points 1, 2 (lot F, dépend de B1), 3, 4, 5 — cadrage UX complet disponible plus haut dans ce carnet pour 1/3/4/5, cadrage Architect ci-dessus pour le point 2.