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

@ -502,6 +502,12 @@ final class _WorkoutTemplateProgramDetailScreenState
itemCount: widget.program.exercises.length,
itemBuilder: (context, index) {
final exercise = widget.program.exercises[index];
final autoStartNextTimedStepCustomized =
exercise.autoStartNextTimedStepOverride != null;
final autoStartNextTimedStepStateLabel =
autoStartNextTimedStepCustomized
? 'Personnalisé pour cette séance'
: 'Réglage du programme';
return Card(
child: Padding(
padding: const EdgeInsets.all(12),
@ -564,6 +570,38 @@ final class _WorkoutTemplateProgramDetailScreenState
});
},
),
if (exercise.exerciseStepsSnapshot.isNotEmpty) ...[
const SizedBox(height: 16),
Text(
'Séquence',
style: Theme.of(context).textTheme.titleMedium,
),
SwitchListTile(
contentPadding: EdgeInsets.zero,
title: const Text(
'Enchaîner automatiquement les chronos consécutifs',
),
subtitle: Text(
'$autoStartNextTimedStepStateLabel\n'
'${_autoStartHelpText(exercise.autoStartNextTimedStepEffective)}',
),
value: exercise.autoStartNextTimedStepEffective,
onChanged: (value) {
setState(() {
exercise.autoStartNextTimedStepOverride = value;
});
},
),
if (autoStartNextTimedStepCustomized)
TextButton(
onPressed: () {
setState(() {
exercise.autoStartNextTimedStepOverride = null;
});
},
child: const Text('Revenir au réglage du programme'),
),
],
],
),
),
@ -621,6 +659,8 @@ final class WorkoutTemplateProgramDraft {
override.targetTimeSecondsOverride;
exercise.targetRepsOverride = override.targetRepsOverride;
exercise.targetScoreOverride = override.targetScoreOverride;
exercise.autoStartNextTimedStepOverride =
override.autoStartNextTimedStepOverride;
}
}
}
@ -667,6 +707,7 @@ final class WorkoutTemplateProgramDraft {
targetTimeSecondsOverride: exercise.targetTimeSecondsOverride,
targetRepsOverride: exercise.targetRepsOverride,
targetScoreOverride: exercise.targetScoreOverride,
autoStartNextTimedStepOverride: exercise.autoStartNextTimedStepOverride,
);
}
}
@ -680,6 +721,8 @@ final class WorkoutTemplateExerciseDraft {
required this.timeEnabled,
required this.repsEnabled,
required this.scoreEnabled,
this.exerciseStepsSnapshot = const [],
this.autoStartNextTimedStepSnapshot = true,
this.scoreUnitSnapshot,
this.targetTimeSeconds,
this.targetReps,
@ -689,6 +732,7 @@ final class WorkoutTemplateExerciseDraft {
this.targetTimeSecondsOverride,
this.targetRepsOverride,
this.targetScoreOverride,
this.autoStartNextTimedStepOverride,
});
final String snapshotExerciseId;
@ -697,6 +741,8 @@ final class WorkoutTemplateExerciseDraft {
final bool timeEnabled;
final bool repsEnabled;
final bool scoreEnabled;
final List<ExerciseStep> exerciseStepsSnapshot;
final bool autoStartNextTimedStepSnapshot;
final String? scoreUnitSnapshot;
final int? targetTimeSeconds;
final int? targetReps;
@ -706,12 +752,18 @@ final class WorkoutTemplateExerciseDraft {
int? targetTimeSecondsOverride;
int? targetRepsOverride;
double? targetScoreOverride;
bool? autoStartNextTimedStepOverride;
bool get autoStartNextTimedStepEffective {
return autoStartNextTimedStepOverride ?? autoStartNextTimedStepSnapshot;
}
bool get hasOverride {
return setsCountOverride != null ||
targetTimeSecondsOverride != null ||
targetRepsOverride != null ||
targetScoreOverride != null;
targetScoreOverride != null ||
autoStartNextTimedStepOverride != null;
}
}
@ -746,6 +798,10 @@ List<WorkoutTemplateExerciseDraft> _parseExercises(
final rawExercises = snapshot['exercises'] as List<dynamic>? ?? const [];
return rawExercises.map((raw) {
final exercise = raw as Map<String, dynamic>;
final autoStartNextTimedStepSnapshot =
(exercise['autoStartNextTimedStepOverride'] as bool?) ??
(exercise['autoStartNextTimedStepSnapshot'] as bool?) ??
true;
return WorkoutTemplateExerciseDraft(
snapshotExerciseId: exercise['id'] as String,
exerciseNameSnapshot: exercise['exerciseNameSnapshot'] as String,
@ -753,6 +809,10 @@ List<WorkoutTemplateExerciseDraft> _parseExercises(
timeEnabled: exercise['timeEnabled'] as bool,
repsEnabled: exercise['repsEnabled'] as bool,
scoreEnabled: exercise['scoreEnabled'] as bool,
exerciseStepsSnapshot: _parseExerciseSteps(
exercise['exerciseStepsSnapshot'],
),
autoStartNextTimedStepSnapshot: autoStartNextTimedStepSnapshot,
scoreUnitSnapshot: exercise['scoreUnitSnapshot'] as String?,
targetTimeSeconds: exercise['targetTimeSeconds'] as int?,
targetReps: exercise['targetReps'] as int?,
@ -761,6 +821,34 @@ List<WorkoutTemplateExerciseDraft> _parseExercises(
}).toList();
}
List<ExerciseStep> _parseExerciseSteps(Object? value) {
final rawSteps = value as List<dynamic>? ?? const [];
return rawSteps.map((raw) {
final step = raw as Map<String, dynamic>;
return ExerciseStep(
id: step['id'] as String,
position: step['position'] as int,
name: step['name'] as String,
type: ExerciseStepType.values.byName(step['type'] as String),
defaultTargetValue: step['defaultTargetValue'] as int,
hasScore: step['hasScore'] as bool? ?? false,
scoreInputMode: ScoreInputMode.values.byName(
step['scoreInputMode'] as String? ?? ScoreInputMode.manual.name,
),
scoreLabel: step['scoreLabel'] as String?,
scoreUnit: step['scoreUnit'] as String?,
defaultTargetScore: (step['defaultTargetScore'] as num?)?.toDouble(),
defaultTargetScoreTimeMs: step['defaultTargetScoreTimeMs'] as int?,
);
}).toList();
}
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.";
}
String _templateSummary(WorkoutTemplate template) {
final programCount = template.programs.length;
final exerciseCount = template.programs.fold<int>(