Files
GameTime/lib/infrastructure/local/tables.dart
Blomios bdd7877ea5 feat(exercice): icône d'exercice (ticket #38)
Étend le modèle Drift (tables.dart, app_database.dart/.g.dart), les
entités/use cases/repositories et les écrans bibliothèque d'exercices
et programme pour associer une icône à chaque exercice. flutter
analyze propre, 72/72 tests verts, build APK debug validé. Dernier
ticket du backlog.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 14:25:26 +02:00

492 lines
20 KiB
Dart

import 'package:drift/drift.dart';
abstract class SyncableTable extends Table {
TextColumn get id => text()();
DateTimeColumn get createdAt => dateTime()();
DateTimeColumn get updatedAt => dateTime()();
DateTimeColumn get deletedAt => dateTime().nullable()();
IntColumn get schemaVersion => integer().withDefault(const Constant(1))();
TextColumn get syncState => text().customConstraint(
"NOT NULL CHECK (sync_state IN ('localOnly', 'dirty', 'synced', "
"'deleted'))",
)();
IntColumn get localRevision =>
integer().customConstraint('NOT NULL CHECK (local_revision >= 0)')();
TextColumn get originDeviceId => text().withLength(min: 1)();
TextColumn get futureOwnerProfileId => text().nullable()();
DateTimeColumn get lastSyncedAt => dateTime().nullable()();
TextColumn get remoteRevision => text().nullable()();
@override
Set<Column> get primaryKey => {id};
@override
List<String> get customConstraints => const [];
}
class MediaAssets extends SyncableTable {
@override
String get tableName => 'media_assets';
TextColumn get kind => text()();
TextColumn get localUri => text().withLength(min: 1)();
TextColumn get mimeType => text().nullable()();
IntColumn get sizeBytes => integer().nullable()();
IntColumn get width => integer().nullable()();
IntColumn get height => integer().nullable()();
IntColumn get durationMs => integer().nullable()();
TextColumn get checksum => text().nullable()();
TextColumn get remoteUri => text().nullable()();
TextColumn get thumbnailLocalUri => text().nullable()();
@override
List<String> get customConstraints => [
"CHECK (kind IN ('image', 'video'))",
'CHECK (size_bytes IS NULL OR size_bytes >= 0)',
'CHECK (width IS NULL OR width > 0)',
'CHECK (height IS NULL OR height > 0)',
'CHECK (duration_ms IS NULL OR duration_ms >= 0)',
];
}
class Exercises extends SyncableTable {
@override
String get tableName => 'exercises';
TextColumn get name => text().withLength(min: 1)();
TextColumn get description => text().nullable()();
@ReferenceName('exerciseIconReferences')
TextColumn get iconMediaId =>
text().nullable().references(MediaAssets, #id)();
@ReferenceName('exerciseVideoReferences')
TextColumn get videoMediaId =>
text().nullable().references(MediaAssets, #id)();
BoolColumn get hasTimeMeasure => boolean()();
BoolColumn get hasRepsMeasure => boolean()();
BoolColumn get hasScoreMeasure => boolean()();
TextColumn get scoreInputMode =>
text().withDefault(const Constant('manual'))();
TextColumn get scoreLabel => text().nullable()();
TextColumn get scoreUnit => text().nullable()();
IntColumn get defaultTargetTimeSeconds => integer().nullable()();
IntColumn get defaultTargetReps => integer().nullable()();
RealColumn get defaultTargetScore => real().nullable()();
IntColumn get defaultTargetScoreTimeMs => integer().nullable()();
DateTimeColumn get archivedAt => dateTime().nullable()();
@override
List<String> get customConstraints => [
'CHECK (has_time_measure OR has_reps_measure OR has_score_measure)',
"CHECK (score_input_mode IN ('manual', 'stopwatch'))",
"CHECK (has_score_measure OR score_input_mode = 'manual')",
"CHECK (score_input_mode != 'manual' OR NOT has_score_measure OR "
'(score_label IS NOT NULL AND length(trim(score_label)) > 0 '
'AND score_unit IS NOT NULL AND length(trim(score_unit)) > 0))',
'CHECK (default_target_time_seconds IS NULL OR '
'default_target_time_seconds > 0)',
'CHECK (default_target_reps IS NULL OR default_target_reps > 0)',
'CHECK (default_target_score IS NULL OR default_target_score > 0)',
'CHECK (default_target_score_time_ms IS NULL OR '
'default_target_score_time_ms > 0)',
];
}
class ExerciseImages extends SyncableTable {
@override
String get tableName => 'exercise_images';
TextColumn get exerciseId => text().references(Exercises, #id)();
TextColumn get mediaAssetId => text().references(MediaAssets, #id)();
IntColumn get position => integer()();
@override
List<String> get customConstraints => [
'UNIQUE (exercise_id, position)',
'UNIQUE (exercise_id, media_asset_id)',
'CHECK (position >= 0 AND position < 5)',
];
}
class Programs extends SyncableTable {
@override
String get tableName => 'programs';
TextColumn get name => text().withLength(min: 1)();
IntColumn get defaultRestSeconds => integer()();
@override
List<String> get customConstraints => ['CHECK (default_rest_seconds >= 0)'];
}
class ProgramExercises extends SyncableTable {
@override
String get tableName => 'program_exercises';
TextColumn get programId => text().references(Programs, #id)();
TextColumn get sourceExerciseId =>
text().nullable().references(Exercises, #id)();
IntColumn get position => integer()();
TextColumn get exerciseNameSnapshot => text().withLength(min: 1)();
TextColumn get exerciseDescriptionSnapshot => text().nullable()();
@ReferenceName('programExerciseImageSnapshotReferences')
TextColumn get exerciseImageMediaIdSnapshot =>
text().nullable().references(MediaAssets, #id)();
TextColumn get exerciseImageMediaIdsSnapshotJson => text().nullable()();
@ReferenceName('programExerciseVideoSnapshotReferences')
TextColumn get exerciseVideoMediaIdSnapshot =>
text().nullable().references(MediaAssets, #id)();
BoolColumn get exerciseArchivedSnapshot =>
boolean().withDefault(const Constant(false))();
BoolColumn get availableTimeSnapshot => boolean()();
BoolColumn get availableRepsSnapshot => boolean()();
BoolColumn get availableScoreSnapshot => boolean()();
TextColumn get scoreInputModeSnapshot =>
text().withDefault(const Constant('manual'))();
TextColumn get scoreLabelSnapshot => text().nullable()();
TextColumn get scoreUnitSnapshot => text().nullable()();
IntColumn get setsCount => integer()();
BoolColumn get timeEnabled => boolean()();
BoolColumn get repsEnabled => boolean()();
BoolColumn get scoreEnabled => boolean()();
IntColumn get targetTimeSeconds => integer().nullable()();
IntColumn get targetReps => integer().nullable()();
RealColumn get targetScore => real().nullable()();
IntColumn get targetScoreTimeMs => integer().nullable()();
IntColumn get restSecondsOverride => integer().nullable()();
@override
List<String> get customConstraints => [
'UNIQUE (program_id, position)',
'CHECK (position >= 0)',
'CHECK (sets_count > 0)',
'CHECK (time_enabled OR reps_enabled OR score_enabled)',
'CHECK (NOT time_enabled OR available_time_snapshot)',
'CHECK (NOT reps_enabled OR available_reps_snapshot)',
'CHECK (NOT score_enabled OR available_score_snapshot)',
'CHECK (target_time_seconds IS NULL OR target_time_seconds > 0)',
'CHECK (target_reps IS NULL OR target_reps > 0)',
'CHECK (target_score IS NULL OR target_score >= 0)',
'CHECK (target_score_time_ms IS NULL OR target_score_time_ms > 0)',
'CHECK (rest_seconds_override IS NULL OR rest_seconds_override >= 0)',
'CHECK (target_time_seconds IS NULL OR time_enabled)',
'CHECK (target_reps IS NULL OR reps_enabled)',
"CHECK (score_input_mode_snapshot IN ('manual', 'stopwatch'))",
'CHECK (target_score IS NULL OR '
"(score_enabled AND score_input_mode_snapshot = 'manual'))",
'CHECK (target_score_time_ms IS NULL OR '
"(score_enabled AND score_input_mode_snapshot = 'stopwatch'))",
'CHECK (target_score IS NULL OR target_score_time_ms IS NULL)',
"CHECK (score_input_mode_snapshot != 'manual' OR "
'NOT available_score_snapshot OR (score_label_snapshot '
'IS NOT NULL AND length(trim(score_label_snapshot)) > 0 '
'AND score_unit_snapshot IS NOT NULL '
'AND length(trim(score_unit_snapshot)) > 0))',
];
}
class WorkoutTemplates extends SyncableTable {
@override
String get tableName => 'workout_templates';
TextColumn get name => text().withLength(min: 1)();
DateTimeColumn get lastStartedAt => dateTime().nullable()();
}
class WorkoutTemplatePrograms extends SyncableTable {
@override
String get tableName => 'workout_template_programs';
TextColumn get workoutTemplateId =>
text().references(WorkoutTemplates, #id)();
TextColumn get sourceProgramId =>
text().nullable().references(Programs, #id)();
IntColumn get position => integer()();
TextColumn get programNameSnapshot => text().withLength(min: 1)();
IntColumn get defaultRestSecondsSnapshot => integer()();
TextColumn get programSnapshotJson => text().withLength(min: 1)();
@override
List<String> get customConstraints => [
'UNIQUE (workout_template_id, position)',
'CHECK (position >= 0)',
'CHECK (default_rest_seconds_snapshot >= 0)',
];
}
class WorkoutTemplateExerciseOverrides extends SyncableTable {
@override
String get tableName => 'workout_template_exercise_overrides';
TextColumn get workoutTemplateProgramId =>
text().references(WorkoutTemplatePrograms, #id)();
TextColumn get snapshotProgramExerciseId => text().withLength(min: 1)();
IntColumn get setsCountOverride => integer().nullable()();
IntColumn get targetTimeSecondsOverride => integer().nullable()();
IntColumn get targetRepsOverride => integer().nullable()();
RealColumn get targetScoreOverride => real().nullable()();
IntColumn get targetScoreTimeMsOverride => integer().nullable()();
@override
List<String> get customConstraints => [
'UNIQUE (workout_template_program_id, snapshot_program_exercise_id)',
'CHECK (sets_count_override IS NULL OR sets_count_override > 0)',
'CHECK (target_time_seconds_override IS NULL OR '
'target_time_seconds_override > 0)',
'CHECK (target_reps_override IS NULL OR target_reps_override > 0)',
'CHECK (target_score_override IS NULL OR target_score_override >= 0)',
'CHECK (target_score_time_ms_override IS NULL OR '
'target_score_time_ms_override > 0)',
'CHECK (target_score_override IS NULL OR '
'target_score_time_ms_override IS NULL)',
];
}
class ActiveWorkoutSessions extends SyncableTable {
@override
String get tableName => 'active_workout_sessions';
TextColumn get sourceWorkoutTemplateId =>
text().nullable().references(WorkoutTemplates, #id)();
TextColumn get status => text()();
DateTimeColumn get startedAt => dateTime()();
DateTimeColumn get pausedAt => dateTime().nullable()();
DateTimeColumn get endedAt => dateTime().nullable()();
DateTimeColumn get lastPersistedAt => dateTime()();
IntColumn get elapsedActiveMs => integer()();
IntColumn get currentProgramIndex => integer()();
IntColumn get currentExerciseIndex => integer()();
IntColumn get currentSetIndex => integer()();
TextColumn get resolvedTemplateSnapshotJson => text().withLength(min: 1)();
@override
List<String> get customConstraints => [
"CHECK (status IN ('running', 'paused', 'savedExit', 'completed', "
"'abandoned'))",
'CHECK (elapsed_active_ms >= 0)',
'CHECK (current_program_index >= 0)',
'CHECK (current_exercise_index >= 0)',
'CHECK (current_set_index >= 0)',
];
}
class ActiveSetResults extends SyncableTable {
@override
String get tableName => 'active_set_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()();
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 scoreInputModeSnapshot =>
text().withDefault(const Constant('manual'))();
TextColumn get scoreLabelSnapshot => text().nullable()();
TextColumn get scoreUnitSnapshot => text().nullable()();
TextColumn get note => text().nullable()();
TextColumn get status => text().withDefault(const Constant('completed'))();
@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 (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 (status IN ('completed', 'skipped'))",
'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 (score_input_mode_snapshot IN ('manual', 'stopwatch'))",
'CHECK (actual_score IS NULL OR '
"score_input_mode_snapshot = 'manual')",
'CHECK (actual_score_time_ms IS NULL OR '
"score_input_mode_snapshot = 'stopwatch')",
'CHECK (actual_score IS NULL OR actual_score_time_ms IS NULL)',
'CHECK (actual_score IS NULL OR (score_label_snapshot IS NOT NULL '
'AND length(trim(score_label_snapshot)) > 0 '
'AND score_unit_snapshot IS NOT NULL '
'AND length(trim(score_unit_snapshot)) > 0))',
];
}
class ActiveScoreStopwatchStates extends SyncableTable {
@override
String get tableName => 'active_score_stopwatch_states';
TextColumn get activeWorkoutSessionId =>
text().references(ActiveWorkoutSessions, #id)();
IntColumn get programIndex => integer()();
IntColumn get exerciseIndex => integer()();
IntColumn get setIndex => integer()();
TextColumn get status => text()();
DateTimeColumn get startedAt => dateTime()();
IntColumn get accumulatedMs => integer()();
DateTimeColumn get stoppedAt => dateTime().nullable()();
@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 (status IN ('running', 'stopped'))",
'CHECK (accumulated_ms >= 0)',
];
}
class ActiveRestStates extends SyncableTable {
@override
String get tableName => 'active_rest_states';
TextColumn get activeWorkoutSessionId =>
text().references(ActiveWorkoutSessions, #id)();
IntColumn get afterProgramIndex => integer()();
IntColumn get afterExerciseIndex => integer()();
IntColumn get afterSetIndex => integer()();
IntColumn get plannedRestSeconds => integer()();
IntColumn get adjustedRestSeconds => integer()();
DateTimeColumn get startedAt => dateTime()();
DateTimeColumn get endedAt => dateTime().nullable()();
DateTimeColumn get skippedAt => dateTime().nullable()();
@override
List<String> get customConstraints => [
'CHECK (after_program_index >= 0)',
'CHECK (after_exercise_index >= 0)',
'CHECK (after_set_index >= 0)',
'CHECK (planned_rest_seconds >= 0)',
'CHECK (adjusted_rest_seconds >= 0)',
'CHECK (ended_at IS NULL OR skipped_at IS NULL)',
];
}
class WorkoutHistories extends SyncableTable {
@override
String get tableName => 'workout_history';
TextColumn get sourceWorkoutTemplateId =>
text().nullable().references(WorkoutTemplates, #id)();
TextColumn get sourceActiveWorkoutSessionId =>
text().nullable().references(ActiveWorkoutSessions, #id)();
TextColumn get nameSnapshot => text().withLength(min: 1)();
DateTimeColumn get startedAt => dateTime()();
DateTimeColumn get endedAt => dateTime()();
IntColumn get totalActiveMs => integer()();
BoolColumn get completed => boolean()();
TextColumn get historySnapshotJson => text().withLength(min: 1)();
@override
List<String> get customConstraints => ['CHECK (total_active_ms >= 0)'];
}
class WorkoutHistorySetResults extends SyncableTable {
@override
String get tableName => 'workout_history_set_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()();
TextColumn get programNameSnapshot => text().withLength(min: 1)();
TextColumn get exerciseNameSnapshot => text().withLength(min: 1)();
BoolColumn get timeEnabledSnapshot => boolean()();
BoolColumn get repsEnabledSnapshot => boolean()();
BoolColumn get scoreEnabledSnapshot => boolean()();
TextColumn get scoreInputModeSnapshot =>
text().withDefault(const Constant('manual'))();
IntColumn get targetTimeSecondsSnapshot => integer().nullable()();
IntColumn get targetRepsSnapshot => integer().nullable()();
RealColumn get targetScoreSnapshot => real().nullable()();
IntColumn get targetScoreTimeMsSnapshot => integer().nullable()();
IntColumn get actualTimeMs => integer().nullable()();
IntColumn get actualReps => integer().nullable()();
RealColumn get actualScore => real().nullable()();
IntColumn get actualScoreTimeMs => integer().nullable()();
TextColumn get scoreLabelSnapshot => text().nullable()();
TextColumn get scoreUnitSnapshot => text().nullable()();
DateTimeColumn get startedAt => dateTime().nullable()();
DateTimeColumn get completedAt => dateTime().nullable()();
TextColumn get status => text().withDefault(const Constant('completed'))();
@override
List<String> get customConstraints => [
'UNIQUE (workout_history_id, program_index, exercise_index, '
'set_index)',
'CHECK (program_index >= 0)',
'CHECK (exercise_index >= 0)',
'CHECK (set_index >= 0)',
'CHECK (time_enabled_snapshot OR reps_enabled_snapshot OR '
'score_enabled_snapshot)',
'CHECK (target_time_seconds_snapshot IS NULL OR '
'target_time_seconds_snapshot > 0)',
'CHECK (target_reps_snapshot IS NULL OR target_reps_snapshot > 0)',
'CHECK (target_score_snapshot IS NULL OR target_score_snapshot >= 0)',
'CHECK (target_score_time_ms_snapshot IS NULL OR '
'target_score_time_ms_snapshot > 0)',
'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 (status IN ('completed', 'skipped'))",
'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 (score_input_mode_snapshot IN ('manual', 'stopwatch'))",
'CHECK (target_score_snapshot IS NULL OR '
"(score_enabled_snapshot AND score_input_mode_snapshot = 'manual'))",
'CHECK (target_score_time_ms_snapshot IS NULL OR '
"(score_enabled_snapshot AND score_input_mode_snapshot = 'stopwatch'))",
'CHECK (target_score_snapshot IS NULL OR '
'target_score_time_ms_snapshot IS NULL)',
'CHECK (actual_score IS NULL OR '
"score_input_mode_snapshot = 'manual')",
'CHECK (actual_score_time_ms IS NULL OR '
"score_input_mode_snapshot = 'stopwatch')",
'CHECK (actual_score IS NULL OR actual_score_time_ms IS NULL)',
'CHECK (actual_score IS NULL OR (score_label_snapshot IS NOT NULL '
'AND length(trim(score_label_snapshot)) > 0 '
'AND score_unit_snapshot IS NOT NULL '
'AND length(trim(score_unit_snapshot)) > 0))',
];
}
class ChangeLogEntries extends Table {
@override
String get tableName => 'change_log';
TextColumn get id => text()();
TextColumn get entityType => text().withLength(min: 1)();
TextColumn get entityId => text().withLength(min: 1)();
TextColumn get operation => text()();
TextColumn get payloadHash => text().nullable()();
IntColumn get localRevision => integer()();
TextColumn get originDeviceId => text().withLength(min: 1)();
DateTimeColumn get createdAt => dateTime()();
DateTimeColumn get syncedAt => dateTime().nullable()();
@override
Set<Column> get primaryKey => {id};
@override
List<String> get customConstraints => const [
"CHECK (operation IN ('insert', 'update', 'softDelete', 'restore'))",
'CHECK (local_revision >= 0)',
];
}