feat(exercice): état d'exécution Chrono suivant prêt (ticket #78)
Ajoute l'état "Chrono suivant prêt" sur workout_execution_screen.dart pour l'auto-enchaînement des chronos d'étapes. Corrige _startCurrentStepTimerIfNeeded() qui forçait à tort startTimer() : il se contente maintenant de rafraîchir le ticker d'affichage, laissant le backend (#76) seul décisionnaire du démarrage automatique ou de l'attente du chrono suivant. flutter pub get OK, dart format appliqué (aucun changement), analyze propre (mêmes infos préexistantes), 153/153 tests verts (dont le test de non-régression pour l'enchaînement automatique activé par défaut), build APK debug validé. Dernier ticket de développement du chantier "auto-enchaînement des chronos d'étapes" (#73) — il ne reste que la QA finale (#79). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -740,16 +740,9 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _startCurrentStepTimerIfNeeded() async {
|
Future<void> _startCurrentStepTimerIfNeeded() {
|
||||||
final view = _stepProgress;
|
_refreshStepTicker();
|
||||||
final step = view?.currentStep;
|
return Future<void>.value();
|
||||||
if (view == null ||
|
|
||||||
step == null ||
|
|
||||||
step.type != ExerciseStepType.time ||
|
|
||||||
view.state.status == ActiveExerciseStepProgressStatus.runningTimer) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await _startCurrentStepTimer();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _completeCurrentStep() async {
|
Future<void> _completeCurrentStep() async {
|
||||||
@ -1537,6 +1530,7 @@ final class _StepSequencePanel extends StatelessWidget {
|
|||||||
running:
|
running:
|
||||||
view.state.status ==
|
view.state.status ==
|
||||||
ActiveExerciseStepProgressStatus.runningTimer,
|
ActiveExerciseStepProgressStatus.runningTimer,
|
||||||
|
readyToStart: _isNextTimedStepReady(view),
|
||||||
onStartTimer: onStartTimer,
|
onStartTimer: onStartTimer,
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
@ -1590,12 +1584,14 @@ final class _TimedStepBody extends StatelessWidget {
|
|||||||
required this.step,
|
required this.step,
|
||||||
required this.remainingLabel,
|
required this.remainingLabel,
|
||||||
required this.running,
|
required this.running,
|
||||||
|
required this.readyToStart,
|
||||||
required this.onStartTimer,
|
required this.onStartTimer,
|
||||||
});
|
});
|
||||||
|
|
||||||
final ExerciseStep step;
|
final ExerciseStep step;
|
||||||
final String remainingLabel;
|
final String remainingLabel;
|
||||||
final bool running;
|
final bool running;
|
||||||
|
final bool readyToStart;
|
||||||
final VoidCallback onStartTimer;
|
final VoidCallback onStartTimer;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -1615,10 +1611,22 @@ final class _TimedStepBody extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
if (!running) ...[
|
if (!running) ...[
|
||||||
const SizedBox(height: 12),
|
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(
|
FilledButton.icon(
|
||||||
onPressed: onStartTimer,
|
onPressed: onStartTimer,
|
||||||
icon: const Icon(Icons.play_arrow),
|
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 {
|
final class _RepsStepBody extends StatelessWidget {
|
||||||
const _RepsStepBody({required this.step, required this.onCompleteStep});
|
const _RepsStepBody({required this.step, required this.onCompleteStep});
|
||||||
|
|
||||||
|
|||||||
@ -1202,7 +1202,7 @@ void main() {
|
|||||||
|
|
||||||
await tester.tap(find.text('Démarrer la séquence'));
|
await tester.tap(find.text('Démarrer la séquence'));
|
||||||
await tester.pump();
|
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(const Duration(milliseconds: 200));
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
@ -1211,9 +1211,86 @@ void main() {
|
|||||||
expect(find.text('ÉTAPE 2 / 2'), findsOneWidget);
|
expect(find.text('ÉTAPE 2 / 2'), findsOneWidget);
|
||||||
expect(find.text('Droite'), findsOneWidget);
|
expect(find.text('Droite'), findsOneWidget);
|
||||||
expect(find.text('00:02'), findsOneWidget);
|
expect(find.text('00:02'), findsOneWidget);
|
||||||
|
expect(
|
||||||
|
repository.stepProgressStates.single.status,
|
||||||
|
ActiveExerciseStepProgressStatus.runningTimer,
|
||||||
|
);
|
||||||
expect(audio.longBeeps, 1);
|
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', (
|
testWidgets('la séquence passe au deuxième passage si répétitions > 1', (
|
||||||
tester,
|
tester,
|
||||||
) async {
|
) async {
|
||||||
@ -1460,6 +1537,7 @@ String _sessionSnapshot({
|
|||||||
ScoreInputMode scoreInputMode = ScoreInputMode.manual,
|
ScoreInputMode scoreInputMode = ScoreInputMode.manual,
|
||||||
List<String> imageMediaIds = const [],
|
List<String> imageMediaIds = const [],
|
||||||
List<Map<String, Object?>> exerciseSteps = const [],
|
List<Map<String, Object?>> exerciseSteps = const [],
|
||||||
|
bool autoStartNextTimedStep = true,
|
||||||
String? videoMediaId,
|
String? videoMediaId,
|
||||||
}) {
|
}) {
|
||||||
return jsonEncode({
|
return jsonEncode({
|
||||||
@ -1490,6 +1568,7 @@ String _sessionSnapshot({
|
|||||||
'exerciseVideoMediaIdSnapshot': videoMediaId,
|
'exerciseVideoMediaIdSnapshot': videoMediaId,
|
||||||
'videoMediaIdSnapshot': videoMediaId,
|
'videoMediaIdSnapshot': videoMediaId,
|
||||||
'exerciseStepsSnapshot': exerciseSteps,
|
'exerciseStepsSnapshot': exerciseSteps,
|
||||||
|
'autoStartNextTimedStepSnapshot': autoStartNextTimedStep,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
|
|||||||
Reference in New Issue
Block a user