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:
@ -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) {
|
||||
|
||||
@ -3047,8 +3047,7 @@ domain.Exercise _exerciseFromPayload(RemoteSyncedItem item) {
|
||||
defaultTargetReps: payload['defaultTargetReps'] as int?,
|
||||
defaultTargetScore: (payload['defaultTargetScore'] as num?)?.toDouble(),
|
||||
defaultTargetScoreTimeMs: payload['defaultTargetScoreTimeMs'] as int?,
|
||||
autoStartNextTimedStep:
|
||||
payload['autoStartNextTimedStep'] as bool? ?? true,
|
||||
autoStartNextTimedStep: payload['autoStartNextTimedStep'] as bool? ?? true,
|
||||
steps: _stepsFromPayload(payload['steps']),
|
||||
archivedAt: _dateTimeFromPayload(payload['archivedAt']),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user