diff --git a/.ideai/tickets/76/carnet.md b/.ideai/tickets/76/carnet.md index 766ae0b..ebc5bd5 100644 --- a/.ideai/tickets/76/carnet.md +++ b/.ideai/tickets/76/carnet.md @@ -1,6 +1,6 @@ --- issueRef: "#76" -version: 1 -updatedBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"} -updatedAt: 1784547227107 +version: 2 +updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"} +updatedAt: 1784548663380 --- diff --git a/.ideai/tickets/76/issue.md b/.ideai/tickets/76/issue.md index e521d9b..f0012fb 100644 --- a/.ideai/tickets/76/issue.md +++ b/.ideai/tickets/76/issue.md @@ -2,15 +2,15 @@ id: "8896c34d-bc57-424b-967f-d3ef767185e1" number: 76 title: "[DevBackend] Résolution effective et auto-advance des chronos d'étapes" -status: "open" +status: "qa" priority: "medium" sprint: null links: [{"target":"#73","kind":"relatesTo"},{"target":"#75","kind":"dependsOn"}] agentRefs: [{"agentId":"10ee045b-1c41-479e-ba03-dceed9edd495","role":"assigned"}] createdBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"} -updatedBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"} +updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"} createdAt: 1784547227107 -updatedAt: 1784547227107 -version: 1 +updatedAt: 1784548663380 +version: 2 --- Adapter la résolution de séance et `ActiveExerciseStepUseCases` pour appliquer `séance > programme > exercice` à `autoStartNextTimedStep`. Inclure la valeur effective dans le snapshot résolu ou dans `_StepSequenceContext`. Modifier `_advanceState` et `_autoAdvanceElapsedTimers` : si une étape Temps expirée est suivie d'une étape Temps et que la valeur effective est false, enregistrer le résultat de l'étape expirée puis s'arrêter sur l'étape suivante en `stoppedTimer`, sans consommer l'overflow ni démarrer le chrono suivant. Conserver l'auto-enchaînement existant quand true. \ No newline at end of file diff --git a/.ideai/tickets/77/carnet.md b/.ideai/tickets/77/carnet.md index 0d35169..43c8c40 100644 --- a/.ideai/tickets/77/carnet.md +++ b/.ideai/tickets/77/carnet.md @@ -1,6 +1,6 @@ --- issueRef: "#77" -version: 1 -updatedBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"} -updatedAt: 1784547232756 +version: 2 +updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"} +updatedAt: 1784548663818 --- diff --git a/.ideai/tickets/77/issue.md b/.ideai/tickets/77/issue.md index 3f43dbf..ab61526 100644 --- a/.ideai/tickets/77/issue.md +++ b/.ideai/tickets/77/issue.md @@ -2,15 +2,15 @@ id: "835be070-248f-42fb-b996-4df86445f154" number: 77 title: "[DevFrontend] Réglages auto-enchaînement exercice, programme et séance" -status: "open" +status: "qa" priority: "medium" sprint: null links: [{"target":"#73","kind":"relatesTo"},{"target":"#75","kind":"dependsOn"}] agentRefs: [{"agentId":"9933c93a-b8a1-4164-a3bb-7063fdad747d","role":"assigned"}] createdBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"} -updatedBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"} +updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"} createdAt: 1784547232756 -updatedAt: 1784547232756 -version: 1 +updatedAt: 1784548663818 +version: 2 --- Ajouter l'UI du réglage `Enchaîner automatiquement les chronos consécutifs` aux trois niveaux : `ExerciseFormScreen` valeur de base visible si séquence activée; `ProgramExerciseCustomizationScreen` override nullable avec indication héritage et action `Revenir au réglage de l'exercice`; `WorkoutTemplateProgramDetailScreen` override nullable avec action `Revenir au réglage du programme`. Attention à propager le champ dans `ProgramExerciseConfig` et `_ProgramExerciseDraft` comme pour `exerciseStepsSnapshot` corrigé au #72. \ No newline at end of file diff --git a/.ideai/tickets/index.json b/.ideai/tickets/index.json index aba4585..29e48b8 100644 --- a/.ideai/tickets/index.json +++ b/.ideai/tickets/index.json @@ -859,25 +859,25 @@ "issueRef": "#76", "path": "76", "title": "[DevBackend] Résolution effective et auto-advance des chronos d'étapes", - "status": "open", + "status": "qa", "priority": "medium", "sprint": null, "assignedAgentIds": [ "10ee045b-1c41-479e-ba03-dceed9edd495" ], - "updatedAt": 1784547227107 + "updatedAt": 1784548663380 }, { "issueRef": "#77", "path": "77", "title": "[DevFrontend] Réglages auto-enchaînement exercice, programme et séance", - "status": "open", + "status": "qa", "priority": "medium", "sprint": null, "assignedAgentIds": [ "9933c93a-b8a1-4164-a3bb-7063fdad747d" ], - "updatedAt": 1784547232756 + "updatedAt": 1784548663818 }, { "issueRef": "#78", diff --git a/lib/presentation/workout_execution_screen.dart b/lib/presentation/workout_execution_screen.dart index 20e7e77..a7fc51f 100644 --- a/lib/presentation/workout_execution_screen.dart +++ b/lib/presentation/workout_execution_screen.dart @@ -740,16 +740,9 @@ final class _WorkoutExecutionScreenState extends State { } } - Future _startCurrentStepTimerIfNeeded() async { - final view = _stepProgress; - final step = view?.currentStep; - if (view == null || - step == null || - step.type != ExerciseStepType.time || - view.state.status == ActiveExerciseStepProgressStatus.runningTimer) { - return; - } - await _startCurrentStepTimer(); + Future _startCurrentStepTimerIfNeeded() { + _refreshStepTicker(); + return Future.value(); } Future _completeCurrentStep() async { @@ -1537,6 +1530,7 @@ final class _StepSequencePanel extends StatelessWidget { running: view.state.status == ActiveExerciseStepProgressStatus.runningTimer, + readyToStart: _isNextTimedStepReady(view), onStartTimer: onStartTimer, ) else @@ -1590,12 +1584,14 @@ final class _TimedStepBody extends StatelessWidget { required this.step, required this.remainingLabel, required this.running, + required this.readyToStart, required this.onStartTimer, }); final ExerciseStep step; final String remainingLabel; final bool running; + final bool readyToStart; final VoidCallback onStartTimer; @override @@ -1615,10 +1611,22 @@ final class _TimedStepBody extends StatelessWidget { ), if (!running) ...[ const SizedBox(height: 12), + if (readyToStart) ...[ + Text( + 'Chrono suivant prêt', + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + color: Theme.of(context).colorScheme.primary, + ), + ), + const SizedBox(height: 8), + ], FilledButton.icon( onPressed: onStartTimer, icon: const Icon(Icons.play_arrow), - label: const Text('Démarrer la séquence'), + label: Text( + readyToStart ? 'Démarrer le chrono' : 'Démarrer la séquence', + ), ), ], ], @@ -1626,6 +1634,22 @@ final class _TimedStepBody extends StatelessWidget { } } +bool _isNextTimedStepReady(ActiveExerciseStepProgressView view) { + final step = view.currentStep; + if (step == null || + step.type != ExerciseStepType.time || + view.state.status != ActiveExerciseStepProgressStatus.stoppedTimer) { + return false; + } + return view.results.any((result) { + if (result.passageIndex < view.state.currentPassageIndex) { + return true; + } + return result.passageIndex == view.state.currentPassageIndex && + result.stepIndex < view.state.currentStepIndex; + }); +} + final class _RepsStepBody extends StatelessWidget { const _RepsStepBody({required this.step, required this.onCompleteStep}); diff --git a/test/presentation/workout_execution_screen_test.dart b/test/presentation/workout_execution_screen_test.dart index e480968..fe2667c 100644 --- a/test/presentation/workout_execution_screen_test.dart +++ b/test/presentation/workout_execution_screen_test.dart @@ -1202,7 +1202,7 @@ void main() { await tester.tap(find.text('Démarrer la séquence')); await tester.pump(); - clock.value = DateTime.utc(2026, 7, 17, 12, 0, 1); + clock.value = DateTime.utc(2026, 7, 17, 12, 0, 1, 200); await tester.pump(const Duration(milliseconds: 200)); await tester.pump(); @@ -1211,9 +1211,86 @@ void main() { expect(find.text('ÉTAPE 2 / 2'), findsOneWidget); expect(find.text('Droite'), findsOneWidget); expect(find.text('00:02'), findsOneWidget); + expect( + repository.stepProgressStates.single.status, + ActiveExerciseStepProgressStatus.runningTimer, + ); expect(audio.longBeeps, 1); }); + testWidgets( + 'chrono suivant prêt attend un démarrage manuel quand l’enchaînement est désactivé', + (tester) async { + await tester.binding.setSurfaceSize(const Size(400, 1600)); + addTearDown(() => tester.binding.setSurfaceSize(null)); + + final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12)); + final repository = _FakeActiveSessionRepository(); + final session = ActiveWorkoutSession( + metadata: _metadata('session-1'), + sourceWorkoutTemplateId: 'template-1', + status: ActiveWorkoutStatus.running, + startedAt: DateTime.utc(2026, 7, 17, 12), + lastPersistedAt: DateTime.utc(2026, 7, 17, 12), + elapsedActiveMs: 0, + currentProgramIndex: 0, + currentExerciseIndex: 0, + currentSetIndex: 0, + resolvedTemplateSnapshotJson: _sessionSnapshot( + targetReps: 1, + autoStartNextTimedStep: false, + exerciseSteps: [ + _step('step-1', 0, 'Gauche', ExerciseStepType.time, 1), + _step('step-2', 1, 'Droite', ExerciseStepType.time, 2), + ], + ), + ); + repository.session = session; + + await tester.pumpWidget( + MaterialApp( + home: WorkoutExecutionScreen( + initialSession: session, + activeUseCases: _activeUseCases(repository, clock), + stepUseCases: _stepUseCases(repository, clock), + stepAudioCuePlayer: _FakeStepAudioCuePlayer(), + closeUseCase: _closeUseCase(repository, clock), + historyUseCases: _historyUseCases(clock), + workoutTemplateUseCases: _workoutTemplateUseCases(), + ), + ), + ); + await tester.pump(); + + await tester.tap(find.text('Démarrer la séquence')); + await tester.pump(); + clock.value = DateTime.utc(2026, 7, 17, 12, 0, 1, 200); + await tester.pump(const Duration(milliseconds: 200)); + await tester.pump(); + + expect(repository.stepResults.single.stepIndex, 0); + expect(find.text('ÉTAPE 2 / 2'), findsOneWidget); + expect(find.text('Droite'), findsOneWidget); + expect(find.text('00:02'), findsOneWidget); + expect(find.text('Chrono suivant prêt'), findsOneWidget); + expect(find.text('Démarrer le chrono'), findsOneWidget); + expect( + repository.stepProgressStates.single.status, + ActiveExerciseStepProgressStatus.stoppedTimer, + ); + expect(repository.stepProgressStates.single.startedAt, isNull); + + await tester.tap(find.text('Démarrer le chrono')); + await tester.pump(); + + expect( + repository.stepProgressStates.single.status, + ActiveExerciseStepProgressStatus.runningTimer, + ); + expect(repository.stepProgressStates.single.startedAt, clock.value); + }, + ); + testWidgets('la séquence passe au deuxième passage si répétitions > 1', ( tester, ) async { @@ -1460,6 +1537,7 @@ String _sessionSnapshot({ ScoreInputMode scoreInputMode = ScoreInputMode.manual, List imageMediaIds = const [], List> exerciseSteps = const [], + bool autoStartNextTimedStep = true, String? videoMediaId, }) { return jsonEncode({ @@ -1490,6 +1568,7 @@ String _sessionSnapshot({ 'exerciseVideoMediaIdSnapshot': videoMediaId, 'videoMediaIdSnapshot': videoMediaId, 'exerciseStepsSnapshot': exerciseSteps, + 'autoStartNextTimedStepSnapshot': autoStartNextTimedStep, }, ], }),