feat(exercice): résolution effective et auto-advance des chronos d'étapes (ticket #76)

Ajoute la résolution effective de l'auto-enchaînement (application/use_cases.dart)
et son support dans l'adapter Drift (infrastructure/local/drift_repositories.dart).
flutter pub get OK, dart format appliqué, analyze propre (mêmes infos
préexistantes), tests verts, build APK debug validé.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-20 13:56:49 +02:00
parent bb90df6411
commit ea1e240c87
4 changed files with 596 additions and 169 deletions

View File

@ -1214,10 +1214,8 @@ final class ProgramUseCases {
exerciseImageMediaIdsSnapshot: input.exerciseImageMediaIdsSnapshot,
exerciseVideoMediaIdSnapshot: input.exerciseVideoMediaIdSnapshot,
exerciseStepsSnapshot: input.exerciseStepsSnapshot,
autoStartNextTimedStepSnapshot:
input.autoStartNextTimedStepSnapshot,
autoStartNextTimedStepOverride:
input.autoStartNextTimedStepOverride,
autoStartNextTimedStepSnapshot: input.autoStartNextTimedStepSnapshot,
autoStartNextTimedStepOverride: input.autoStartNextTimedStepOverride,
exerciseArchivedSnapshot: input.exerciseArchivedSnapshot,
availableTimeSnapshot: input.availableTimeSnapshot,
availableRepsSnapshot: input.availableRepsSnapshot,
@ -1478,8 +1476,7 @@ final class WorkoutTemplateUseCases {
targetRepsOverride: input.targetRepsOverride,
targetScoreOverride: input.targetScoreOverride,
targetScoreTimeMsOverride: input.targetScoreTimeMsOverride,
autoStartNextTimedStepOverride:
input.autoStartNextTimedStepOverride,
autoStartNextTimedStepOverride: input.autoStartNextTimedStepOverride,
),
);
}
@ -2387,16 +2384,43 @@ final class ActiveExerciseStepUseCases {
setIndex: setIndex,
);
final now = clock.now();
final state = await _autoAdvanceElapsedTimers(
context,
await _requiredStepProgressState(
sessionId: sessionId,
programIndex: programIndex,
exerciseIndex: exerciseIndex,
setIndex: setIndex,
),
now,
final currentState = await _requiredStepProgressState(
sessionId: sessionId,
programIndex: programIndex,
exerciseIndex: exerciseIndex,
setIndex: setIndex,
);
final currentStep = _currentStep(context, currentState);
if (currentState.status == ActiveExerciseStepProgressStatus.runningTimer &&
currentStep.type == ExerciseStepType.time) {
final targetMs = currentStep.defaultTargetValue * 1000;
final elapsedMs = currentState.elapsedMillisecondsAt(now);
if (elapsedMs >= targetMs) {
final completed = _stepResult(
context: context,
state: currentState,
step: currentStep,
status: SetResultStatus.completed,
now: now,
actualTimeMs: targetMs,
actualScore: actualScore,
actualScoreTimeMs: actualScoreTimeMs,
note: note,
);
await sessionRepository.saveExerciseStepResult(completed);
await sessionRepository.saveExerciseStepProgressState(
_advanceState(
context,
currentState,
now,
runningTimerOverflowMs: elapsedMs - targetMs,
completedStep: currentStep,
),
);
return completed;
}
}
final state = await _autoAdvanceElapsedTimers(context, currentState, now);
final step = _currentStep(context, state);
if (state.status == ActiveExerciseStepProgressStatus.sequenceComplete) {
throw const DomainException('Exercise step sequence is complete.');
@ -2419,7 +2443,7 @@ final class ActiveExerciseStepUseCases {
);
await sessionRepository.saveExerciseStepResult(completed);
await sessionRepository.saveExerciseStepProgressState(
_advanceState(context, state, now),
_advanceState(context, state, now, completedStep: step),
);
return completed;
}
@ -2453,7 +2477,7 @@ final class ActiveExerciseStepUseCases {
);
await sessionRepository.saveExerciseStepResult(skipped);
await sessionRepository.saveExerciseStepProgressState(
_advanceState(context, state, now),
_advanceState(context, state, now, completedStep: step),
);
return skipped;
}
@ -2491,7 +2515,7 @@ final class ActiveExerciseStepUseCases {
);
skipped.add(result);
await sessionRepository.saveExerciseStepResult(result);
state = _advanceState(context, state, now);
state = _advanceState(context, state, now, completedStep: step);
}
await sessionRepository.saveExerciseStepProgressState(state);
return skipped;
@ -2528,7 +2552,7 @@ final class ActiveExerciseStepUseCases {
);
skipped.add(result);
await sessionRepository.saveExerciseStepResult(result);
state = _advanceState(context, state, now);
state = _advanceState(context, state, now, completedStep: step);
}
await sessionRepository.saveExerciseStepProgressState(state);
return skipped;
@ -2581,6 +2605,7 @@ final class ActiveExerciseStepUseCases {
expectedPassages: snapshot.repsEnabled
? (snapshot.targetReps ?? 1).clamp(1, 1 << 31)
: 1,
autoStartNextTimedStep: snapshot.autoStartNextTimedStepEffective,
);
}
@ -2638,6 +2663,7 @@ final class ActiveExerciseStepUseCases {
current,
now,
runningTimerOverflowMs: overflowMs,
completedStep: step,
);
await sessionRepository.saveExerciseStepProgressState(current);
}
@ -2649,6 +2675,7 @@ final class ActiveExerciseStepUseCases {
ActiveExerciseStepProgressState state,
DateTime now, {
int runningTimerOverflowMs = 0,
ExerciseStep? completedStep,
}) {
var nextPassage = state.currentPassageIndex;
var nextStep = state.currentStepIndex + 1;
@ -2666,8 +2693,15 @@ final class ActiveExerciseStepUseCases {
);
}
final step = context.steps[nextStep];
final shouldStopBeforeNextTimedStep =
!context.autoStartNextTimedStep &&
completedStep?.type == ExerciseStepType.time &&
step.type == ExerciseStepType.time;
final resolvedOverflowMs = shouldStopBeforeNextTimedStep
? 0
: runningTimerOverflowMs;
final status = step.type == ExerciseStepType.time
? runningTimerOverflowMs > 0
? resolvedOverflowMs > 0
? ActiveExerciseStepProgressStatus.runningTimer
: ActiveExerciseStepProgressStatus.stoppedTimer
: ActiveExerciseStepProgressStatus.waitingManual;
@ -2678,7 +2712,7 @@ final class ActiveExerciseStepUseCases {
currentStepSnapshotId: step.id,
status: status,
startedAt: status == ActiveExerciseStepProgressStatus.runningTimer
? now.subtract(Duration(milliseconds: runningTimerOverflowMs))
? now.subtract(Duration(milliseconds: resolvedOverflowMs))
: null,
accumulatedMs: 0,
lastTransitionAt: now,
@ -3320,6 +3354,15 @@ _ResolvedExerciseSnapshot? _findExerciseSnapshot({
}
final exercise = exercises[exerciseIndex] as Map<String, dynamic>;
final id = exercise['id'] as String? ?? 'exercise-$exerciseIndex';
final autoStartNextTimedStepEffective =
_templateStepChainingOverride(
decoded,
workoutTemplateProgramId: programSnapshotId,
snapshotProgramExerciseId: id,
) ??
(exercise['autoStartNextTimedStepOverride'] as bool?) ??
(exercise['autoStartNextTimedStepSnapshot'] as bool?) ??
true;
return _ResolvedExerciseSnapshot(
programSnapshotId: programSnapshotId,
exerciseSnapshotId: id,
@ -3338,9 +3381,31 @@ _ResolvedExerciseSnapshot? _findExerciseSnapshot({
scoreLabelSnapshot: exercise['scoreLabelSnapshot'] as String?,
scoreUnitSnapshot: exercise['scoreUnitSnapshot'] as String?,
steps: _exerciseStepsFromSnapshot(exercise['exerciseStepsSnapshot']),
autoStartNextTimedStepEffective: autoStartNextTimedStepEffective,
);
}
bool? _templateStepChainingOverride(
Map<String, dynamic> resolvedTemplateSnapshot, {
required String workoutTemplateProgramId,
required String snapshotProgramExerciseId,
}) {
final overrides =
(resolvedTemplateSnapshot['overrides'] as List<dynamic>? ?? const []);
for (final raw in overrides) {
if (raw is! Map) {
continue;
}
final override = Map<String, dynamic>.from(raw);
if (override['workoutTemplateProgramId'] == workoutTemplateProgramId &&
override['snapshotProgramExerciseId'] == snapshotProgramExerciseId) {
final value = override['autoStartNextTimedStepOverride'];
return value is bool ? value : null;
}
}
return null;
}
List<_SetPositionSnapshot> _listSetSnapshots(
String resolvedTemplateSnapshotJson,
) {
@ -3422,6 +3487,7 @@ final class _StepSequenceContext {
required this.setIndex,
required this.steps,
required this.expectedPassages,
required this.autoStartNextTimedStep,
});
final String sessionId;
@ -3432,6 +3498,7 @@ final class _StepSequenceContext {
final int setIndex;
final List<ExerciseStep> steps;
final int expectedPassages;
final bool autoStartNextTimedStep;
}
List<WorkoutHistorySetResult> _historyResultsFromActiveResults({
@ -3523,6 +3590,10 @@ Map<String, _ResolvedExerciseSnapshot> _exerciseSnapshotsById(
scoreLabelSnapshot: exercise['scoreLabelSnapshot'] as String?,
scoreUnitSnapshot: exercise['scoreUnitSnapshot'] as String?,
steps: _exerciseStepsFromSnapshot(exercise['exerciseStepsSnapshot']),
autoStartNextTimedStepEffective:
(exercise['autoStartNextTimedStepOverride'] as bool?) ??
(exercise['autoStartNextTimedStepSnapshot'] as bool?) ??
true,
);
}
}
@ -3546,6 +3617,7 @@ final class _ResolvedExerciseSnapshot {
this.scoreLabelSnapshot,
this.scoreUnitSnapshot,
this.steps = const [],
this.autoStartNextTimedStepEffective = true,
});
final String programSnapshotId;
@ -3563,6 +3635,7 @@ final class _ResolvedExerciseSnapshot {
final String? scoreLabelSnapshot;
final String? scoreUnitSnapshot;
final List<ExerciseStep> steps;
final bool autoStartNextTimedStepEffective;
}
List<ExerciseStep> _exerciseStepsFromSnapshot(Object? value) {