Ajoute la persistance de l'exécution par étapes avec résultats par passage (ports, use cases, entités, modèle Drift avec migration) et l'éditeur d'exercice avec séquence d'étapes sur exercise_library_screen.dart. Développés dans le même worktree partagé par DevBackend et DevFrontend ; commit combiné plutôt que séparé car les fakes de repository de test corrigés (nouvelles interfaces #58) touchent plusieurs écrans sans lien avec l'éditeur lui-même (historique, accueil, exécution, séance-modèle). Corrige au passage un ListTile sans ancêtre Material dans le nouvel éditeur d'étapes (Material(type: MaterialType.transparency)). flutter pub get OK, build_runner OK, analyze propre (mêmes infos préexistantes), 93/93 tests verts, build APK debug validé. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -81,6 +81,10 @@ abstract interface class ActiveSessionRepository {
|
||||
Future<void> saveSetResult(ActiveSetResult result);
|
||||
Future<void> saveRestState(ActiveRestState restState);
|
||||
Future<void> saveScoreStopwatchState(ActiveScoreStopwatchState state);
|
||||
Future<void> saveExerciseStepProgressState(
|
||||
ActiveExerciseStepProgressState state,
|
||||
);
|
||||
Future<void> saveExerciseStepResult(ActiveExerciseStepResult result);
|
||||
Future<void> deleteScoreStopwatchState({
|
||||
required String sessionId,
|
||||
required int programIndex,
|
||||
@ -95,7 +99,19 @@ abstract interface class ActiveSessionRepository {
|
||||
required int exerciseIndex,
|
||||
required int setIndex,
|
||||
});
|
||||
Future<ActiveExerciseStepProgressState?> findExerciseStepProgressState({
|
||||
required String sessionId,
|
||||
required int programIndex,
|
||||
required int exerciseIndex,
|
||||
required int setIndex,
|
||||
});
|
||||
Future<List<ActiveSetResult>> listSetResults(String sessionId);
|
||||
Future<List<ActiveExerciseStepProgressState>> listExerciseStepProgressStates(
|
||||
String sessionId,
|
||||
);
|
||||
Future<List<ActiveExerciseStepResult>> listExerciseStepResults(
|
||||
String sessionId,
|
||||
);
|
||||
Future<List<ActiveRestState>> listRestStates(String sessionId);
|
||||
Future<List<ActiveScoreStopwatchState>> listScoreStopwatchStates(
|
||||
String sessionId,
|
||||
@ -107,6 +123,7 @@ abstract interface class WorkoutHistoryRepository {
|
||||
Future<List<WorkoutHistory>> listActive();
|
||||
Future<void> save(WorkoutHistory history);
|
||||
Future<void> saveSetResult(WorkoutHistorySetResult result);
|
||||
Future<void> saveStepResult(WorkoutHistoryStepResult result);
|
||||
Future<void> delete(String id, DateTime deletedAt);
|
||||
}
|
||||
|
||||
|
||||
@ -68,6 +68,7 @@ final class ExerciseUseCases {
|
||||
defaultTargetReps: defaultTargetReps,
|
||||
defaultTargetScore: defaultTargetScore,
|
||||
defaultTargetScoreTimeMs: defaultTargetScoreTimeMs,
|
||||
steps: steps,
|
||||
);
|
||||
await repository.save(exercise);
|
||||
return exercise;
|
||||
@ -962,6 +963,23 @@ final class ActiveWorkoutSessionUseCases {
|
||||
);
|
||||
await sessionRepository.saveScoreStopwatchState(stopped);
|
||||
}
|
||||
final runningStepTimers =
|
||||
(await sessionRepository.listExerciseStepProgressStates(sessionId))
|
||||
.where(
|
||||
(state) =>
|
||||
state.status ==
|
||||
ActiveExerciseStepProgressStatus.runningTimer,
|
||||
);
|
||||
for (final state in runningStepTimers) {
|
||||
final pausedStep = state.copyWith(
|
||||
metadata: state.metadata.touch(now),
|
||||
status: ActiveExerciseStepProgressStatus.pausedTimer,
|
||||
accumulatedMs: state.elapsedMillisecondsAt(now),
|
||||
startedAt: null,
|
||||
lastTransitionAt: now,
|
||||
);
|
||||
await sessionRepository.saveExerciseStepProgressState(pausedStep);
|
||||
}
|
||||
final paused = session.pause(now);
|
||||
await sessionRepository.save(paused);
|
||||
return paused;
|
||||
@ -1441,6 +1459,555 @@ final class ActiveWorkoutSessionUseCases {
|
||||
}
|
||||
}
|
||||
|
||||
final class ActiveExerciseStepProgressView {
|
||||
const ActiveExerciseStepProgressView({
|
||||
required this.state,
|
||||
required this.steps,
|
||||
required this.expectedPassages,
|
||||
required this.results,
|
||||
});
|
||||
|
||||
final ActiveExerciseStepProgressState state;
|
||||
final List<ExerciseStep> steps;
|
||||
final int expectedPassages;
|
||||
final List<ActiveExerciseStepResult> results;
|
||||
|
||||
ExerciseStep? get currentStep =>
|
||||
state.status == ActiveExerciseStepProgressStatus.sequenceComplete
|
||||
? null
|
||||
: steps[state.currentStepIndex];
|
||||
}
|
||||
|
||||
final class ActiveExerciseStepUseCases {
|
||||
const ActiveExerciseStepUseCases({
|
||||
required this.sessionRepository,
|
||||
required this.clock,
|
||||
required this.ids,
|
||||
required this.originDeviceId,
|
||||
});
|
||||
|
||||
final ActiveSessionRepository sessionRepository;
|
||||
final Clock clock;
|
||||
final IdGenerator ids;
|
||||
final String originDeviceId;
|
||||
|
||||
Future<ActiveExerciseStepProgressView> startOrResumeProgress({
|
||||
required String sessionId,
|
||||
required int programIndex,
|
||||
required int exerciseIndex,
|
||||
required int setIndex,
|
||||
}) async {
|
||||
final context = await _stepContext(
|
||||
sessionId: sessionId,
|
||||
programIndex: programIndex,
|
||||
exerciseIndex: exerciseIndex,
|
||||
setIndex: setIndex,
|
||||
);
|
||||
final now = clock.now();
|
||||
final existing = await sessionRepository.findExerciseStepProgressState(
|
||||
sessionId: sessionId,
|
||||
programIndex: programIndex,
|
||||
exerciseIndex: exerciseIndex,
|
||||
setIndex: setIndex,
|
||||
);
|
||||
final state = existing ??
|
||||
_initialStepProgressState(
|
||||
sessionId: sessionId,
|
||||
context: context,
|
||||
now: now,
|
||||
);
|
||||
if (existing == null) {
|
||||
await sessionRepository.saveExerciseStepProgressState(state);
|
||||
}
|
||||
final advanced = await _autoAdvanceElapsedTimers(context, state, now);
|
||||
return ActiveExerciseStepProgressView(
|
||||
state: advanced,
|
||||
steps: context.steps,
|
||||
expectedPassages: context.expectedPassages,
|
||||
results: await _stepResultsForPosition(context),
|
||||
);
|
||||
}
|
||||
|
||||
Future<ActiveExerciseStepProgressView> readProgress({
|
||||
required String sessionId,
|
||||
required int programIndex,
|
||||
required int exerciseIndex,
|
||||
required int setIndex,
|
||||
}) => startOrResumeProgress(
|
||||
sessionId: sessionId,
|
||||
programIndex: programIndex,
|
||||
exerciseIndex: exerciseIndex,
|
||||
setIndex: setIndex,
|
||||
);
|
||||
|
||||
Future<ActiveExerciseStepProgressState> startTimer({
|
||||
required String sessionId,
|
||||
required int programIndex,
|
||||
required int exerciseIndex,
|
||||
required int setIndex,
|
||||
}) async {
|
||||
final view = await startOrResumeProgress(
|
||||
sessionId: sessionId,
|
||||
programIndex: programIndex,
|
||||
exerciseIndex: exerciseIndex,
|
||||
setIndex: setIndex,
|
||||
);
|
||||
final step = view.currentStep;
|
||||
if (step == null || step.type != ExerciseStepType.time) {
|
||||
throw const DomainException('Current exercise step is not timed.');
|
||||
}
|
||||
if (view.state.status == ActiveExerciseStepProgressStatus.runningTimer) {
|
||||
return view.state;
|
||||
}
|
||||
final now = clock.now();
|
||||
final running = view.state.copyWith(
|
||||
metadata: view.state.metadata.touch(now),
|
||||
status: ActiveExerciseStepProgressStatus.runningTimer,
|
||||
startedAt: now,
|
||||
lastTransitionAt: now,
|
||||
);
|
||||
await sessionRepository.saveExerciseStepProgressState(running);
|
||||
return running;
|
||||
}
|
||||
|
||||
Future<ActiveExerciseStepProgressState> pauseTimer({
|
||||
required String sessionId,
|
||||
required int programIndex,
|
||||
required int exerciseIndex,
|
||||
required int setIndex,
|
||||
}) async {
|
||||
final state = await _requiredStepProgressState(
|
||||
sessionId: sessionId,
|
||||
programIndex: programIndex,
|
||||
exerciseIndex: exerciseIndex,
|
||||
setIndex: setIndex,
|
||||
);
|
||||
if (state.status != ActiveExerciseStepProgressStatus.runningTimer) {
|
||||
throw const DomainException('Exercise step timer is not running.');
|
||||
}
|
||||
final now = clock.now();
|
||||
final paused = state.copyWith(
|
||||
metadata: state.metadata.touch(now),
|
||||
status: ActiveExerciseStepProgressStatus.pausedTimer,
|
||||
startedAt: null,
|
||||
accumulatedMs: state.elapsedMillisecondsAt(now),
|
||||
lastTransitionAt: now,
|
||||
);
|
||||
await sessionRepository.saveExerciseStepProgressState(paused);
|
||||
return paused;
|
||||
}
|
||||
|
||||
Future<ActiveExerciseStepProgressState> resumeTimer({
|
||||
required String sessionId,
|
||||
required int programIndex,
|
||||
required int exerciseIndex,
|
||||
required int setIndex,
|
||||
}) {
|
||||
return startTimer(
|
||||
sessionId: sessionId,
|
||||
programIndex: programIndex,
|
||||
exerciseIndex: exerciseIndex,
|
||||
setIndex: setIndex,
|
||||
);
|
||||
}
|
||||
|
||||
int elapsedMilliseconds(ActiveExerciseStepProgressState state) {
|
||||
return state.elapsedMillisecondsAt(clock.now());
|
||||
}
|
||||
|
||||
int remainingMilliseconds({
|
||||
required ActiveExerciseStepProgressState state,
|
||||
required ExerciseStep step,
|
||||
}) {
|
||||
final targetMs = step.defaultTargetValue * 1000;
|
||||
final remaining = targetMs - elapsedMilliseconds(state);
|
||||
return remaining < 0 ? 0 : remaining;
|
||||
}
|
||||
|
||||
Future<ActiveExerciseStepResult> completeCurrentStep({
|
||||
required String sessionId,
|
||||
required int programIndex,
|
||||
required int exerciseIndex,
|
||||
required int setIndex,
|
||||
int? actualReps,
|
||||
double? actualScore,
|
||||
int? actualScoreTimeMs,
|
||||
String? note,
|
||||
}) async {
|
||||
final context = await _stepContext(
|
||||
sessionId: sessionId,
|
||||
programIndex: programIndex,
|
||||
exerciseIndex: exerciseIndex,
|
||||
setIndex: setIndex,
|
||||
);
|
||||
final now = clock.now();
|
||||
final state = await _autoAdvanceElapsedTimers(
|
||||
context,
|
||||
await _requiredStepProgressState(
|
||||
sessionId: sessionId,
|
||||
programIndex: programIndex,
|
||||
exerciseIndex: exerciseIndex,
|
||||
setIndex: setIndex,
|
||||
),
|
||||
now,
|
||||
);
|
||||
final step = _currentStep(context, state);
|
||||
if (state.status == ActiveExerciseStepProgressStatus.sequenceComplete) {
|
||||
throw const DomainException('Exercise step sequence is complete.');
|
||||
}
|
||||
final completed = _stepResult(
|
||||
context: context,
|
||||
state: state,
|
||||
step: step,
|
||||
status: SetResultStatus.completed,
|
||||
now: now,
|
||||
actualTimeMs: step.type == ExerciseStepType.time
|
||||
? state.elapsedMillisecondsAt(now)
|
||||
: null,
|
||||
actualReps: step.type == ExerciseStepType.reps
|
||||
? actualReps ?? step.defaultTargetValue
|
||||
: null,
|
||||
actualScore: actualScore,
|
||||
actualScoreTimeMs: actualScoreTimeMs,
|
||||
note: note,
|
||||
);
|
||||
await sessionRepository.saveExerciseStepResult(completed);
|
||||
await sessionRepository.saveExerciseStepProgressState(
|
||||
_advanceState(context, state, now),
|
||||
);
|
||||
return completed;
|
||||
}
|
||||
|
||||
Future<ActiveExerciseStepResult> skipCurrentStep({
|
||||
required String sessionId,
|
||||
required int programIndex,
|
||||
required int exerciseIndex,
|
||||
required int setIndex,
|
||||
}) async {
|
||||
final context = await _stepContext(
|
||||
sessionId: sessionId,
|
||||
programIndex: programIndex,
|
||||
exerciseIndex: exerciseIndex,
|
||||
setIndex: setIndex,
|
||||
);
|
||||
final state = await _requiredStepProgressState(
|
||||
sessionId: sessionId,
|
||||
programIndex: programIndex,
|
||||
exerciseIndex: exerciseIndex,
|
||||
setIndex: setIndex,
|
||||
);
|
||||
final now = clock.now();
|
||||
final step = _currentStep(context, state);
|
||||
final skipped = _stepResult(
|
||||
context: context,
|
||||
state: state,
|
||||
step: step,
|
||||
status: SetResultStatus.skipped,
|
||||
now: now,
|
||||
);
|
||||
await sessionRepository.saveExerciseStepResult(skipped);
|
||||
await sessionRepository.saveExerciseStepProgressState(
|
||||
_advanceState(context, state, now),
|
||||
);
|
||||
return skipped;
|
||||
}
|
||||
|
||||
Future<List<ActiveExerciseStepResult>> skipCurrentPassage({
|
||||
required String sessionId,
|
||||
required int programIndex,
|
||||
required int exerciseIndex,
|
||||
required int setIndex,
|
||||
}) async {
|
||||
final context = await _stepContext(
|
||||
sessionId: sessionId,
|
||||
programIndex: programIndex,
|
||||
exerciseIndex: exerciseIndex,
|
||||
setIndex: setIndex,
|
||||
);
|
||||
var state = await _requiredStepProgressState(
|
||||
sessionId: sessionId,
|
||||
programIndex: programIndex,
|
||||
exerciseIndex: exerciseIndex,
|
||||
setIndex: setIndex,
|
||||
);
|
||||
final now = clock.now();
|
||||
final skipped = <ActiveExerciseStepResult>[];
|
||||
final passage = state.currentPassageIndex;
|
||||
while (state.status != ActiveExerciseStepProgressStatus.sequenceComplete &&
|
||||
state.currentPassageIndex == passage) {
|
||||
final step = _currentStep(context, state);
|
||||
final result = _stepResult(
|
||||
context: context,
|
||||
state: state,
|
||||
step: step,
|
||||
status: SetResultStatus.skipped,
|
||||
now: now,
|
||||
);
|
||||
skipped.add(result);
|
||||
await sessionRepository.saveExerciseStepResult(result);
|
||||
state = _advanceState(context, state, now);
|
||||
}
|
||||
await sessionRepository.saveExerciseStepProgressState(state);
|
||||
return skipped;
|
||||
}
|
||||
|
||||
Future<List<ActiveExerciseStepResult>> skipSequence({
|
||||
required String sessionId,
|
||||
required int programIndex,
|
||||
required int exerciseIndex,
|
||||
required int setIndex,
|
||||
}) async {
|
||||
final context = await _stepContext(
|
||||
sessionId: sessionId,
|
||||
programIndex: programIndex,
|
||||
exerciseIndex: exerciseIndex,
|
||||
setIndex: setIndex,
|
||||
);
|
||||
var state = await _requiredStepProgressState(
|
||||
sessionId: sessionId,
|
||||
programIndex: programIndex,
|
||||
exerciseIndex: exerciseIndex,
|
||||
setIndex: setIndex,
|
||||
);
|
||||
final now = clock.now();
|
||||
final skipped = <ActiveExerciseStepResult>[];
|
||||
while (state.status != ActiveExerciseStepProgressStatus.sequenceComplete) {
|
||||
final step = _currentStep(context, state);
|
||||
final result = _stepResult(
|
||||
context: context,
|
||||
state: state,
|
||||
step: step,
|
||||
status: SetResultStatus.skipped,
|
||||
now: now,
|
||||
);
|
||||
skipped.add(result);
|
||||
await sessionRepository.saveExerciseStepResult(result);
|
||||
state = _advanceState(context, state, now);
|
||||
}
|
||||
await sessionRepository.saveExerciseStepProgressState(state);
|
||||
return skipped;
|
||||
}
|
||||
|
||||
Future<ActiveExerciseStepProgressState> _requiredStepProgressState({
|
||||
required String sessionId,
|
||||
required int programIndex,
|
||||
required int exerciseIndex,
|
||||
required int setIndex,
|
||||
}) async {
|
||||
final state = await sessionRepository.findExerciseStepProgressState(
|
||||
sessionId: sessionId,
|
||||
programIndex: programIndex,
|
||||
exerciseIndex: exerciseIndex,
|
||||
setIndex: setIndex,
|
||||
);
|
||||
if (state == null) {
|
||||
throw const DomainException('Exercise step progress state not found.');
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
Future<_StepSequenceContext> _stepContext({
|
||||
required String sessionId,
|
||||
required int programIndex,
|
||||
required int exerciseIndex,
|
||||
required int setIndex,
|
||||
}) async {
|
||||
final session = await sessionRepository.findById(sessionId);
|
||||
if (session == null) {
|
||||
throw const DomainException('Active workout session not found.');
|
||||
}
|
||||
final snapshot = _findExerciseSnapshot(
|
||||
resolvedTemplateSnapshotJson: session.resolvedTemplateSnapshotJson,
|
||||
programIndex: programIndex,
|
||||
exerciseIndex: exerciseIndex,
|
||||
);
|
||||
if (snapshot == null || snapshot.steps.isEmpty) {
|
||||
throw const DomainException('Exercise step sequence not found.');
|
||||
}
|
||||
return _StepSequenceContext(
|
||||
sessionId: sessionId,
|
||||
programSnapshotId: snapshot.programSnapshotId,
|
||||
exerciseSnapshotId: snapshot.exerciseSnapshotId,
|
||||
programIndex: programIndex,
|
||||
exerciseIndex: exerciseIndex,
|
||||
setIndex: setIndex,
|
||||
steps: snapshot.steps,
|
||||
expectedPassages: snapshot.repsEnabled
|
||||
? (snapshot.targetReps ?? 1).clamp(1, 1 << 31)
|
||||
: 1,
|
||||
);
|
||||
}
|
||||
|
||||
ActiveExerciseStepProgressState _initialStepProgressState({
|
||||
required String sessionId,
|
||||
required _StepSequenceContext context,
|
||||
required DateTime now,
|
||||
}) {
|
||||
final firstStep = context.steps.first;
|
||||
return ActiveExerciseStepProgressState(
|
||||
metadata: _newMetadata(ids, originDeviceId, now),
|
||||
activeWorkoutSessionId: sessionId,
|
||||
programIndex: context.programIndex,
|
||||
exerciseIndex: context.exerciseIndex,
|
||||
setIndex: context.setIndex,
|
||||
currentPassageIndex: 0,
|
||||
currentStepIndex: 0,
|
||||
currentStepSnapshotId: firstStep.id,
|
||||
status: firstStep.type == ExerciseStepType.time
|
||||
? ActiveExerciseStepProgressStatus.stoppedTimer
|
||||
: ActiveExerciseStepProgressStatus.waitingManual,
|
||||
accumulatedMs: 0,
|
||||
lastTransitionAt: now,
|
||||
);
|
||||
}
|
||||
|
||||
Future<ActiveExerciseStepProgressState> _autoAdvanceElapsedTimers(
|
||||
_StepSequenceContext context,
|
||||
ActiveExerciseStepProgressState state,
|
||||
DateTime now,
|
||||
) async {
|
||||
var current = state;
|
||||
while (current.status == ActiveExerciseStepProgressStatus.runningTimer) {
|
||||
final step = _currentStep(context, current);
|
||||
if (step.type != ExerciseStepType.time) {
|
||||
break;
|
||||
}
|
||||
final targetMs = step.defaultTargetValue * 1000;
|
||||
final elapsed = current.elapsedMillisecondsAt(now);
|
||||
if (elapsed < targetMs) {
|
||||
break;
|
||||
}
|
||||
final result = _stepResult(
|
||||
context: context,
|
||||
state: current,
|
||||
step: step,
|
||||
status: SetResultStatus.completed,
|
||||
now: now,
|
||||
actualTimeMs: targetMs,
|
||||
);
|
||||
await sessionRepository.saveExerciseStepResult(result);
|
||||
final overflowMs = elapsed - targetMs;
|
||||
current = _advanceState(
|
||||
context,
|
||||
current,
|
||||
now,
|
||||
runningTimerOverflowMs: overflowMs,
|
||||
);
|
||||
await sessionRepository.saveExerciseStepProgressState(current);
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
ActiveExerciseStepProgressState _advanceState(
|
||||
_StepSequenceContext context,
|
||||
ActiveExerciseStepProgressState state,
|
||||
DateTime now, {
|
||||
int runningTimerOverflowMs = 0,
|
||||
}) {
|
||||
var nextPassage = state.currentPassageIndex;
|
||||
var nextStep = state.currentStepIndex + 1;
|
||||
if (nextStep >= context.steps.length) {
|
||||
nextStep = 0;
|
||||
nextPassage++;
|
||||
}
|
||||
if (nextPassage >= context.expectedPassages) {
|
||||
return state.copyWith(
|
||||
metadata: state.metadata.touch(now),
|
||||
status: ActiveExerciseStepProgressStatus.sequenceComplete,
|
||||
startedAt: null,
|
||||
accumulatedMs: 0,
|
||||
lastTransitionAt: now,
|
||||
);
|
||||
}
|
||||
final step = context.steps[nextStep];
|
||||
final status = step.type == ExerciseStepType.time
|
||||
? runningTimerOverflowMs > 0
|
||||
? ActiveExerciseStepProgressStatus.runningTimer
|
||||
: ActiveExerciseStepProgressStatus.stoppedTimer
|
||||
: ActiveExerciseStepProgressStatus.waitingManual;
|
||||
return state.copyWith(
|
||||
metadata: state.metadata.touch(now),
|
||||
currentPassageIndex: nextPassage,
|
||||
currentStepIndex: nextStep,
|
||||
currentStepSnapshotId: step.id,
|
||||
status: status,
|
||||
startedAt: status == ActiveExerciseStepProgressStatus.runningTimer
|
||||
? now.subtract(Duration(milliseconds: runningTimerOverflowMs))
|
||||
: null,
|
||||
accumulatedMs: 0,
|
||||
lastTransitionAt: now,
|
||||
);
|
||||
}
|
||||
|
||||
ExerciseStep _currentStep(
|
||||
_StepSequenceContext context,
|
||||
ActiveExerciseStepProgressState state,
|
||||
) {
|
||||
if (state.status == ActiveExerciseStepProgressStatus.sequenceComplete) {
|
||||
throw const DomainException('Exercise step sequence is complete.');
|
||||
}
|
||||
return context.steps[state.currentStepIndex];
|
||||
}
|
||||
|
||||
Future<List<ActiveExerciseStepResult>> _stepResultsForPosition(
|
||||
_StepSequenceContext context,
|
||||
) async {
|
||||
return (await sessionRepository.listExerciseStepResults(context.sessionId))
|
||||
.where(
|
||||
(result) =>
|
||||
result.programIndex == context.programIndex &&
|
||||
result.exerciseIndex == context.exerciseIndex &&
|
||||
result.setIndex == context.setIndex,
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
ActiveExerciseStepResult _stepResult({
|
||||
required _StepSequenceContext context,
|
||||
required ActiveExerciseStepProgressState state,
|
||||
required ExerciseStep step,
|
||||
required SetResultStatus status,
|
||||
required DateTime now,
|
||||
int? actualTimeMs,
|
||||
int? actualReps,
|
||||
double? actualScore,
|
||||
int? actualScoreTimeMs,
|
||||
String? note,
|
||||
}) {
|
||||
return ActiveExerciseStepResult(
|
||||
metadata: _newMetadata(ids, originDeviceId, now),
|
||||
activeWorkoutSessionId: context.sessionId,
|
||||
programSnapshotId: context.programSnapshotId,
|
||||
exerciseSnapshotId: context.exerciseSnapshotId,
|
||||
programIndex: context.programIndex,
|
||||
exerciseIndex: context.exerciseIndex,
|
||||
setIndex: context.setIndex,
|
||||
passageIndex: state.currentPassageIndex,
|
||||
stepIndex: state.currentStepIndex,
|
||||
stepSnapshotId: step.id,
|
||||
stepNameSnapshot: step.name,
|
||||
stepTypeSnapshot: step.type,
|
||||
targetValueSnapshot: step.defaultTargetValue,
|
||||
hasScoreSnapshot: step.hasScore,
|
||||
scoreInputModeSnapshot: step.hasScore ? step.scoreInputMode : null,
|
||||
scoreLabelSnapshot: step.scoreLabel,
|
||||
scoreUnitSnapshot: step.scoreUnit,
|
||||
targetScoreSnapshot: step.defaultTargetScore,
|
||||
targetScoreTimeMsSnapshot: step.defaultTargetScoreTimeMs,
|
||||
status: status,
|
||||
startedAt: state.startedAt,
|
||||
completedAt: status == SetResultStatus.completed ? now : null,
|
||||
actualTimeMs: status == SetResultStatus.completed ? actualTimeMs : null,
|
||||
actualReps: status == SetResultStatus.completed ? actualReps : null,
|
||||
actualScore: status == SetResultStatus.completed ? actualScore : null,
|
||||
actualScoreTimeMs: status == SetResultStatus.completed
|
||||
? actualScoreTimeMs
|
||||
: null,
|
||||
note: note,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final class CloseWorkoutSessionUseCase {
|
||||
const CloseWorkoutSessionUseCase({
|
||||
required this.sessionRepository,
|
||||
@ -1467,6 +2034,9 @@ final class CloseWorkoutSessionUseCase {
|
||||
}
|
||||
final now = clock.now();
|
||||
final results = await sessionRepository.listSetResults(sessionId);
|
||||
final stepResults = await sessionRepository.listExerciseStepResults(
|
||||
sessionId,
|
||||
);
|
||||
final historyId = ids.newId();
|
||||
final historyResults = _historyResultsFromActiveResults(
|
||||
historyId: historyId,
|
||||
@ -1476,6 +2046,13 @@ final class CloseWorkoutSessionUseCase {
|
||||
ids: ids,
|
||||
originDeviceId: originDeviceId,
|
||||
);
|
||||
final historyStepResults = _historyStepResultsFromActiveResults(
|
||||
historyId: historyId,
|
||||
results: stepResults,
|
||||
now: now,
|
||||
ids: ids,
|
||||
originDeviceId: originDeviceId,
|
||||
);
|
||||
final history = WorkoutHistory(
|
||||
metadata: EntityMetadata(
|
||||
id: historyId,
|
||||
@ -1512,8 +2089,38 @@ final class CloseWorkoutSessionUseCase {
|
||||
},
|
||||
)
|
||||
.toList(),
|
||||
'stepResults': stepResults
|
||||
.map(
|
||||
(result) => {
|
||||
'programSnapshotId': result.programSnapshotId,
|
||||
'exerciseSnapshotId': result.exerciseSnapshotId,
|
||||
'programIndex': result.programIndex,
|
||||
'exerciseIndex': result.exerciseIndex,
|
||||
'setIndex': result.setIndex,
|
||||
'passageIndex': result.passageIndex,
|
||||
'stepIndex': result.stepIndex,
|
||||
'stepSnapshotId': result.stepSnapshotId,
|
||||
'stepNameSnapshot': result.stepNameSnapshot,
|
||||
'stepTypeSnapshot': result.stepTypeSnapshot.name,
|
||||
'targetValueSnapshot': result.targetValueSnapshot,
|
||||
'hasScoreSnapshot': result.hasScoreSnapshot,
|
||||
'scoreInputModeSnapshot': result.scoreInputModeSnapshot?.name,
|
||||
'scoreLabelSnapshot': result.scoreLabelSnapshot,
|
||||
'scoreUnitSnapshot': result.scoreUnitSnapshot,
|
||||
'targetScoreSnapshot': result.targetScoreSnapshot,
|
||||
'targetScoreTimeMsSnapshot': result.targetScoreTimeMsSnapshot,
|
||||
'status': result.status.name,
|
||||
'actualTimeMs': result.actualTimeMs,
|
||||
'actualReps': result.actualReps,
|
||||
'actualScore': result.actualScore,
|
||||
'actualScoreTimeMs': result.actualScoreTimeMs,
|
||||
'note': result.note,
|
||||
},
|
||||
)
|
||||
.toList(),
|
||||
}),
|
||||
results: historyResults,
|
||||
stepResults: historyStepResults,
|
||||
);
|
||||
await historyRepository.save(history);
|
||||
return history;
|
||||
@ -1867,6 +2474,53 @@ _SetPositionSnapshot? _findSetSnapshot({
|
||||
return null;
|
||||
}
|
||||
|
||||
_ResolvedExerciseSnapshot? _findExerciseSnapshot({
|
||||
required String resolvedTemplateSnapshotJson,
|
||||
required int programIndex,
|
||||
required int exerciseIndex,
|
||||
}) {
|
||||
final decoded =
|
||||
jsonDecode(resolvedTemplateSnapshotJson) as Map<String, dynamic>;
|
||||
final programs = (decoded['programs'] as List<dynamic>? ?? const []);
|
||||
if (programIndex < 0 || programIndex >= programs.length) {
|
||||
return null;
|
||||
}
|
||||
final program = programs[programIndex] as Map<String, dynamic>;
|
||||
final programSnapshotId = program['id'] as String? ?? 'program-$programIndex';
|
||||
final programName = program['programNameSnapshot'] as String? ?? '';
|
||||
final programSnapshotJson = program['programSnapshotJson'] as String?;
|
||||
if (programSnapshotJson == null) {
|
||||
return null;
|
||||
}
|
||||
final programSnapshot =
|
||||
jsonDecode(programSnapshotJson) as Map<String, dynamic>;
|
||||
final exercises = (programSnapshot['exercises'] as List<dynamic>? ?? const []);
|
||||
if (exerciseIndex < 0 || exerciseIndex >= exercises.length) {
|
||||
return null;
|
||||
}
|
||||
final exercise = exercises[exerciseIndex] as Map<String, dynamic>;
|
||||
final id = exercise['id'] as String? ?? 'exercise-$exerciseIndex';
|
||||
return _ResolvedExerciseSnapshot(
|
||||
programSnapshotId: programSnapshotId,
|
||||
exerciseSnapshotId: id,
|
||||
programNameSnapshot: programName,
|
||||
exerciseNameSnapshot: exercise['exerciseNameSnapshot'] as String? ?? id,
|
||||
timeEnabled: exercise['timeEnabled'] == true,
|
||||
repsEnabled: exercise['repsEnabled'] == true,
|
||||
scoreEnabled: exercise['scoreEnabled'] == true,
|
||||
targetTimeSeconds: exercise['targetTimeSeconds'] as int?,
|
||||
targetReps: exercise['targetReps'] as int?,
|
||||
targetScore: (exercise['targetScore'] as num?)?.toDouble(),
|
||||
targetScoreTimeMs: exercise['targetScoreTimeMs'] as int?,
|
||||
scoreInputModeSnapshot: _scoreInputModeFromSnapshot(
|
||||
exercise['scoreInputModeSnapshot'],
|
||||
),
|
||||
scoreLabelSnapshot: exercise['scoreLabelSnapshot'] as String?,
|
||||
scoreUnitSnapshot: exercise['scoreUnitSnapshot'] as String?,
|
||||
steps: _exerciseStepsFromSnapshot(exercise['exerciseStepsSnapshot']),
|
||||
);
|
||||
}
|
||||
|
||||
List<_SetPositionSnapshot> _listSetSnapshots(
|
||||
String resolvedTemplateSnapshotJson,
|
||||
) {
|
||||
@ -1938,6 +2592,28 @@ final class _SetPositionSnapshot {
|
||||
final ScoreInputMode scoreInputModeSnapshot;
|
||||
}
|
||||
|
||||
final class _StepSequenceContext {
|
||||
const _StepSequenceContext({
|
||||
required this.sessionId,
|
||||
required this.programSnapshotId,
|
||||
required this.exerciseSnapshotId,
|
||||
required this.programIndex,
|
||||
required this.exerciseIndex,
|
||||
required this.setIndex,
|
||||
required this.steps,
|
||||
required this.expectedPassages,
|
||||
});
|
||||
|
||||
final String sessionId;
|
||||
final String programSnapshotId;
|
||||
final String exerciseSnapshotId;
|
||||
final int programIndex;
|
||||
final int exerciseIndex;
|
||||
final int setIndex;
|
||||
final List<ExerciseStep> steps;
|
||||
final int expectedPassages;
|
||||
}
|
||||
|
||||
List<WorkoutHistorySetResult> _historyResultsFromActiveResults({
|
||||
required String historyId,
|
||||
required List<ActiveSetResult> results,
|
||||
@ -2010,6 +2686,8 @@ Map<String, _ResolvedExerciseSnapshot> _exerciseSnapshotsById(
|
||||
continue;
|
||||
}
|
||||
snapshots[id] = _ResolvedExerciseSnapshot(
|
||||
programSnapshotId: program['id'] as String? ?? id,
|
||||
exerciseSnapshotId: id,
|
||||
programNameSnapshot: programName,
|
||||
exerciseNameSnapshot: exercise['exerciseNameSnapshot'] as String? ?? id,
|
||||
timeEnabled: exercise['timeEnabled'] == true,
|
||||
@ -2024,6 +2702,7 @@ Map<String, _ResolvedExerciseSnapshot> _exerciseSnapshotsById(
|
||||
),
|
||||
scoreLabelSnapshot: exercise['scoreLabelSnapshot'] as String?,
|
||||
scoreUnitSnapshot: exercise['scoreUnitSnapshot'] as String?,
|
||||
steps: _exerciseStepsFromSnapshot(exercise['exerciseStepsSnapshot']),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -2032,6 +2711,8 @@ Map<String, _ResolvedExerciseSnapshot> _exerciseSnapshotsById(
|
||||
|
||||
final class _ResolvedExerciseSnapshot {
|
||||
const _ResolvedExerciseSnapshot({
|
||||
required this.programSnapshotId,
|
||||
required this.exerciseSnapshotId,
|
||||
required this.programNameSnapshot,
|
||||
required this.exerciseNameSnapshot,
|
||||
required this.timeEnabled,
|
||||
@ -2044,8 +2725,11 @@ final class _ResolvedExerciseSnapshot {
|
||||
required this.scoreInputModeSnapshot,
|
||||
this.scoreLabelSnapshot,
|
||||
this.scoreUnitSnapshot,
|
||||
this.steps = const [],
|
||||
});
|
||||
|
||||
final String programSnapshotId;
|
||||
final String exerciseSnapshotId;
|
||||
final String programNameSnapshot;
|
||||
final String exerciseNameSnapshot;
|
||||
final bool timeEnabled;
|
||||
@ -2058,6 +2742,73 @@ final class _ResolvedExerciseSnapshot {
|
||||
final ScoreInputMode scoreInputModeSnapshot;
|
||||
final String? scoreLabelSnapshot;
|
||||
final String? scoreUnitSnapshot;
|
||||
final List<ExerciseStep> steps;
|
||||
}
|
||||
|
||||
List<ExerciseStep> _exerciseStepsFromSnapshot(Object? value) {
|
||||
if (value is! List) {
|
||||
return const [];
|
||||
}
|
||||
return value.map((item) {
|
||||
final json = Map<String, dynamic>.from(item as Map);
|
||||
return ExerciseStep(
|
||||
id: json['id'] as String,
|
||||
position: json['position'] as int,
|
||||
name: json['name'] as String,
|
||||
type: switch (json['type']) {
|
||||
'time' => ExerciseStepType.time,
|
||||
'reps' => ExerciseStepType.reps,
|
||||
_ => throw const DomainException('Unknown exercise step type.'),
|
||||
},
|
||||
defaultTargetValue: json['defaultTargetValue'] as int,
|
||||
hasScore: json['hasScore'] == true,
|
||||
scoreInputMode: _scoreInputModeFromSnapshot(json['scoreInputMode']),
|
||||
scoreLabel: json['scoreLabel'] as String?,
|
||||
scoreUnit: json['scoreUnit'] as String?,
|
||||
defaultTargetScore: (json['defaultTargetScore'] as num?)?.toDouble(),
|
||||
defaultTargetScoreTimeMs: json['defaultTargetScoreTimeMs'] as int?,
|
||||
);
|
||||
}).toList(growable: false);
|
||||
}
|
||||
|
||||
List<WorkoutHistoryStepResult> _historyStepResultsFromActiveResults({
|
||||
required String historyId,
|
||||
required List<ActiveExerciseStepResult> results,
|
||||
required DateTime now,
|
||||
required IdGenerator ids,
|
||||
required String originDeviceId,
|
||||
}) {
|
||||
return results.map((result) {
|
||||
return WorkoutHistoryStepResult(
|
||||
metadata: _newMetadata(ids, originDeviceId, now),
|
||||
workoutHistoryId: historyId,
|
||||
programSnapshotId: result.programSnapshotId,
|
||||
exerciseSnapshotId: result.exerciseSnapshotId,
|
||||
programIndex: result.programIndex,
|
||||
exerciseIndex: result.exerciseIndex,
|
||||
setIndex: result.setIndex,
|
||||
passageIndex: result.passageIndex,
|
||||
stepIndex: result.stepIndex,
|
||||
stepSnapshotId: result.stepSnapshotId,
|
||||
stepNameSnapshot: result.stepNameSnapshot,
|
||||
stepTypeSnapshot: result.stepTypeSnapshot,
|
||||
targetValueSnapshot: result.targetValueSnapshot,
|
||||
hasScoreSnapshot: result.hasScoreSnapshot,
|
||||
scoreInputModeSnapshot: result.scoreInputModeSnapshot,
|
||||
scoreLabelSnapshot: result.scoreLabelSnapshot,
|
||||
scoreUnitSnapshot: result.scoreUnitSnapshot,
|
||||
targetScoreSnapshot: result.targetScoreSnapshot,
|
||||
targetScoreTimeMsSnapshot: result.targetScoreTimeMsSnapshot,
|
||||
status: result.status,
|
||||
startedAt: result.startedAt,
|
||||
completedAt: result.completedAt,
|
||||
actualTimeMs: result.actualTimeMs,
|
||||
actualReps: result.actualReps,
|
||||
actualScore: result.actualScore,
|
||||
actualScoreTimeMs: result.actualScoreTimeMs,
|
||||
note: result.note,
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
EntityMetadata _newMetadata(
|
||||
|
||||
Reference in New Issue
Block a user