feat(exercice): exécution persistante des étapes et éditeur d'étapes (tickets #58, #59)

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:
2026-07-19 19:52:43 +02:00
parent 4731a257a1
commit ebba22db56
14 changed files with 11371 additions and 2 deletions

View File

@ -419,6 +419,108 @@ class ActiveRestStates extends SyncableTable {
];
}
class ActiveExerciseStepProgressStates extends SyncableTable {
@override
String get tableName => 'active_exercise_step_progress_states';
TextColumn get activeWorkoutSessionId =>
text().references(ActiveWorkoutSessions, #id)();
IntColumn get programIndex => integer()();
IntColumn get exerciseIndex => integer()();
IntColumn get setIndex => integer()();
IntColumn get currentPassageIndex => integer()();
IntColumn get currentStepIndex => integer()();
TextColumn get currentStepSnapshotId => text().withLength(min: 1)();
TextColumn get status => text()();
DateTimeColumn get startedAt => dateTime().nullable()();
IntColumn get accumulatedMs => integer()();
DateTimeColumn get lastTransitionAt => dateTime()();
@override
List<String> get customConstraints => [
'UNIQUE (active_workout_session_id, program_index, exercise_index, '
'set_index)',
'CHECK (program_index >= 0)',
'CHECK (exercise_index >= 0)',
'CHECK (set_index >= 0)',
'CHECK (current_passage_index >= 0)',
'CHECK (current_step_index >= 0)',
"CHECK (status IN ('notStarted', 'waitingManual', 'runningTimer', "
"'pausedTimer', 'stoppedTimer', 'sequenceComplete'))",
'CHECK (status != \'runningTimer\' OR started_at IS NOT NULL)',
'CHECK (accumulated_ms >= 0)',
];
}
class ActiveExerciseStepResults extends SyncableTable {
@override
String get tableName => 'active_exercise_step_results';
TextColumn get activeWorkoutSessionId =>
text().references(ActiveWorkoutSessions, #id)();
TextColumn get programSnapshotId => text().withLength(min: 1)();
TextColumn get exerciseSnapshotId => text().withLength(min: 1)();
IntColumn get programIndex => integer()();
IntColumn get exerciseIndex => integer()();
IntColumn get setIndex => integer()();
IntColumn get passageIndex => integer()();
IntColumn get stepIndex => integer()();
TextColumn get stepSnapshotId => text().withLength(min: 1)();
TextColumn get stepNameSnapshot => text().withLength(min: 1)();
TextColumn get stepTypeSnapshot => text()();
IntColumn get targetValueSnapshot => integer()();
BoolColumn get hasScoreSnapshot => boolean()();
TextColumn get scoreInputModeSnapshot => text().nullable()();
TextColumn get scoreLabelSnapshot => text().nullable()();
TextColumn get scoreUnitSnapshot => text().nullable()();
RealColumn get targetScoreSnapshot => real().nullable()();
IntColumn get targetScoreTimeMsSnapshot => integer().nullable()();
TextColumn get status => text()();
DateTimeColumn get startedAt => dateTime().nullable()();
DateTimeColumn get completedAt => dateTime().nullable()();
IntColumn get actualTimeMs => integer().nullable()();
IntColumn get actualReps => integer().nullable()();
RealColumn get actualScore => real().nullable()();
IntColumn get actualScoreTimeMs => integer().nullable()();
TextColumn get note => text().nullable()();
@override
List<String> get customConstraints => [
'UNIQUE (active_workout_session_id, program_index, exercise_index, '
'set_index, passage_index, step_index)',
'CHECK (program_index >= 0)',
'CHECK (exercise_index >= 0)',
'CHECK (set_index >= 0)',
'CHECK (passage_index >= 0)',
'CHECK (step_index >= 0)',
"CHECK (step_type_snapshot IN ('time', 'reps'))",
'CHECK (target_value_snapshot > 0)',
"CHECK (score_input_mode_snapshot IS NULL OR "
"score_input_mode_snapshot IN ('manual', 'stopwatch'))",
"CHECK (status IN ('completed', 'skipped'))",
'CHECK (actual_time_ms IS NULL OR actual_time_ms >= 0)',
'CHECK (actual_reps IS NULL OR actual_reps >= 0)',
'CHECK (actual_score IS NULL OR actual_score >= 0)',
'CHECK (actual_score_time_ms IS NULL OR actual_score_time_ms >= 0)',
'CHECK (actual_time_ms IS NULL OR step_type_snapshot = \'time\')',
'CHECK (actual_reps IS NULL OR step_type_snapshot = \'reps\')',
'CHECK (status != \'skipped\' OR (actual_time_ms IS NULL '
'AND actual_reps IS NULL AND actual_score IS NULL '
'AND actual_score_time_ms IS NULL))',
'CHECK (target_score_snapshot IS NULL OR '
"(has_score_snapshot AND score_input_mode_snapshot = 'manual'))",
'CHECK (target_score_time_ms_snapshot IS NULL OR '
"(has_score_snapshot AND score_input_mode_snapshot = 'stopwatch'))",
'CHECK (actual_score IS NULL OR '
"(has_score_snapshot AND score_input_mode_snapshot = 'manual'))",
'CHECK (actual_score_time_ms IS NULL OR '
"(has_score_snapshot AND score_input_mode_snapshot = 'stopwatch'))",
'CHECK (actual_score IS NULL OR actual_score_time_ms IS NULL)',
'CHECK (target_score_snapshot IS NULL OR '
'target_score_time_ms_snapshot IS NULL)',
];
}
class WorkoutHistories extends SyncableTable {
@override
String get tableName => 'workout_history';
@ -511,6 +613,74 @@ class WorkoutHistorySetResults extends SyncableTable {
];
}
class WorkoutHistoryStepResults extends SyncableTable {
@override
String get tableName => 'workout_history_step_results';
TextColumn get workoutHistoryId => text().references(WorkoutHistories, #id)();
TextColumn get programSnapshotId => text().withLength(min: 1)();
TextColumn get exerciseSnapshotId => text().withLength(min: 1)();
IntColumn get programIndex => integer()();
IntColumn get exerciseIndex => integer()();
IntColumn get setIndex => integer()();
IntColumn get passageIndex => integer()();
IntColumn get stepIndex => integer()();
TextColumn get stepSnapshotId => text().withLength(min: 1)();
TextColumn get stepNameSnapshot => text().withLength(min: 1)();
TextColumn get stepTypeSnapshot => text()();
IntColumn get targetValueSnapshot => integer()();
BoolColumn get hasScoreSnapshot => boolean()();
TextColumn get scoreInputModeSnapshot => text().nullable()();
TextColumn get scoreLabelSnapshot => text().nullable()();
TextColumn get scoreUnitSnapshot => text().nullable()();
RealColumn get targetScoreSnapshot => real().nullable()();
IntColumn get targetScoreTimeMsSnapshot => integer().nullable()();
TextColumn get status => text()();
DateTimeColumn get startedAt => dateTime().nullable()();
DateTimeColumn get completedAt => dateTime().nullable()();
IntColumn get actualTimeMs => integer().nullable()();
IntColumn get actualReps => integer().nullable()();
RealColumn get actualScore => real().nullable()();
IntColumn get actualScoreTimeMs => integer().nullable()();
TextColumn get note => text().nullable()();
@override
List<String> get customConstraints => [
'UNIQUE (workout_history_id, program_index, exercise_index, set_index, '
'passage_index, step_index)',
'CHECK (program_index >= 0)',
'CHECK (exercise_index >= 0)',
'CHECK (set_index >= 0)',
'CHECK (passage_index >= 0)',
'CHECK (step_index >= 0)',
"CHECK (step_type_snapshot IN ('time', 'reps'))",
'CHECK (target_value_snapshot > 0)',
"CHECK (score_input_mode_snapshot IS NULL OR "
"score_input_mode_snapshot IN ('manual', 'stopwatch'))",
"CHECK (status IN ('completed', 'skipped'))",
'CHECK (actual_time_ms IS NULL OR actual_time_ms >= 0)',
'CHECK (actual_reps IS NULL OR actual_reps >= 0)',
'CHECK (actual_score IS NULL OR actual_score >= 0)',
'CHECK (actual_score_time_ms IS NULL OR actual_score_time_ms >= 0)',
'CHECK (actual_time_ms IS NULL OR step_type_snapshot = \'time\')',
'CHECK (actual_reps IS NULL OR step_type_snapshot = \'reps\')',
'CHECK (status != \'skipped\' OR (actual_time_ms IS NULL '
'AND actual_reps IS NULL AND actual_score IS NULL '
'AND actual_score_time_ms IS NULL))',
'CHECK (target_score_snapshot IS NULL OR '
"(has_score_snapshot AND score_input_mode_snapshot = 'manual'))",
'CHECK (target_score_time_ms_snapshot IS NULL OR '
"(has_score_snapshot AND score_input_mode_snapshot = 'stopwatch'))",
'CHECK (actual_score IS NULL OR '
"(has_score_snapshot AND score_input_mode_snapshot = 'manual'))",
'CHECK (actual_score_time_ms IS NULL OR '
"(has_score_snapshot AND score_input_mode_snapshot = 'stopwatch'))",
'CHECK (actual_score IS NULL OR actual_score_time_ms IS NULL)',
'CHECK (target_score_snapshot IS NULL OR '
'target_score_time_ms_snapshot IS NULL)',
];
}
class ChangeLogEntries extends Table {
@override
String get tableName => 'change_log';