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:
@ -16,6 +16,15 @@ enum SetResultStatus { completed, skipped }
|
||||
|
||||
enum ActiveScoreStopwatchStatus { running, stopped }
|
||||
|
||||
enum ActiveExerciseStepProgressStatus {
|
||||
notStarted,
|
||||
waitingManual,
|
||||
runningTimer,
|
||||
pausedTimer,
|
||||
stoppedTimer,
|
||||
sequenceComplete,
|
||||
}
|
||||
|
||||
final class DomainException implements Exception {
|
||||
const DomainException(this.message);
|
||||
|
||||
@ -916,6 +925,166 @@ final class ActiveRestState {
|
||||
}
|
||||
}
|
||||
|
||||
final class ActiveExerciseStepProgressState {
|
||||
ActiveExerciseStepProgressState({
|
||||
required this.metadata,
|
||||
required this.activeWorkoutSessionId,
|
||||
required this.programIndex,
|
||||
required this.exerciseIndex,
|
||||
required this.setIndex,
|
||||
required this.currentPassageIndex,
|
||||
required this.currentStepIndex,
|
||||
required this.currentStepSnapshotId,
|
||||
required this.status,
|
||||
this.startedAt,
|
||||
required this.accumulatedMs,
|
||||
required this.lastTransitionAt,
|
||||
}) {
|
||||
_requireNonNegative(programIndex, 'Program index');
|
||||
_requireNonNegative(exerciseIndex, 'Exercise index');
|
||||
_requireNonNegative(setIndex, 'Set index');
|
||||
_requireNonNegative(currentPassageIndex, 'Current passage index');
|
||||
_requireNonNegative(currentStepIndex, 'Current step index');
|
||||
_nonBlank(currentStepSnapshotId, 'Current step snapshot id');
|
||||
_requireNonNegative(accumulatedMs, 'Accumulated milliseconds');
|
||||
if (status == ActiveExerciseStepProgressStatus.runningTimer &&
|
||||
startedAt == null) {
|
||||
throw const DomainException(
|
||||
'Running exercise step timer requires a start timestamp.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final EntityMetadata metadata;
|
||||
final String activeWorkoutSessionId;
|
||||
final int programIndex;
|
||||
final int exerciseIndex;
|
||||
final int setIndex;
|
||||
final int currentPassageIndex;
|
||||
final int currentStepIndex;
|
||||
final String currentStepSnapshotId;
|
||||
final ActiveExerciseStepProgressStatus status;
|
||||
final DateTime? startedAt;
|
||||
final int accumulatedMs;
|
||||
final DateTime lastTransitionAt;
|
||||
|
||||
int elapsedMillisecondsAt(DateTime now) {
|
||||
if (status != ActiveExerciseStepProgressStatus.runningTimer ||
|
||||
startedAt == null) {
|
||||
return accumulatedMs;
|
||||
}
|
||||
return accumulatedMs + now.difference(startedAt!).inMilliseconds;
|
||||
}
|
||||
|
||||
ActiveExerciseStepProgressState copyWith({
|
||||
EntityMetadata? metadata,
|
||||
int? currentPassageIndex,
|
||||
int? currentStepIndex,
|
||||
String? currentStepSnapshotId,
|
||||
ActiveExerciseStepProgressStatus? status,
|
||||
Object? startedAt = _unchanged,
|
||||
int? accumulatedMs,
|
||||
DateTime? lastTransitionAt,
|
||||
}) {
|
||||
return ActiveExerciseStepProgressState(
|
||||
metadata: metadata ?? this.metadata,
|
||||
activeWorkoutSessionId: activeWorkoutSessionId,
|
||||
programIndex: programIndex,
|
||||
exerciseIndex: exerciseIndex,
|
||||
setIndex: setIndex,
|
||||
currentPassageIndex: currentPassageIndex ?? this.currentPassageIndex,
|
||||
currentStepIndex: currentStepIndex ?? this.currentStepIndex,
|
||||
currentStepSnapshotId:
|
||||
currentStepSnapshotId ?? this.currentStepSnapshotId,
|
||||
status: status ?? this.status,
|
||||
startedAt: startedAt == _unchanged ? this.startedAt : startedAt as DateTime?,
|
||||
accumulatedMs: accumulatedMs ?? this.accumulatedMs,
|
||||
lastTransitionAt: lastTransitionAt ?? this.lastTransitionAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final class ActiveExerciseStepResult {
|
||||
ActiveExerciseStepResult({
|
||||
required this.metadata,
|
||||
required this.activeWorkoutSessionId,
|
||||
required this.programSnapshotId,
|
||||
required this.exerciseSnapshotId,
|
||||
required this.programIndex,
|
||||
required this.exerciseIndex,
|
||||
required this.setIndex,
|
||||
required this.passageIndex,
|
||||
required this.stepIndex,
|
||||
required this.stepSnapshotId,
|
||||
required this.stepNameSnapshot,
|
||||
required this.stepTypeSnapshot,
|
||||
required this.targetValueSnapshot,
|
||||
required this.hasScoreSnapshot,
|
||||
this.scoreInputModeSnapshot,
|
||||
this.scoreLabelSnapshot,
|
||||
this.scoreUnitSnapshot,
|
||||
this.targetScoreSnapshot,
|
||||
this.targetScoreTimeMsSnapshot,
|
||||
required this.status,
|
||||
this.startedAt,
|
||||
this.completedAt,
|
||||
this.actualTimeMs,
|
||||
this.actualReps,
|
||||
this.actualScore,
|
||||
this.actualScoreTimeMs,
|
||||
this.note,
|
||||
}) {
|
||||
_validateExerciseStepResult(
|
||||
programIndex: programIndex,
|
||||
exerciseIndex: exerciseIndex,
|
||||
setIndex: setIndex,
|
||||
passageIndex: passageIndex,
|
||||
stepIndex: stepIndex,
|
||||
stepTypeSnapshot: stepTypeSnapshot,
|
||||
targetValueSnapshot: targetValueSnapshot,
|
||||
hasScoreSnapshot: hasScoreSnapshot,
|
||||
scoreInputModeSnapshot: scoreInputModeSnapshot,
|
||||
scoreLabelSnapshot: scoreLabelSnapshot,
|
||||
scoreUnitSnapshot: scoreUnitSnapshot,
|
||||
targetScoreSnapshot: targetScoreSnapshot,
|
||||
targetScoreTimeMsSnapshot: targetScoreTimeMsSnapshot,
|
||||
status: status,
|
||||
actualTimeMs: actualTimeMs,
|
||||
actualReps: actualReps,
|
||||
actualScore: actualScore,
|
||||
actualScoreTimeMs: actualScoreTimeMs,
|
||||
);
|
||||
}
|
||||
|
||||
final EntityMetadata metadata;
|
||||
final String activeWorkoutSessionId;
|
||||
final String programSnapshotId;
|
||||
final String exerciseSnapshotId;
|
||||
final int programIndex;
|
||||
final int exerciseIndex;
|
||||
final int setIndex;
|
||||
final int passageIndex;
|
||||
final int stepIndex;
|
||||
final String stepSnapshotId;
|
||||
final String stepNameSnapshot;
|
||||
final ExerciseStepType stepTypeSnapshot;
|
||||
final int targetValueSnapshot;
|
||||
final bool hasScoreSnapshot;
|
||||
final ScoreInputMode? scoreInputModeSnapshot;
|
||||
final String? scoreLabelSnapshot;
|
||||
final String? scoreUnitSnapshot;
|
||||
final double? targetScoreSnapshot;
|
||||
final int? targetScoreTimeMsSnapshot;
|
||||
final SetResultStatus status;
|
||||
final DateTime? startedAt;
|
||||
final DateTime? completedAt;
|
||||
final int? actualTimeMs;
|
||||
final int? actualReps;
|
||||
final double? actualScore;
|
||||
final int? actualScoreTimeMs;
|
||||
final String? note;
|
||||
}
|
||||
|
||||
final class WorkoutHistory {
|
||||
const WorkoutHistory({
|
||||
required this.metadata,
|
||||
@ -928,6 +1097,7 @@ final class WorkoutHistory {
|
||||
required this.completed,
|
||||
required this.historySnapshotJson,
|
||||
this.results = const [],
|
||||
this.stepResults = const [],
|
||||
});
|
||||
|
||||
final EntityMetadata metadata;
|
||||
@ -940,6 +1110,7 @@ final class WorkoutHistory {
|
||||
final bool completed;
|
||||
final String historySnapshotJson;
|
||||
final List<WorkoutHistorySetResult> results;
|
||||
final List<WorkoutHistoryStepResult> stepResults;
|
||||
}
|
||||
|
||||
final class WorkoutHistorySetResult {
|
||||
@ -1021,6 +1192,87 @@ final class WorkoutHistorySetResult {
|
||||
final SetResultStatus status;
|
||||
}
|
||||
|
||||
final class WorkoutHistoryStepResult {
|
||||
WorkoutHistoryStepResult({
|
||||
required this.metadata,
|
||||
required this.workoutHistoryId,
|
||||
required this.programSnapshotId,
|
||||
required this.exerciseSnapshotId,
|
||||
required this.programIndex,
|
||||
required this.exerciseIndex,
|
||||
required this.setIndex,
|
||||
required this.passageIndex,
|
||||
required this.stepIndex,
|
||||
required this.stepSnapshotId,
|
||||
required this.stepNameSnapshot,
|
||||
required this.stepTypeSnapshot,
|
||||
required this.targetValueSnapshot,
|
||||
required this.hasScoreSnapshot,
|
||||
this.scoreInputModeSnapshot,
|
||||
this.scoreLabelSnapshot,
|
||||
this.scoreUnitSnapshot,
|
||||
this.targetScoreSnapshot,
|
||||
this.targetScoreTimeMsSnapshot,
|
||||
required this.status,
|
||||
this.startedAt,
|
||||
this.completedAt,
|
||||
this.actualTimeMs,
|
||||
this.actualReps,
|
||||
this.actualScore,
|
||||
this.actualScoreTimeMs,
|
||||
this.note,
|
||||
}) {
|
||||
_validateExerciseStepResult(
|
||||
programIndex: programIndex,
|
||||
exerciseIndex: exerciseIndex,
|
||||
setIndex: setIndex,
|
||||
passageIndex: passageIndex,
|
||||
stepIndex: stepIndex,
|
||||
stepTypeSnapshot: stepTypeSnapshot,
|
||||
targetValueSnapshot: targetValueSnapshot,
|
||||
hasScoreSnapshot: hasScoreSnapshot,
|
||||
scoreInputModeSnapshot: scoreInputModeSnapshot,
|
||||
scoreLabelSnapshot: scoreLabelSnapshot,
|
||||
scoreUnitSnapshot: scoreUnitSnapshot,
|
||||
targetScoreSnapshot: targetScoreSnapshot,
|
||||
targetScoreTimeMsSnapshot: targetScoreTimeMsSnapshot,
|
||||
status: status,
|
||||
actualTimeMs: actualTimeMs,
|
||||
actualReps: actualReps,
|
||||
actualScore: actualScore,
|
||||
actualScoreTimeMs: actualScoreTimeMs,
|
||||
);
|
||||
}
|
||||
|
||||
final EntityMetadata metadata;
|
||||
final String workoutHistoryId;
|
||||
final String programSnapshotId;
|
||||
final String exerciseSnapshotId;
|
||||
final int programIndex;
|
||||
final int exerciseIndex;
|
||||
final int setIndex;
|
||||
final int passageIndex;
|
||||
final int stepIndex;
|
||||
final String stepSnapshotId;
|
||||
final String stepNameSnapshot;
|
||||
final ExerciseStepType stepTypeSnapshot;
|
||||
final int targetValueSnapshot;
|
||||
final bool hasScoreSnapshot;
|
||||
final ScoreInputMode? scoreInputModeSnapshot;
|
||||
final String? scoreLabelSnapshot;
|
||||
final String? scoreUnitSnapshot;
|
||||
final double? targetScoreSnapshot;
|
||||
final int? targetScoreTimeMsSnapshot;
|
||||
final SetResultStatus status;
|
||||
final DateTime? startedAt;
|
||||
final DateTime? completedAt;
|
||||
final int? actualTimeMs;
|
||||
final int? actualReps;
|
||||
final double? actualScore;
|
||||
final int? actualScoreTimeMs;
|
||||
final String? note;
|
||||
}
|
||||
|
||||
String _nonBlank(String? value, String label) {
|
||||
final trimmed = value?.trim();
|
||||
if (trimmed == null || trimmed.isEmpty) {
|
||||
@ -1222,3 +1474,67 @@ void _requireScoreResultShape({
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _validateExerciseStepResult({
|
||||
required int programIndex,
|
||||
required int exerciseIndex,
|
||||
required int setIndex,
|
||||
required int passageIndex,
|
||||
required int stepIndex,
|
||||
required ExerciseStepType stepTypeSnapshot,
|
||||
required int targetValueSnapshot,
|
||||
required bool hasScoreSnapshot,
|
||||
required ScoreInputMode? scoreInputModeSnapshot,
|
||||
required String? scoreLabelSnapshot,
|
||||
required String? scoreUnitSnapshot,
|
||||
required double? targetScoreSnapshot,
|
||||
required int? targetScoreTimeMsSnapshot,
|
||||
required SetResultStatus status,
|
||||
required int? actualTimeMs,
|
||||
required int? actualReps,
|
||||
required double? actualScore,
|
||||
required int? actualScoreTimeMs,
|
||||
}) {
|
||||
_requireNonNegative(programIndex, 'Program index');
|
||||
_requireNonNegative(exerciseIndex, 'Exercise index');
|
||||
_requireNonNegative(setIndex, 'Set index');
|
||||
_requireNonNegative(passageIndex, 'Passage index');
|
||||
_requireNonNegative(stepIndex, 'Step index');
|
||||
_requirePositive(targetValueSnapshot, 'Step target value snapshot');
|
||||
_requireNullableNonNegative(actualTimeMs, 'Actual time milliseconds');
|
||||
_requireNullableNonNegative(actualReps, 'Actual reps');
|
||||
_requireNullableNonNegativeDouble(actualScore, 'Actual score');
|
||||
_requireNullableNonNegative(actualScoreTimeMs, 'Actual score time ms');
|
||||
if (status == SetResultStatus.skipped &&
|
||||
(actualTimeMs != null ||
|
||||
actualReps != null ||
|
||||
actualScore != null ||
|
||||
actualScoreTimeMs != null)) {
|
||||
throw const DomainException('Skipped step results cannot have values.');
|
||||
}
|
||||
if (actualTimeMs != null && stepTypeSnapshot != ExerciseStepType.time) {
|
||||
throw const DomainException('Actual time requires a time step.');
|
||||
}
|
||||
if (actualReps != null && stepTypeSnapshot != ExerciseStepType.reps) {
|
||||
throw const DomainException('Actual reps requires a reps step.');
|
||||
}
|
||||
final mode = scoreInputModeSnapshot ?? ScoreInputMode.manual;
|
||||
_requireScoreTargetShape(
|
||||
scoreEnabled: hasScoreSnapshot,
|
||||
scoreInputMode: mode,
|
||||
targetScore: targetScoreSnapshot,
|
||||
targetScoreTimeMs: targetScoreTimeMsSnapshot,
|
||||
);
|
||||
_requireScoreResultShape(
|
||||
scoreInputMode: mode,
|
||||
actualScore: actualScore,
|
||||
actualScoreTimeMs: actualScoreTimeMs,
|
||||
);
|
||||
if (!hasScoreSnapshot && (actualScore != null || actualScoreTimeMs != null)) {
|
||||
throw const DomainException('Step score result requires active score.');
|
||||
}
|
||||
if (actualScore != null) {
|
||||
_nonBlank(scoreLabelSnapshot, 'Step score label snapshot');
|
||||
_nonBlank(scoreUnitSnapshot, 'Step score unit snapshot');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user