feat(ui): réglages auto-enchaînement exercice, programme et séance (ticket #77)

Ajoute les réglages d'auto-enchaînement des chronos d'étapes sur
exercise_library_screen.dart, program_screen.dart et
workout_template_screen.dart, s'appuyant sur la résolution du ticket
#76. Propagation du nouveau champ vérifiée exhaustivement sur les 5
points d'impact de _ProgramExerciseDraft (constructeur, fromExercise,
fromProgramExercise, copy(), toConfig()) — pas de répétition du bug
#72. flutter pub get OK, dart format appliqué (7 fichiers reformatés),
analyze propre (mêmes infos préexistantes), 152/152 tests verts, build
APK debug validé. Développés dans le même worktree partagé par
DevBackend et DevFrontend ; feature/step-chaining-77-ui-settings
restée vide, jamais divergée.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-20 13:57:01 +02:00
parent ea1e240c87
commit 4b355152d5
6 changed files with 505 additions and 1 deletions

View File

@ -660,6 +660,11 @@ final class _ProgramExerciseCustomizationScreenState
: scoreUnit == null
? 'Score'
: 'Score ($scoreUnit)';
final autoStartNextTimedStepCustomized =
_draft.autoStartNextTimedStepOverride != null;
final autoStartNextTimedStepStateLabel = autoStartNextTimedStepCustomized
? 'Personnalisé pour ce programme'
: "Réglage de l'exercice";
return Scaffold(
appBar: AppBar(title: const Text("Personnaliser l'exercice")),
bottomNavigationBar: SafeArea(
@ -785,6 +790,36 @@ final class _ProgramExerciseCustomizationScreenState
_draft.targetScoreTimeMs = null;
},
),
if (_draft.exerciseStepsSnapshot.isNotEmpty) ...[
const SizedBox(height: 24),
Text('Séquence', style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 8),
SwitchListTile(
contentPadding: EdgeInsets.zero,
title: const Text(
'Enchaîner automatiquement les chronos consécutifs',
),
subtitle: Text(
'$autoStartNextTimedStepStateLabel\n'
'${_autoStartHelpText(_draft.autoStartNextTimedStepEffective)}',
),
value: _draft.autoStartNextTimedStepEffective,
onChanged: (value) {
setState(() {
_draft.autoStartNextTimedStepOverride = value;
});
},
),
if (autoStartNextTimedStepCustomized)
TextButton(
onPressed: () {
setState(() {
_draft.autoStartNextTimedStepOverride = null;
});
},
child: const Text("Revenir au réglage de l'exercice"),
),
],
const SizedBox(height: 24),
Text('Repos', style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 8),
@ -916,6 +951,8 @@ final class _ProgramExerciseDraft {
this.exerciseImageMediaIdsSnapshot = const [],
this.exerciseVideoMediaIdSnapshot,
this.exerciseStepsSnapshot = const [],
this.autoStartNextTimedStepSnapshot = true,
this.autoStartNextTimedStepOverride,
this.exerciseArchivedSnapshot = false,
required this.availableTimeSnapshot,
required this.availableRepsSnapshot,
@ -946,6 +983,7 @@ final class _ProgramExerciseDraft {
exerciseImageMediaIdsSnapshot: exercise.imageMediaIds,
exerciseVideoMediaIdSnapshot: exercise.videoMediaId,
exerciseStepsSnapshot: exercise.steps,
autoStartNextTimedStepSnapshot: exercise.autoStartNextTimedStep,
exerciseArchivedSnapshot: exercise.archivedAt != null,
availableTimeSnapshot: exercise.hasTimeMeasure,
availableRepsSnapshot: exercise.hasRepsMeasure,
@ -978,6 +1016,8 @@ final class _ProgramExerciseDraft {
exerciseImageMediaIdsSnapshot: exercise.exerciseImageMediaIdsSnapshot,
exerciseVideoMediaIdSnapshot: exercise.exerciseVideoMediaIdSnapshot,
exerciseStepsSnapshot: exercise.exerciseStepsSnapshot,
autoStartNextTimedStepSnapshot: exercise.autoStartNextTimedStepSnapshot,
autoStartNextTimedStepOverride: exercise.autoStartNextTimedStepOverride,
exerciseArchivedSnapshot: exercise.exerciseArchivedSnapshot,
availableTimeSnapshot: exercise.availableTimeSnapshot,
availableRepsSnapshot: exercise.availableRepsSnapshot,
@ -1008,6 +1048,7 @@ final class _ProgramExerciseDraft {
final List<String> exerciseImageMediaIdsSnapshot;
final String? exerciseVideoMediaIdSnapshot;
final List<ExerciseStep> exerciseStepsSnapshot;
final bool autoStartNextTimedStepSnapshot;
final bool exerciseArchivedSnapshot;
final bool availableTimeSnapshot;
final bool availableRepsSnapshot;
@ -1022,6 +1063,11 @@ final class _ProgramExerciseDraft {
double? targetScore;
int? targetScoreTimeMs;
int? restSecondsOverride;
bool? autoStartNextTimedStepOverride;
bool get autoStartNextTimedStepEffective {
return autoStartNextTimedStepOverride ?? autoStartNextTimedStepSnapshot;
}
_ProgramExerciseDraft copy() {
return _ProgramExerciseDraft(
@ -1034,6 +1080,8 @@ final class _ProgramExerciseDraft {
exerciseImageMediaIdsSnapshot: exerciseImageMediaIdsSnapshot,
exerciseVideoMediaIdSnapshot: exerciseVideoMediaIdSnapshot,
exerciseStepsSnapshot: exerciseStepsSnapshot,
autoStartNextTimedStepSnapshot: autoStartNextTimedStepSnapshot,
autoStartNextTimedStepOverride: autoStartNextTimedStepOverride,
exerciseArchivedSnapshot: exerciseArchivedSnapshot,
availableTimeSnapshot: availableTimeSnapshot,
availableRepsSnapshot: availableRepsSnapshot,
@ -1063,6 +1111,8 @@ final class _ProgramExerciseDraft {
exerciseImageMediaIdsSnapshot: exerciseImageMediaIdsSnapshot,
exerciseVideoMediaIdSnapshot: exerciseVideoMediaIdSnapshot,
exerciseStepsSnapshot: exerciseStepsSnapshot,
autoStartNextTimedStepSnapshot: autoStartNextTimedStepSnapshot,
autoStartNextTimedStepOverride: autoStartNextTimedStepOverride,
exerciseArchivedSnapshot: exerciseArchivedSnapshot,
availableTimeSnapshot: availableTimeSnapshot,
availableRepsSnapshot: availableRepsSnapshot,
@ -1083,6 +1133,12 @@ final class _ProgramExerciseDraft {
}
}
String _autoStartHelpText(bool active) {
return active
? 'Le chrono suivant démarre dès que le précédent arrive à 0.'
: "L'app attendra ton démarrage avant de lancer le chrono suivant.";
}
double? _millisecondsToSeconds(int? milliseconds) {
return milliseconds == null ? null : milliseconds / 1000;
}