merge(main): affichage des médias pendant l'exécution (ticket #36)

Fusionne feature/#36-affichage-medias-exercice — analyze propre,
67/67 tests verts, build APK debug validé.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-18 14:16:43 +02:00
15 changed files with 693 additions and 10 deletions

View File

@ -12,3 +12,4 @@
- [gametime-resume-plan-2026-07-18](gametime-resume-plan-2026-07-18.md) — memory note gametime-resume-plan-2026-07-18 - [gametime-resume-plan-2026-07-18](gametime-resume-plan-2026-07-18.md) — memory note gametime-resume-plan-2026-07-18
- [gametime-ux-exercise-default-targets](gametime-ux-exercise-default-targets.md) — memory note gametime-ux-exercise-default-targets - [gametime-ux-exercise-default-targets](gametime-ux-exercise-default-targets.md) — memory note gametime-ux-exercise-default-targets
- [gametime-ux-series-counter](gametime-ux-series-counter.md) — memory note gametime-ux-series-counter - [gametime-ux-series-counter](gametime-ux-series-counter.md) — memory note gametime-ux-series-counter
- [gametime-ux-exercise-media-viewer](gametime-ux-exercise-media-viewer.md) — memory note gametime-ux-exercise-media-viewer

View File

@ -0,0 +1,24 @@
---
name: gametime-ux-exercise-media-viewer
description: memory note gametime-ux-exercise-media-viewer
metadata:
type: project
---
# GameTime — Affichage des médias pendant l'exercice (ticket #36, UX 2026-07-18)
Décision : pas d'affichage permanent des médias dans le flux principal. Bouton secondaire "Voir médias" (OutlinedButton.icon, icône `Icons.perm_media_outlined`) proche du nom de l'exercice actif, affiché UNIQUEMENT si l'exercice a au moins 1 image ou 1 vidéo (sinon pas de bouton du tout, ni désactivé).
## Vue médias
Bottom sheet plein écran, titre "Médias de l'exercice" + nom. Si images ET vidéo : tabs/segmented "Images"/"Vidéo". Images : galerie swipe horizontal avec indicateur "1/5". Vidéo : lecteur avec contrôles natifs. Icône Fermer, pas d'action de modification depuis l'exécution.
## Écran Repos
Sous "Ensuite : <exercice>", afficher aussi "Voir médias" si le prochain exercice en a — bon moment pour consulter les consignes sans gêner la série active.
## Comportement séance
Ouvrir les médias NE met PAS la séance en pause ; le repos continue en arrière-plan. Bandeau "Repos en cours · MM:SS" dans la sheet si repos actif ; devient "Repos terminé" avec action "Reprendre la séance" s'il se termine pendant la consultation.
## Style Court Blazer
Sheet fond surface, liseré supérieur crimson 2px sur le conteneur média, icônes/compteur en or, rayon 6px, pas d'ombre lourde — le média reste une aide contextuelle, pas l'élément dominant.
## Impact technique important signalé par UX
Aujourd'hui seul `exerciseImageMediaIdSnapshot` (singulier) est propagé dans le snapshot de séance/exécution. Il faut l'étendre pour porter la galerie complète (`imageMediaIdsSnapshot`, jusqu'à 5, ordonnée) + `videoMediaIdSnapshot`, cohérent avec la galerie ajoutée au ticket #35.

View File

@ -1,6 +1,11 @@
--- ---
issueRef: "#36" issueRef: "#36"
version: 6 version: 9
updatedBy: {"kind":"user"} updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
updatedAt: 1784329810425 updatedAt: 1784376975122
--- ---
Décision UX consignée dans la mémoire "gametime-ux-exercise-media-viewer" — lis-la en entier avant d'implémenter.
Découpage :
1. [DevBackend] Étendre le snapshot d'exécution pour porter la galerie complète d'images (imageMediaIdsSnapshot, jusqu'à 5, ordonnée, cohérente avec le ticket #35) + videoMediaIdSnapshot, au lieu du seul exerciseImageMediaIdSnapshot actuel. Propager jusqu'à ActiveWorkoutSession/l'écran d'exécution.
2. [DevFrontend] Bouton "Voir médias" (affiché seulement si médias présents) sur l'écran d'exécution actif ET sur l'écran Repos (pour le prochain exercice). Bottom sheet plein écran avec galerie d'images (swipe + indicateur X/Y) et lecteur vidéo si présente. N'affecte pas l'état pause/repos de la séance.

View File

@ -2,15 +2,15 @@
id: "1d0445c0-c8f7-4527-b948-42d894a9e265" id: "1d0445c0-c8f7-4527-b948-42d894a9e265"
number: 36 number: 36
title: "Afficher les images et les vidéos" title: "Afficher les images et les vidéos"
status: "open" status: "closed"
priority: "medium" priority: "medium"
sprint: null sprint: null
links: [{"target":"#35","kind":"dependsOn"}] links: [{"target":"#35","kind":"dependsOn"}]
agentRefs: [{"agentId":"57695b92-24d0-4876-837c-76116e70a6ae","role":"assigned"}] agentRefs: [{"agentId":"57695b92-24d0-4876-837c-76116e70a6ae","role":"assigned"}]
createdBy: {"kind":"user"} createdBy: {"kind":"user"}
updatedBy: {"kind":"user"} updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
createdAt: 1784329666884 createdAt: 1784329666884
updatedAt: 1784329810425 updatedAt: 1784376975122
version: 6 version: 9
--- ---
Pour chaque exercice on a la possibilité d'upload des images et une vidéo. J'iamerais que ces images et cette vidéo, s'il y en a, soit affichés pendant l'exercie, ou au moins qu'on ai la possibilité de les voir. Ces images et cette vidéo peuvent être dans le but de présenter et d'expliquer l'exercice, ils sont donc importants. Laisser UX décider de la façon de les afficher pour que ça ne surchage pas non plus l'interface et que ça reste dans la DA Pour chaque exercice on a la possibilité d'upload des images et une vidéo. J'iamerais que ces images et cette vidéo, s'il y en a, soit affichés pendant l'exercie, ou au moins qu'on ai la possibilité de les voir. Ces images et cette vidéo peuvent être dans le but de présenter et d'expliquer l'exercice, ils sont donc importants. Laisser UX décider de la façon de les afficher pour que ça ne surchage pas non plus l'interface et que ça reste dans la DA

View File

@ -405,13 +405,13 @@
"issueRef": "#36", "issueRef": "#36",
"path": "36", "path": "36",
"title": "Afficher les images et les vidéos", "title": "Afficher les images et les vidéos",
"status": "open", "status": "closed",
"priority": "medium", "priority": "medium",
"sprint": null, "sprint": null,
"assignedAgentIds": [ "assignedAgentIds": [
"57695b92-24d0-4876-837c-76116e70a6ae" "57695b92-24d0-4876-837c-76116e70a6ae"
], ],
"updatedAt": 1784329810425 "updatedAt": 1784376975122
}, },
{ {
"issueRef": "#37", "issueRef": "#37",

View File

@ -439,6 +439,7 @@ final class ProgramUseCases {
exerciseNameSnapshot: input.exerciseNameSnapshot, exerciseNameSnapshot: input.exerciseNameSnapshot,
exerciseDescriptionSnapshot: input.exerciseDescriptionSnapshot, exerciseDescriptionSnapshot: input.exerciseDescriptionSnapshot,
exerciseImageMediaIdSnapshot: input.exerciseImageMediaIdSnapshot, exerciseImageMediaIdSnapshot: input.exerciseImageMediaIdSnapshot,
exerciseImageMediaIdsSnapshot: input.exerciseImageMediaIdsSnapshot,
exerciseVideoMediaIdSnapshot: input.exerciseVideoMediaIdSnapshot, exerciseVideoMediaIdSnapshot: input.exerciseVideoMediaIdSnapshot,
exerciseArchivedSnapshot: input.exerciseArchivedSnapshot, exerciseArchivedSnapshot: input.exerciseArchivedSnapshot,
availableTimeSnapshot: input.availableTimeSnapshot, availableTimeSnapshot: input.availableTimeSnapshot,
@ -555,6 +556,7 @@ final class ProgramExerciseConfig {
required this.exerciseNameSnapshot, required this.exerciseNameSnapshot,
this.exerciseDescriptionSnapshot, this.exerciseDescriptionSnapshot,
this.exerciseImageMediaIdSnapshot, this.exerciseImageMediaIdSnapshot,
this.exerciseImageMediaIdsSnapshot = const [],
this.exerciseVideoMediaIdSnapshot, this.exerciseVideoMediaIdSnapshot,
this.exerciseArchivedSnapshot = false, this.exerciseArchivedSnapshot = false,
required this.availableTimeSnapshot, required this.availableTimeSnapshot,
@ -577,6 +579,7 @@ final class ProgramExerciseConfig {
final String exerciseNameSnapshot; final String exerciseNameSnapshot;
final String? exerciseDescriptionSnapshot; final String? exerciseDescriptionSnapshot;
final String? exerciseImageMediaIdSnapshot; final String? exerciseImageMediaIdSnapshot;
final List<String> exerciseImageMediaIdsSnapshot;
final String? exerciseVideoMediaIdSnapshot; final String? exerciseVideoMediaIdSnapshot;
final bool exerciseArchivedSnapshot; final bool exerciseArchivedSnapshot;
final bool availableTimeSnapshot; final bool availableTimeSnapshot;
@ -1501,6 +1504,8 @@ List<ProgramExercise> _repositionProgramExercises(
exercises[index].exerciseDescriptionSnapshot, exercises[index].exerciseDescriptionSnapshot,
exerciseImageMediaIdSnapshot: exerciseImageMediaIdSnapshot:
exercises[index].exerciseImageMediaIdSnapshot, exercises[index].exerciseImageMediaIdSnapshot,
exerciseImageMediaIdsSnapshot:
exercises[index].exerciseImageMediaIdsSnapshot,
exerciseVideoMediaIdSnapshot: exerciseVideoMediaIdSnapshot:
exercises[index].exerciseVideoMediaIdSnapshot, exercises[index].exerciseVideoMediaIdSnapshot,
exerciseArchivedSnapshot: exercises[index].exerciseArchivedSnapshot, exerciseArchivedSnapshot: exercises[index].exerciseArchivedSnapshot,

View File

@ -303,6 +303,7 @@ final class ProgramExercise {
required String exerciseNameSnapshot, required String exerciseNameSnapshot,
this.exerciseDescriptionSnapshot, this.exerciseDescriptionSnapshot,
this.exerciseImageMediaIdSnapshot, this.exerciseImageMediaIdSnapshot,
List<String> exerciseImageMediaIdsSnapshot = const [],
this.exerciseVideoMediaIdSnapshot, this.exerciseVideoMediaIdSnapshot,
this.exerciseArchivedSnapshot = false, this.exerciseArchivedSnapshot = false,
required this.availableTimeSnapshot, required this.availableTimeSnapshot,
@ -323,6 +324,12 @@ final class ProgramExercise {
}) : exerciseNameSnapshot = _nonBlank( }) : exerciseNameSnapshot = _nonBlank(
exerciseNameSnapshot, exerciseNameSnapshot,
'Exercise snapshot name', 'Exercise snapshot name',
),
exerciseImageMediaIdsSnapshot = _validatedImageMediaIds(
exerciseImageMediaIdsSnapshot.isEmpty &&
exerciseImageMediaIdSnapshot != null
? [exerciseImageMediaIdSnapshot]
: exerciseImageMediaIdsSnapshot,
) { ) {
_requireNonNegative(position, 'Position'); _requireNonNegative(position, 'Position');
_requirePositive(setsCount, 'Sets count'); _requirePositive(setsCount, 'Sets count');
@ -360,6 +367,7 @@ final class ProgramExercise {
final String exerciseNameSnapshot; final String exerciseNameSnapshot;
final String? exerciseDescriptionSnapshot; final String? exerciseDescriptionSnapshot;
final String? exerciseImageMediaIdSnapshot; final String? exerciseImageMediaIdSnapshot;
final List<String> exerciseImageMediaIdsSnapshot;
final String? exerciseVideoMediaIdSnapshot; final String? exerciseVideoMediaIdSnapshot;
final bool exerciseArchivedSnapshot; final bool exerciseArchivedSnapshot;
final bool availableTimeSnapshot; final bool availableTimeSnapshot;
@ -405,6 +413,7 @@ final class ProgramExercise {
exerciseNameSnapshot: exercise.name, exerciseNameSnapshot: exercise.name,
exerciseDescriptionSnapshot: exercise.description, exerciseDescriptionSnapshot: exercise.description,
exerciseImageMediaIdSnapshot: exercise.imageMediaId, exerciseImageMediaIdSnapshot: exercise.imageMediaId,
exerciseImageMediaIdsSnapshot: exercise.imageMediaIds,
exerciseVideoMediaIdSnapshot: exercise.videoMediaId, exerciseVideoMediaIdSnapshot: exercise.videoMediaId,
exerciseArchivedSnapshot: exercise.archivedAt != null, exerciseArchivedSnapshot: exercise.archivedAt != null,
availableTimeSnapshot: exercise.hasTimeMeasure, availableTimeSnapshot: exercise.hasTimeMeasure,
@ -432,6 +441,8 @@ final class ProgramExercise {
'exerciseNameSnapshot': exerciseNameSnapshot, 'exerciseNameSnapshot': exerciseNameSnapshot,
'exerciseDescriptionSnapshot': exerciseDescriptionSnapshot, 'exerciseDescriptionSnapshot': exerciseDescriptionSnapshot,
'exerciseImageMediaIdSnapshot': exerciseImageMediaIdSnapshot, 'exerciseImageMediaIdSnapshot': exerciseImageMediaIdSnapshot,
'exerciseImageMediaIdsSnapshot': exerciseImageMediaIdsSnapshot,
'imageMediaIdsSnapshot': exerciseImageMediaIdsSnapshot,
'exerciseVideoMediaIdSnapshot': exerciseVideoMediaIdSnapshot, 'exerciseVideoMediaIdSnapshot': exerciseVideoMediaIdSnapshot,
'exerciseArchivedSnapshot': exerciseArchivedSnapshot, 'exerciseArchivedSnapshot': exerciseArchivedSnapshot,
'availableTimeSnapshot': availableTimeSnapshot, 'availableTimeSnapshot': availableTimeSnapshot,

View File

@ -37,7 +37,7 @@ final class AppDatabase extends _$AppDatabase {
} }
@override @override
int get schemaVersion => 5; int get schemaVersion => 6;
@override @override
MigrationStrategy get migration { MigrationStrategy get migration {
@ -67,6 +67,9 @@ final class AppDatabase extends _$AppDatabase {
if (from < 5) { if (from < 5) {
await _migrateToSchema5(migrator); await _migrateToSchema5(migrator);
} }
if (from < 6) {
await _migrateToSchema6();
}
await _createIndexes(); await _createIndexes();
}, },
beforeOpen: (details) async { beforeOpen: (details) async {
@ -250,4 +253,11 @@ extension on AppDatabase {
'WHERE image_media_id IS NOT NULL', 'WHERE image_media_id IS NOT NULL',
); );
} }
Future<void> _migrateToSchema6() async {
await customStatement(
'ALTER TABLE program_exercises ADD COLUMN '
'exercise_image_media_ids_snapshot_json TEXT',
);
}
} }

View File

@ -11148,6 +11148,17 @@ class $ProgramExercisesTable extends ProgramExercises
'REFERENCES media_assets (id)', 'REFERENCES media_assets (id)',
), ),
); );
static const VerificationMeta _exerciseImageMediaIdsSnapshotJsonMeta =
const VerificationMeta('exerciseImageMediaIdsSnapshotJson');
@override
late final GeneratedColumn<String> exerciseImageMediaIdsSnapshotJson =
GeneratedColumn<String>(
'exercise_image_media_ids_snapshot_json',
aliasedName,
true,
type: DriftSqlType.string,
requiredDuringInsert: false,
);
static const VerificationMeta _exerciseVideoMediaIdSnapshotMeta = static const VerificationMeta _exerciseVideoMediaIdSnapshotMeta =
const VerificationMeta('exerciseVideoMediaIdSnapshot'); const VerificationMeta('exerciseVideoMediaIdSnapshot');
@override @override
@ -11380,6 +11391,7 @@ class $ProgramExercisesTable extends ProgramExercises
exerciseNameSnapshot, exerciseNameSnapshot,
exerciseDescriptionSnapshot, exerciseDescriptionSnapshot,
exerciseImageMediaIdSnapshot, exerciseImageMediaIdSnapshot,
exerciseImageMediaIdsSnapshotJson,
exerciseVideoMediaIdSnapshot, exerciseVideoMediaIdSnapshot,
exerciseArchivedSnapshot, exerciseArchivedSnapshot,
availableTimeSnapshot, availableTimeSnapshot,
@ -11557,6 +11569,15 @@ class $ProgramExercisesTable extends ProgramExercises
), ),
); );
} }
if (data.containsKey('exercise_image_media_ids_snapshot_json')) {
context.handle(
_exerciseImageMediaIdsSnapshotJsonMeta,
exerciseImageMediaIdsSnapshotJson.isAcceptableOrUnknown(
data['exercise_image_media_ids_snapshot_json']!,
_exerciseImageMediaIdsSnapshotJsonMeta,
),
);
}
if (data.containsKey('exercise_video_media_id_snapshot')) { if (data.containsKey('exercise_video_media_id_snapshot')) {
context.handle( context.handle(
_exerciseVideoMediaIdSnapshotMeta, _exerciseVideoMediaIdSnapshotMeta,
@ -11795,6 +11816,10 @@ class $ProgramExercisesTable extends ProgramExercises
DriftSqlType.string, DriftSqlType.string,
data['${effectivePrefix}exercise_image_media_id_snapshot'], data['${effectivePrefix}exercise_image_media_id_snapshot'],
), ),
exerciseImageMediaIdsSnapshotJson: attachedDatabase.typeMapping.read(
DriftSqlType.string,
data['${effectivePrefix}exercise_image_media_ids_snapshot_json'],
),
exerciseVideoMediaIdSnapshot: attachedDatabase.typeMapping.read( exerciseVideoMediaIdSnapshot: attachedDatabase.typeMapping.read(
DriftSqlType.string, DriftSqlType.string,
data['${effectivePrefix}exercise_video_media_id_snapshot'], data['${effectivePrefix}exercise_video_media_id_snapshot'],
@ -11890,6 +11915,7 @@ class ProgramExercise extends DataClass implements Insertable<ProgramExercise> {
final String exerciseNameSnapshot; final String exerciseNameSnapshot;
final String? exerciseDescriptionSnapshot; final String? exerciseDescriptionSnapshot;
final String? exerciseImageMediaIdSnapshot; final String? exerciseImageMediaIdSnapshot;
final String? exerciseImageMediaIdsSnapshotJson;
final String? exerciseVideoMediaIdSnapshot; final String? exerciseVideoMediaIdSnapshot;
final bool exerciseArchivedSnapshot; final bool exerciseArchivedSnapshot;
final bool availableTimeSnapshot; final bool availableTimeSnapshot;
@ -11925,6 +11951,7 @@ class ProgramExercise extends DataClass implements Insertable<ProgramExercise> {
required this.exerciseNameSnapshot, required this.exerciseNameSnapshot,
this.exerciseDescriptionSnapshot, this.exerciseDescriptionSnapshot,
this.exerciseImageMediaIdSnapshot, this.exerciseImageMediaIdSnapshot,
this.exerciseImageMediaIdsSnapshotJson,
this.exerciseVideoMediaIdSnapshot, this.exerciseVideoMediaIdSnapshot,
required this.exerciseArchivedSnapshot, required this.exerciseArchivedSnapshot,
required this.availableTimeSnapshot, required this.availableTimeSnapshot,
@ -11981,6 +12008,11 @@ class ProgramExercise extends DataClass implements Insertable<ProgramExercise> {
exerciseImageMediaIdSnapshot, exerciseImageMediaIdSnapshot,
); );
} }
if (!nullToAbsent || exerciseImageMediaIdsSnapshotJson != null) {
map['exercise_image_media_ids_snapshot_json'] = Variable<String>(
exerciseImageMediaIdsSnapshotJson,
);
}
if (!nullToAbsent || exerciseVideoMediaIdSnapshot != null) { if (!nullToAbsent || exerciseVideoMediaIdSnapshot != null) {
map['exercise_video_media_id_snapshot'] = Variable<String>( map['exercise_video_media_id_snapshot'] = Variable<String>(
exerciseVideoMediaIdSnapshot, exerciseVideoMediaIdSnapshot,
@ -12056,6 +12088,10 @@ class ProgramExercise extends DataClass implements Insertable<ProgramExercise> {
exerciseImageMediaIdSnapshot == null && nullToAbsent exerciseImageMediaIdSnapshot == null && nullToAbsent
? const Value.absent() ? const Value.absent()
: Value(exerciseImageMediaIdSnapshot), : Value(exerciseImageMediaIdSnapshot),
exerciseImageMediaIdsSnapshotJson:
exerciseImageMediaIdsSnapshotJson == null && nullToAbsent
? const Value.absent()
: Value(exerciseImageMediaIdsSnapshotJson),
exerciseVideoMediaIdSnapshot: exerciseVideoMediaIdSnapshot:
exerciseVideoMediaIdSnapshot == null && nullToAbsent exerciseVideoMediaIdSnapshot == null && nullToAbsent
? const Value.absent() ? const Value.absent()
@ -12124,6 +12160,9 @@ class ProgramExercise extends DataClass implements Insertable<ProgramExercise> {
exerciseImageMediaIdSnapshot: serializer.fromJson<String?>( exerciseImageMediaIdSnapshot: serializer.fromJson<String?>(
json['exerciseImageMediaIdSnapshot'], json['exerciseImageMediaIdSnapshot'],
), ),
exerciseImageMediaIdsSnapshotJson: serializer.fromJson<String?>(
json['exerciseImageMediaIdsSnapshotJson'],
),
exerciseVideoMediaIdSnapshot: serializer.fromJson<String?>( exerciseVideoMediaIdSnapshot: serializer.fromJson<String?>(
json['exerciseVideoMediaIdSnapshot'], json['exerciseVideoMediaIdSnapshot'],
), ),
@ -12186,6 +12225,9 @@ class ProgramExercise extends DataClass implements Insertable<ProgramExercise> {
'exerciseImageMediaIdSnapshot': serializer.toJson<String?>( 'exerciseImageMediaIdSnapshot': serializer.toJson<String?>(
exerciseImageMediaIdSnapshot, exerciseImageMediaIdSnapshot,
), ),
'exerciseImageMediaIdsSnapshotJson': serializer.toJson<String?>(
exerciseImageMediaIdsSnapshotJson,
),
'exerciseVideoMediaIdSnapshot': serializer.toJson<String?>( 'exerciseVideoMediaIdSnapshot': serializer.toJson<String?>(
exerciseVideoMediaIdSnapshot, exerciseVideoMediaIdSnapshot,
), ),
@ -12230,6 +12272,7 @@ class ProgramExercise extends DataClass implements Insertable<ProgramExercise> {
String? exerciseNameSnapshot, String? exerciseNameSnapshot,
Value<String?> exerciseDescriptionSnapshot = const Value.absent(), Value<String?> exerciseDescriptionSnapshot = const Value.absent(),
Value<String?> exerciseImageMediaIdSnapshot = const Value.absent(), Value<String?> exerciseImageMediaIdSnapshot = const Value.absent(),
Value<String?> exerciseImageMediaIdsSnapshotJson = const Value.absent(),
Value<String?> exerciseVideoMediaIdSnapshot = const Value.absent(), Value<String?> exerciseVideoMediaIdSnapshot = const Value.absent(),
bool? exerciseArchivedSnapshot, bool? exerciseArchivedSnapshot,
bool? availableTimeSnapshot, bool? availableTimeSnapshot,
@ -12275,6 +12318,9 @@ class ProgramExercise extends DataClass implements Insertable<ProgramExercise> {
exerciseImageMediaIdSnapshot: exerciseImageMediaIdSnapshot.present exerciseImageMediaIdSnapshot: exerciseImageMediaIdSnapshot.present
? exerciseImageMediaIdSnapshot.value ? exerciseImageMediaIdSnapshot.value
: this.exerciseImageMediaIdSnapshot, : this.exerciseImageMediaIdSnapshot,
exerciseImageMediaIdsSnapshotJson: exerciseImageMediaIdsSnapshotJson.present
? exerciseImageMediaIdsSnapshotJson.value
: this.exerciseImageMediaIdsSnapshotJson,
exerciseVideoMediaIdSnapshot: exerciseVideoMediaIdSnapshot.present exerciseVideoMediaIdSnapshot: exerciseVideoMediaIdSnapshot.present
? exerciseVideoMediaIdSnapshot.value ? exerciseVideoMediaIdSnapshot.value
: this.exerciseVideoMediaIdSnapshot, : this.exerciseVideoMediaIdSnapshot,
@ -12347,6 +12393,10 @@ class ProgramExercise extends DataClass implements Insertable<ProgramExercise> {
exerciseImageMediaIdSnapshot: data.exerciseImageMediaIdSnapshot.present exerciseImageMediaIdSnapshot: data.exerciseImageMediaIdSnapshot.present
? data.exerciseImageMediaIdSnapshot.value ? data.exerciseImageMediaIdSnapshot.value
: this.exerciseImageMediaIdSnapshot, : this.exerciseImageMediaIdSnapshot,
exerciseImageMediaIdsSnapshotJson:
data.exerciseImageMediaIdsSnapshotJson.present
? data.exerciseImageMediaIdsSnapshotJson.value
: this.exerciseImageMediaIdsSnapshotJson,
exerciseVideoMediaIdSnapshot: data.exerciseVideoMediaIdSnapshot.present exerciseVideoMediaIdSnapshot: data.exerciseVideoMediaIdSnapshot.present
? data.exerciseVideoMediaIdSnapshot.value ? data.exerciseVideoMediaIdSnapshot.value
: this.exerciseVideoMediaIdSnapshot, : this.exerciseVideoMediaIdSnapshot,
@ -12421,6 +12471,9 @@ class ProgramExercise extends DataClass implements Insertable<ProgramExercise> {
..write( ..write(
'exerciseImageMediaIdSnapshot: $exerciseImageMediaIdSnapshot, ', 'exerciseImageMediaIdSnapshot: $exerciseImageMediaIdSnapshot, ',
) )
..write(
'exerciseImageMediaIdsSnapshotJson: $exerciseImageMediaIdsSnapshotJson, ',
)
..write( ..write(
'exerciseVideoMediaIdSnapshot: $exerciseVideoMediaIdSnapshot, ', 'exerciseVideoMediaIdSnapshot: $exerciseVideoMediaIdSnapshot, ',
) )
@ -12463,6 +12516,7 @@ class ProgramExercise extends DataClass implements Insertable<ProgramExercise> {
exerciseNameSnapshot, exerciseNameSnapshot,
exerciseDescriptionSnapshot, exerciseDescriptionSnapshot,
exerciseImageMediaIdSnapshot, exerciseImageMediaIdSnapshot,
exerciseImageMediaIdsSnapshotJson,
exerciseVideoMediaIdSnapshot, exerciseVideoMediaIdSnapshot,
exerciseArchivedSnapshot, exerciseArchivedSnapshot,
availableTimeSnapshot, availableTimeSnapshot,
@ -12504,6 +12558,8 @@ class ProgramExercise extends DataClass implements Insertable<ProgramExercise> {
this.exerciseDescriptionSnapshot && this.exerciseDescriptionSnapshot &&
other.exerciseImageMediaIdSnapshot == other.exerciseImageMediaIdSnapshot ==
this.exerciseImageMediaIdSnapshot && this.exerciseImageMediaIdSnapshot &&
other.exerciseImageMediaIdsSnapshotJson ==
this.exerciseImageMediaIdsSnapshotJson &&
other.exerciseVideoMediaIdSnapshot == other.exerciseVideoMediaIdSnapshot ==
this.exerciseVideoMediaIdSnapshot && this.exerciseVideoMediaIdSnapshot &&
other.exerciseArchivedSnapshot == this.exerciseArchivedSnapshot && other.exerciseArchivedSnapshot == this.exerciseArchivedSnapshot &&
@ -12542,6 +12598,7 @@ class ProgramExercisesCompanion extends UpdateCompanion<ProgramExercise> {
final Value<String> exerciseNameSnapshot; final Value<String> exerciseNameSnapshot;
final Value<String?> exerciseDescriptionSnapshot; final Value<String?> exerciseDescriptionSnapshot;
final Value<String?> exerciseImageMediaIdSnapshot; final Value<String?> exerciseImageMediaIdSnapshot;
final Value<String?> exerciseImageMediaIdsSnapshotJson;
final Value<String?> exerciseVideoMediaIdSnapshot; final Value<String?> exerciseVideoMediaIdSnapshot;
final Value<bool> exerciseArchivedSnapshot; final Value<bool> exerciseArchivedSnapshot;
final Value<bool> availableTimeSnapshot; final Value<bool> availableTimeSnapshot;
@ -12578,6 +12635,7 @@ class ProgramExercisesCompanion extends UpdateCompanion<ProgramExercise> {
this.exerciseNameSnapshot = const Value.absent(), this.exerciseNameSnapshot = const Value.absent(),
this.exerciseDescriptionSnapshot = const Value.absent(), this.exerciseDescriptionSnapshot = const Value.absent(),
this.exerciseImageMediaIdSnapshot = const Value.absent(), this.exerciseImageMediaIdSnapshot = const Value.absent(),
this.exerciseImageMediaIdsSnapshotJson = const Value.absent(),
this.exerciseVideoMediaIdSnapshot = const Value.absent(), this.exerciseVideoMediaIdSnapshot = const Value.absent(),
this.exerciseArchivedSnapshot = const Value.absent(), this.exerciseArchivedSnapshot = const Value.absent(),
this.availableTimeSnapshot = const Value.absent(), this.availableTimeSnapshot = const Value.absent(),
@ -12615,6 +12673,7 @@ class ProgramExercisesCompanion extends UpdateCompanion<ProgramExercise> {
required String exerciseNameSnapshot, required String exerciseNameSnapshot,
this.exerciseDescriptionSnapshot = const Value.absent(), this.exerciseDescriptionSnapshot = const Value.absent(),
this.exerciseImageMediaIdSnapshot = const Value.absent(), this.exerciseImageMediaIdSnapshot = const Value.absent(),
this.exerciseImageMediaIdsSnapshotJson = const Value.absent(),
this.exerciseVideoMediaIdSnapshot = const Value.absent(), this.exerciseVideoMediaIdSnapshot = const Value.absent(),
this.exerciseArchivedSnapshot = const Value.absent(), this.exerciseArchivedSnapshot = const Value.absent(),
required bool availableTimeSnapshot, required bool availableTimeSnapshot,
@ -12667,6 +12726,7 @@ class ProgramExercisesCompanion extends UpdateCompanion<ProgramExercise> {
Expression<String>? exerciseNameSnapshot, Expression<String>? exerciseNameSnapshot,
Expression<String>? exerciseDescriptionSnapshot, Expression<String>? exerciseDescriptionSnapshot,
Expression<String>? exerciseImageMediaIdSnapshot, Expression<String>? exerciseImageMediaIdSnapshot,
Expression<String>? exerciseImageMediaIdsSnapshotJson,
Expression<String>? exerciseVideoMediaIdSnapshot, Expression<String>? exerciseVideoMediaIdSnapshot,
Expression<bool>? exerciseArchivedSnapshot, Expression<bool>? exerciseArchivedSnapshot,
Expression<bool>? availableTimeSnapshot, Expression<bool>? availableTimeSnapshot,
@ -12708,6 +12768,9 @@ class ProgramExercisesCompanion extends UpdateCompanion<ProgramExercise> {
'exercise_description_snapshot': exerciseDescriptionSnapshot, 'exercise_description_snapshot': exerciseDescriptionSnapshot,
if (exerciseImageMediaIdSnapshot != null) if (exerciseImageMediaIdSnapshot != null)
'exercise_image_media_id_snapshot': exerciseImageMediaIdSnapshot, 'exercise_image_media_id_snapshot': exerciseImageMediaIdSnapshot,
if (exerciseImageMediaIdsSnapshotJson != null)
'exercise_image_media_ids_snapshot_json':
exerciseImageMediaIdsSnapshotJson,
if (exerciseVideoMediaIdSnapshot != null) if (exerciseVideoMediaIdSnapshot != null)
'exercise_video_media_id_snapshot': exerciseVideoMediaIdSnapshot, 'exercise_video_media_id_snapshot': exerciseVideoMediaIdSnapshot,
if (exerciseArchivedSnapshot != null) if (exerciseArchivedSnapshot != null)
@ -12755,6 +12818,7 @@ class ProgramExercisesCompanion extends UpdateCompanion<ProgramExercise> {
Value<String>? exerciseNameSnapshot, Value<String>? exerciseNameSnapshot,
Value<String?>? exerciseDescriptionSnapshot, Value<String?>? exerciseDescriptionSnapshot,
Value<String?>? exerciseImageMediaIdSnapshot, Value<String?>? exerciseImageMediaIdSnapshot,
Value<String?>? exerciseImageMediaIdsSnapshotJson,
Value<String?>? exerciseVideoMediaIdSnapshot, Value<String?>? exerciseVideoMediaIdSnapshot,
Value<bool>? exerciseArchivedSnapshot, Value<bool>? exerciseArchivedSnapshot,
Value<bool>? availableTimeSnapshot, Value<bool>? availableTimeSnapshot,
@ -12794,6 +12858,9 @@ class ProgramExercisesCompanion extends UpdateCompanion<ProgramExercise> {
exerciseDescriptionSnapshot ?? this.exerciseDescriptionSnapshot, exerciseDescriptionSnapshot ?? this.exerciseDescriptionSnapshot,
exerciseImageMediaIdSnapshot: exerciseImageMediaIdSnapshot:
exerciseImageMediaIdSnapshot ?? this.exerciseImageMediaIdSnapshot, exerciseImageMediaIdSnapshot ?? this.exerciseImageMediaIdSnapshot,
exerciseImageMediaIdsSnapshotJson:
exerciseImageMediaIdsSnapshotJson ??
this.exerciseImageMediaIdsSnapshotJson,
exerciseVideoMediaIdSnapshot: exerciseVideoMediaIdSnapshot:
exerciseVideoMediaIdSnapshot ?? this.exerciseVideoMediaIdSnapshot, exerciseVideoMediaIdSnapshot ?? this.exerciseVideoMediaIdSnapshot,
exerciseArchivedSnapshot: exerciseArchivedSnapshot:
@ -12883,6 +12950,11 @@ class ProgramExercisesCompanion extends UpdateCompanion<ProgramExercise> {
exerciseImageMediaIdSnapshot.value, exerciseImageMediaIdSnapshot.value,
); );
} }
if (exerciseImageMediaIdsSnapshotJson.present) {
map['exercise_image_media_ids_snapshot_json'] = Variable<String>(
exerciseImageMediaIdsSnapshotJson.value,
);
}
if (exerciseVideoMediaIdSnapshot.present) { if (exerciseVideoMediaIdSnapshot.present) {
map['exercise_video_media_id_snapshot'] = Variable<String>( map['exercise_video_media_id_snapshot'] = Variable<String>(
exerciseVideoMediaIdSnapshot.value, exerciseVideoMediaIdSnapshot.value,
@ -12974,6 +13046,9 @@ class ProgramExercisesCompanion extends UpdateCompanion<ProgramExercise> {
..write( ..write(
'exerciseImageMediaIdSnapshot: $exerciseImageMediaIdSnapshot, ', 'exerciseImageMediaIdSnapshot: $exerciseImageMediaIdSnapshot, ',
) )
..write(
'exerciseImageMediaIdsSnapshotJson: $exerciseImageMediaIdsSnapshotJson, ',
)
..write( ..write(
'exerciseVideoMediaIdSnapshot: $exerciseVideoMediaIdSnapshot, ', 'exerciseVideoMediaIdSnapshot: $exerciseVideoMediaIdSnapshot, ',
) )
@ -25793,6 +25868,7 @@ typedef $$ProgramExercisesTableCreateCompanionBuilder =
required String exerciseNameSnapshot, required String exerciseNameSnapshot,
Value<String?> exerciseDescriptionSnapshot, Value<String?> exerciseDescriptionSnapshot,
Value<String?> exerciseImageMediaIdSnapshot, Value<String?> exerciseImageMediaIdSnapshot,
Value<String?> exerciseImageMediaIdsSnapshotJson,
Value<String?> exerciseVideoMediaIdSnapshot, Value<String?> exerciseVideoMediaIdSnapshot,
Value<bool> exerciseArchivedSnapshot, Value<bool> exerciseArchivedSnapshot,
required bool availableTimeSnapshot, required bool availableTimeSnapshot,
@ -25831,6 +25907,7 @@ typedef $$ProgramExercisesTableUpdateCompanionBuilder =
Value<String> exerciseNameSnapshot, Value<String> exerciseNameSnapshot,
Value<String?> exerciseDescriptionSnapshot, Value<String?> exerciseDescriptionSnapshot,
Value<String?> exerciseImageMediaIdSnapshot, Value<String?> exerciseImageMediaIdSnapshot,
Value<String?> exerciseImageMediaIdsSnapshotJson,
Value<String?> exerciseVideoMediaIdSnapshot, Value<String?> exerciseVideoMediaIdSnapshot,
Value<bool> exerciseArchivedSnapshot, Value<bool> exerciseArchivedSnapshot,
Value<bool> availableTimeSnapshot, Value<bool> availableTimeSnapshot,
@ -26019,6 +26096,12 @@ class $$ProgramExercisesTableFilterComposer
builder: (column) => ColumnFilters(column), builder: (column) => ColumnFilters(column),
); );
ColumnFilters<String> get exerciseImageMediaIdsSnapshotJson =>
$composableBuilder(
column: $table.exerciseImageMediaIdsSnapshotJson,
builder: (column) => ColumnFilters(column),
);
ColumnFilters<bool> get exerciseArchivedSnapshot => $composableBuilder( ColumnFilters<bool> get exerciseArchivedSnapshot => $composableBuilder(
column: $table.exerciseArchivedSnapshot, column: $table.exerciseArchivedSnapshot,
builder: (column) => ColumnFilters(column), builder: (column) => ColumnFilters(column),
@ -26271,6 +26354,12 @@ class $$ProgramExercisesTableOrderingComposer
builder: (column) => ColumnOrderings(column), builder: (column) => ColumnOrderings(column),
); );
ColumnOrderings<String> get exerciseImageMediaIdsSnapshotJson =>
$composableBuilder(
column: $table.exerciseImageMediaIdsSnapshotJson,
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<bool> get exerciseArchivedSnapshot => $composableBuilder( ColumnOrderings<bool> get exerciseArchivedSnapshot => $composableBuilder(
column: $table.exerciseArchivedSnapshot, column: $table.exerciseArchivedSnapshot,
builder: (column) => ColumnOrderings(column), builder: (column) => ColumnOrderings(column),
@ -26511,6 +26600,12 @@ class $$ProgramExercisesTableAnnotationComposer
builder: (column) => column, builder: (column) => column,
); );
GeneratedColumn<String> get exerciseImageMediaIdsSnapshotJson =>
$composableBuilder(
column: $table.exerciseImageMediaIdsSnapshotJson,
builder: (column) => column,
);
GeneratedColumn<bool> get exerciseArchivedSnapshot => $composableBuilder( GeneratedColumn<bool> get exerciseArchivedSnapshot => $composableBuilder(
column: $table.exerciseArchivedSnapshot, column: $table.exerciseArchivedSnapshot,
builder: (column) => column, builder: (column) => column,
@ -26736,6 +26831,8 @@ class $$ProgramExercisesTableTableManager
const Value.absent(), const Value.absent(),
Value<String?> exerciseImageMediaIdSnapshot = Value<String?> exerciseImageMediaIdSnapshot =
const Value.absent(), const Value.absent(),
Value<String?> exerciseImageMediaIdsSnapshotJson =
const Value.absent(),
Value<String?> exerciseVideoMediaIdSnapshot = Value<String?> exerciseVideoMediaIdSnapshot =
const Value.absent(), const Value.absent(),
Value<bool> exerciseArchivedSnapshot = const Value.absent(), Value<bool> exerciseArchivedSnapshot = const Value.absent(),
@ -26773,6 +26870,8 @@ class $$ProgramExercisesTableTableManager
exerciseNameSnapshot: exerciseNameSnapshot, exerciseNameSnapshot: exerciseNameSnapshot,
exerciseDescriptionSnapshot: exerciseDescriptionSnapshot, exerciseDescriptionSnapshot: exerciseDescriptionSnapshot,
exerciseImageMediaIdSnapshot: exerciseImageMediaIdSnapshot, exerciseImageMediaIdSnapshot: exerciseImageMediaIdSnapshot,
exerciseImageMediaIdsSnapshotJson:
exerciseImageMediaIdsSnapshotJson,
exerciseVideoMediaIdSnapshot: exerciseVideoMediaIdSnapshot, exerciseVideoMediaIdSnapshot: exerciseVideoMediaIdSnapshot,
exerciseArchivedSnapshot: exerciseArchivedSnapshot, exerciseArchivedSnapshot: exerciseArchivedSnapshot,
availableTimeSnapshot: availableTimeSnapshot, availableTimeSnapshot: availableTimeSnapshot,
@ -26813,6 +26912,8 @@ class $$ProgramExercisesTableTableManager
const Value.absent(), const Value.absent(),
Value<String?> exerciseImageMediaIdSnapshot = Value<String?> exerciseImageMediaIdSnapshot =
const Value.absent(), const Value.absent(),
Value<String?> exerciseImageMediaIdsSnapshotJson =
const Value.absent(),
Value<String?> exerciseVideoMediaIdSnapshot = Value<String?> exerciseVideoMediaIdSnapshot =
const Value.absent(), const Value.absent(),
Value<bool> exerciseArchivedSnapshot = const Value.absent(), Value<bool> exerciseArchivedSnapshot = const Value.absent(),
@ -26850,6 +26951,8 @@ class $$ProgramExercisesTableTableManager
exerciseNameSnapshot: exerciseNameSnapshot, exerciseNameSnapshot: exerciseNameSnapshot,
exerciseDescriptionSnapshot: exerciseDescriptionSnapshot, exerciseDescriptionSnapshot: exerciseDescriptionSnapshot,
exerciseImageMediaIdSnapshot: exerciseImageMediaIdSnapshot, exerciseImageMediaIdSnapshot: exerciseImageMediaIdSnapshot,
exerciseImageMediaIdsSnapshotJson:
exerciseImageMediaIdsSnapshotJson,
exerciseVideoMediaIdSnapshot: exerciseVideoMediaIdSnapshot, exerciseVideoMediaIdSnapshot: exerciseVideoMediaIdSnapshot,
exerciseArchivedSnapshot: exerciseArchivedSnapshot, exerciseArchivedSnapshot: exerciseArchivedSnapshot,
availableTimeSnapshot: availableTimeSnapshot, availableTimeSnapshot: availableTimeSnapshot,

View File

@ -1,3 +1,5 @@
import 'dart:convert';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import '../../application/application.dart'; import '../../application/application.dart';
@ -1208,6 +1210,9 @@ db.ProgramExercisesCompanion _programExerciseCompanion(
exerciseNameSnapshot: Value(exercise.exerciseNameSnapshot), exerciseNameSnapshot: Value(exercise.exerciseNameSnapshot),
exerciseDescriptionSnapshot: Value(exercise.exerciseDescriptionSnapshot), exerciseDescriptionSnapshot: Value(exercise.exerciseDescriptionSnapshot),
exerciseImageMediaIdSnapshot: Value(exercise.exerciseImageMediaIdSnapshot), exerciseImageMediaIdSnapshot: Value(exercise.exerciseImageMediaIdSnapshot),
exerciseImageMediaIdsSnapshotJson: Value(
_encodeImageMediaIdsSnapshot(exercise.exerciseImageMediaIdsSnapshot),
),
exerciseVideoMediaIdSnapshot: Value(exercise.exerciseVideoMediaIdSnapshot), exerciseVideoMediaIdSnapshot: Value(exercise.exerciseVideoMediaIdSnapshot),
exerciseArchivedSnapshot: Value(exercise.exerciseArchivedSnapshot), exerciseArchivedSnapshot: Value(exercise.exerciseArchivedSnapshot),
availableTimeSnapshot: Value(exercise.availableTimeSnapshot), availableTimeSnapshot: Value(exercise.availableTimeSnapshot),
@ -1239,6 +1244,10 @@ domain.ProgramExercise _programExerciseFromRow(db.ProgramExercise row) {
exerciseNameSnapshot: row.exerciseNameSnapshot, exerciseNameSnapshot: row.exerciseNameSnapshot,
exerciseDescriptionSnapshot: row.exerciseDescriptionSnapshot, exerciseDescriptionSnapshot: row.exerciseDescriptionSnapshot,
exerciseImageMediaIdSnapshot: row.exerciseImageMediaIdSnapshot, exerciseImageMediaIdSnapshot: row.exerciseImageMediaIdSnapshot,
exerciseImageMediaIdsSnapshot: _decodeImageMediaIdsSnapshot(
row.exerciseImageMediaIdsSnapshotJson,
row.exerciseImageMediaIdSnapshot,
),
exerciseVideoMediaIdSnapshot: row.exerciseVideoMediaIdSnapshot, exerciseVideoMediaIdSnapshot: row.exerciseVideoMediaIdSnapshot,
exerciseArchivedSnapshot: row.exerciseArchivedSnapshot, exerciseArchivedSnapshot: row.exerciseArchivedSnapshot,
availableTimeSnapshot: row.availableTimeSnapshot, availableTimeSnapshot: row.availableTimeSnapshot,
@ -1766,6 +1775,29 @@ domain.ActiveScoreStopwatchStatus _scoreStopwatchStatusFromDb(String value) =>
), ),
}; };
String? _encodeImageMediaIdsSnapshot(List<String> imageMediaIds) {
return imageMediaIds.isEmpty ? null : jsonEncode(imageMediaIds);
}
List<String> _decodeImageMediaIdsSnapshot(
String? encoded,
String? fallbackImageMediaId,
) {
if (encoded == null || encoded.trim().isEmpty) {
return [if (fallbackImageMediaId != null) fallbackImageMediaId];
}
final decoded = jsonDecode(encoded);
if (decoded is! List) {
throw const domain.DomainException(
'Invalid exercise image media ids snapshot.',
);
}
return decoded
.whereType<String>()
.where((id) => id.trim().isNotEmpty)
.toList(growable: false);
}
domain.ActiveWorkoutStatus _activeStatusFromDb(String value) => switch (value) { domain.ActiveWorkoutStatus _activeStatusFromDb(String value) => switch (value) {
'running' => domain.ActiveWorkoutStatus.running, 'running' => domain.ActiveWorkoutStatus.running,
'paused' => domain.ActiveWorkoutStatus.paused, 'paused' => domain.ActiveWorkoutStatus.paused,

View File

@ -128,6 +128,7 @@ class ProgramExercises extends SyncableTable {
@ReferenceName('programExerciseImageSnapshotReferences') @ReferenceName('programExerciseImageSnapshotReferences')
TextColumn get exerciseImageMediaIdSnapshot => TextColumn get exerciseImageMediaIdSnapshot =>
text().nullable().references(MediaAssets, #id)(); text().nullable().references(MediaAssets, #id)();
TextColumn get exerciseImageMediaIdsSnapshotJson => text().nullable()();
@ReferenceName('programExerciseVideoSnapshotReferences') @ReferenceName('programExerciseVideoSnapshotReferences')
TextColumn get exerciseVideoMediaIdSnapshot => TextColumn get exerciseVideoMediaIdSnapshot =>

View File

@ -870,6 +870,7 @@ final class _ProgramExerciseDraft {
required this.exerciseNameSnapshot, required this.exerciseNameSnapshot,
this.exerciseDescriptionSnapshot, this.exerciseDescriptionSnapshot,
this.exerciseImageMediaIdSnapshot, this.exerciseImageMediaIdSnapshot,
this.exerciseImageMediaIdsSnapshot = const [],
this.exerciseVideoMediaIdSnapshot, this.exerciseVideoMediaIdSnapshot,
this.exerciseArchivedSnapshot = false, this.exerciseArchivedSnapshot = false,
required this.availableTimeSnapshot, required this.availableTimeSnapshot,
@ -897,6 +898,7 @@ final class _ProgramExerciseDraft {
exerciseNameSnapshot: exercise.name, exerciseNameSnapshot: exercise.name,
exerciseDescriptionSnapshot: exercise.description, exerciseDescriptionSnapshot: exercise.description,
exerciseImageMediaIdSnapshot: exercise.imageMediaId, exerciseImageMediaIdSnapshot: exercise.imageMediaId,
exerciseImageMediaIdsSnapshot: exercise.imageMediaIds,
exerciseVideoMediaIdSnapshot: exercise.videoMediaId, exerciseVideoMediaIdSnapshot: exercise.videoMediaId,
exerciseArchivedSnapshot: exercise.archivedAt != null, exerciseArchivedSnapshot: exercise.archivedAt != null,
availableTimeSnapshot: exercise.hasTimeMeasure, availableTimeSnapshot: exercise.hasTimeMeasure,
@ -927,6 +929,7 @@ final class _ProgramExerciseDraft {
exerciseNameSnapshot: exercise.exerciseNameSnapshot, exerciseNameSnapshot: exercise.exerciseNameSnapshot,
exerciseDescriptionSnapshot: exercise.exerciseDescriptionSnapshot, exerciseDescriptionSnapshot: exercise.exerciseDescriptionSnapshot,
exerciseImageMediaIdSnapshot: exercise.exerciseImageMediaIdSnapshot, exerciseImageMediaIdSnapshot: exercise.exerciseImageMediaIdSnapshot,
exerciseImageMediaIdsSnapshot: exercise.exerciseImageMediaIdsSnapshot,
exerciseVideoMediaIdSnapshot: exercise.exerciseVideoMediaIdSnapshot, exerciseVideoMediaIdSnapshot: exercise.exerciseVideoMediaIdSnapshot,
exerciseArchivedSnapshot: exercise.exerciseArchivedSnapshot, exerciseArchivedSnapshot: exercise.exerciseArchivedSnapshot,
availableTimeSnapshot: exercise.availableTimeSnapshot, availableTimeSnapshot: exercise.availableTimeSnapshot,
@ -955,6 +958,7 @@ final class _ProgramExerciseDraft {
final String exerciseNameSnapshot; final String exerciseNameSnapshot;
final String? exerciseDescriptionSnapshot; final String? exerciseDescriptionSnapshot;
final String? exerciseImageMediaIdSnapshot; final String? exerciseImageMediaIdSnapshot;
final List<String> exerciseImageMediaIdsSnapshot;
final String? exerciseVideoMediaIdSnapshot; final String? exerciseVideoMediaIdSnapshot;
final bool exerciseArchivedSnapshot; final bool exerciseArchivedSnapshot;
final bool availableTimeSnapshot; final bool availableTimeSnapshot;
@ -979,6 +983,7 @@ final class _ProgramExerciseDraft {
exerciseNameSnapshot: exerciseNameSnapshot, exerciseNameSnapshot: exerciseNameSnapshot,
exerciseDescriptionSnapshot: exerciseDescriptionSnapshot, exerciseDescriptionSnapshot: exerciseDescriptionSnapshot,
exerciseImageMediaIdSnapshot: exerciseImageMediaIdSnapshot, exerciseImageMediaIdSnapshot: exerciseImageMediaIdSnapshot,
exerciseImageMediaIdsSnapshot: exerciseImageMediaIdsSnapshot,
exerciseVideoMediaIdSnapshot: exerciseVideoMediaIdSnapshot, exerciseVideoMediaIdSnapshot: exerciseVideoMediaIdSnapshot,
exerciseArchivedSnapshot: exerciseArchivedSnapshot, exerciseArchivedSnapshot: exerciseArchivedSnapshot,
availableTimeSnapshot: availableTimeSnapshot, availableTimeSnapshot: availableTimeSnapshot,
@ -1006,6 +1011,7 @@ final class _ProgramExerciseDraft {
exerciseNameSnapshot: exerciseNameSnapshot, exerciseNameSnapshot: exerciseNameSnapshot,
exerciseDescriptionSnapshot: exerciseDescriptionSnapshot, exerciseDescriptionSnapshot: exerciseDescriptionSnapshot,
exerciseImageMediaIdSnapshot: exerciseImageMediaIdSnapshot, exerciseImageMediaIdSnapshot: exerciseImageMediaIdSnapshot,
exerciseImageMediaIdsSnapshot: exerciseImageMediaIdsSnapshot,
exerciseVideoMediaIdSnapshot: exerciseVideoMediaIdSnapshot, exerciseVideoMediaIdSnapshot: exerciseVideoMediaIdSnapshot,
exerciseArchivedSnapshot: exerciseArchivedSnapshot, exerciseArchivedSnapshot: exerciseArchivedSnapshot,
availableTimeSnapshot: availableTimeSnapshot, availableTimeSnapshot: availableTimeSnapshot,

View File

@ -130,6 +130,20 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
Text(_exercise.name, style: Theme.of(context).textTheme.headlineMedium), Text(_exercise.name, style: Theme.of(context).textTheme.headlineMedium),
if (_exercise.hasMedia) ...[
const SizedBox(height: 12),
Align(
alignment: Alignment.centerLeft,
child: Tooltip(
message: 'Voir les médias de lexercice',
child: OutlinedButton.icon(
onPressed: () => _openExerciseMedia(_exercise),
icon: const Icon(Icons.perm_media_outlined),
label: const Text('Voir médias'),
),
),
),
],
const SizedBox(height: 16), const SizedBox(height: 16),
SetMeasureInput( SetMeasureInput(
exercise: _exercise, exercise: _exercise,
@ -183,6 +197,19 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
fontSize: 30, fontSize: 30,
), ),
), ),
if (nextExercise.hasMedia) ...[
const SizedBox(height: 12),
Center(
child: Tooltip(
message: 'Voir les médias de lexercice',
child: OutlinedButton.icon(
onPressed: () => _openExerciseMedia(nextExercise),
icon: const Icon(Icons.perm_media_outlined),
label: const Text('Voir médias'),
),
),
),
],
], ],
const Spacer(), const Spacer(),
Row( Row(
@ -282,6 +309,23 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
); );
} }
Future<void> _openExerciseMedia(ExecutionExercise exercise) async {
if (!exercise.hasMedia) {
return;
}
await showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
useSafeArea: true,
builder: (context) => _ExerciseMediaSheet(
exercise: exercise,
restRemainingSeconds: _mode == WorkoutExecutionMode.rest
? _remainingRestSeconds
: null,
),
);
}
bool get _canPauseFromNavigation { bool get _canPauseFromNavigation {
return _mode == WorkoutExecutionMode.active || return _mode == WorkoutExecutionMode.active ||
_mode == WorkoutExecutionMode.rest; _mode == WorkoutExecutionMode.rest;
@ -816,6 +860,228 @@ final class _RestPlanBanner extends StatelessWidget {
} }
} }
enum _MediaSheetSection { images, video }
final class _ExerciseMediaSheet extends StatefulWidget {
const _ExerciseMediaSheet({
required this.exercise,
this.restRemainingSeconds,
});
final ExecutionExercise exercise;
final int? restRemainingSeconds;
@override
State<_ExerciseMediaSheet> createState() => _ExerciseMediaSheetState();
}
final class _ExerciseMediaSheetState extends State<_ExerciseMediaSheet> {
late final PageController _pageController;
late _MediaSheetSection _section;
var _imageIndex = 0;
@override
void initState() {
super.initState();
_pageController = PageController();
_section = widget.exercise.imageMediaIds.isNotEmpty
? _MediaSheetSection.images
: _MediaSheetSection.video;
}
@override
void dispose() {
_pageController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final exercise = widget.exercise;
final hasImages = exercise.imageMediaIds.isNotEmpty;
final hasVideo = exercise.videoMediaId != null;
return SizedBox(
height: MediaQuery.of(context).size.height * 0.95,
child: Scaffold(
appBar: AppBar(
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Médias de lexercice'),
Text(exercise.name, style: Theme.of(context).textTheme.bodySmall),
],
),
automaticallyImplyLeading: false,
actions: [
IconButton(
tooltip: 'Fermer',
onPressed: () => Navigator.of(context).pop(),
icon: const Icon(Icons.close),
),
],
),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
if (widget.restRemainingSeconds != null) ...[
_RestPlanBanner(remainingSeconds: widget.restRemainingSeconds!),
const SizedBox(height: 16),
],
if (hasImages && hasVideo) ...[
Wrap(
spacing: 8,
children: [
ChoiceChip(
label: const Text('Images'),
selected: _section == _MediaSheetSection.images,
onSelected: (_) {
setState(() => _section = _MediaSheetSection.images);
},
),
ChoiceChip(
label: const Text('Vidéo'),
selected: _section == _MediaSheetSection.video,
onSelected: (_) {
setState(() => _section = _MediaSheetSection.video);
},
),
],
),
const SizedBox(height: 16),
],
if (_section == _MediaSheetSection.images && hasImages)
_ImageMediaGallery(
imageMediaIds: exercise.imageMediaIds,
pageController: _pageController,
currentIndex: _imageIndex,
onPageChanged: (index) => setState(() => _imageIndex = index),
)
else if (hasVideo)
_VideoMediaPanel(videoMediaId: exercise.videoMediaId!),
],
),
),
);
}
}
final class _ImageMediaGallery extends StatelessWidget {
const _ImageMediaGallery({
required this.imageMediaIds,
required this.pageController,
required this.currentIndex,
required this.onPageChanged,
});
final List<String> imageMediaIds;
final PageController pageController;
final int currentIndex;
final ValueChanged<int> onPageChanged;
@override
Widget build(BuildContext context) {
return CourtBlazerAccentPanel(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Align(
alignment: Alignment.centerRight,
child: Text(
'${currentIndex + 1}/${imageMediaIds.length}',
style: Theme.of(context).textTheme.labelLarge?.copyWith(
color: Theme.of(context).colorScheme.primary,
),
),
),
const SizedBox(height: 12),
SizedBox(
height: 320,
child: PageView.builder(
controller: pageController,
itemCount: imageMediaIds.length,
onPageChanged: onPageChanged,
itemBuilder: (context, index) {
return _ImageMediaPage(
mediaAssetId: imageMediaIds[index],
index: index,
);
},
),
),
],
),
);
}
}
final class _ImageMediaPage extends StatelessWidget {
const _ImageMediaPage({required this.mediaAssetId, required this.index});
final String mediaAssetId;
final int index;
@override
Widget build(BuildContext context) {
final tokens = courtBlazerTokensOf(context);
return Container(
margin: const EdgeInsets.symmetric(horizontal: 4),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(6),
border: Border.all(color: tokens.border),
color: Theme.of(context).colorScheme.surface,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.image_outlined,
size: 64,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(height: 16),
Text(
'Image ${index + 1}',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 4),
Text(mediaAssetId, style: Theme.of(context).textTheme.bodySmall),
],
),
);
}
}
final class _VideoMediaPanel extends StatelessWidget {
const _VideoMediaPanel({required this.videoMediaId});
final String videoMediaId;
@override
Widget build(BuildContext context) {
return CourtBlazerAccentPanel(
padding: const EdgeInsets.all(16),
child: SizedBox(
height: 260,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.play_circle_outline,
size: 64,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(height: 16),
Text('Vidéo', style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 4),
Text(videoMediaId, style: Theme.of(context).textTheme.bodySmall),
],
),
),
);
}
}
final class _WorkoutPlanSetTile extends StatelessWidget { final class _WorkoutPlanSetTile extends StatelessWidget {
const _WorkoutPlanSetTile({ const _WorkoutPlanSetTile({
required this.position, required this.position,
@ -1364,6 +1630,8 @@ final class ExecutionExercise {
required this.repsEnabled, required this.repsEnabled,
required this.scoreEnabled, required this.scoreEnabled,
required this.restSeconds, required this.restSeconds,
this.imageMediaIds = const [],
this.videoMediaId,
this.scoreInputMode = ScoreInputMode.manual, this.scoreInputMode = ScoreInputMode.manual,
this.scoreLabel, this.scoreLabel,
this.targetTimeSeconds, this.targetTimeSeconds,
@ -1386,6 +1654,10 @@ final class ExecutionExercise {
repsEnabled: exercise['repsEnabled'] as bool, repsEnabled: exercise['repsEnabled'] as bool,
scoreEnabled: exercise['scoreEnabled'] as bool, scoreEnabled: exercise['scoreEnabled'] as bool,
restSeconds: exercise['restSecondsOverride'] as int? ?? 0, restSeconds: exercise['restSecondsOverride'] as int? ?? 0,
imageMediaIds: _imageMediaIdsFromSnapshot(exercise),
videoMediaId:
exercise['videoMediaIdSnapshot'] as String? ??
exercise['exerciseVideoMediaIdSnapshot'] as String?,
scoreInputMode: _scoreInputModeFromSnapshot( scoreInputMode: _scoreInputModeFromSnapshot(
exercise['scoreInputModeSnapshot'], exercise['scoreInputModeSnapshot'],
), ),
@ -1410,6 +1682,8 @@ final class ExecutionExercise {
final bool repsEnabled; final bool repsEnabled;
final bool scoreEnabled; final bool scoreEnabled;
final int restSeconds; final int restSeconds;
final List<String> imageMediaIds;
final String? videoMediaId;
final ScoreInputMode scoreInputMode; final ScoreInputMode scoreInputMode;
final String? scoreLabel; final String? scoreLabel;
final int? targetTimeSeconds; final int? targetTimeSeconds;
@ -1420,6 +1694,26 @@ final class ExecutionExercise {
bool get manualScoreEnabled { bool get manualScoreEnabled {
return scoreEnabled && scoreInputMode == ScoreInputMode.manual; return scoreEnabled && scoreInputMode == ScoreInputMode.manual;
} }
bool get hasMedia {
return imageMediaIds.isNotEmpty || videoMediaId != null;
}
}
List<String> _imageMediaIdsFromSnapshot(Map<String, dynamic> exercise) {
final imageMediaIds =
exercise['imageMediaIdsSnapshot'] ??
exercise['exerciseImageMediaIdsSnapshot'];
if (imageMediaIds is List) {
return imageMediaIds
.whereType<String>()
.where((id) => id.trim().isNotEmpty)
.toList(growable: false);
}
final imageMediaId = exercise['exerciseImageMediaIdSnapshot'] as String?;
return [
if (imageMediaId != null && imageMediaId.trim().isNotEmpty) imageMediaId,
];
} }
ScoreInputMode _scoreInputModeFromSnapshot(Object? value) { ScoreInputMode _scoreInputModeFromSnapshot(Object? value) {

View File

@ -225,6 +225,66 @@ void main() {
expect(updated.imageMediaId, 'media-1'); expect(updated.imageMediaId, 'media-1');
}); });
test('program exercise snapshot keeps the full image gallery', () async {
final exerciseRepository = _FakeExerciseRepository()
..exercise = Exercise(
metadata: _metadata('exercise-1'),
name: 'Squat',
imageMediaIds: const ['media-1', 'media-2', 'media-3'],
videoMediaId: 'video-1',
hasTimeMeasure: false,
hasRepsMeasure: true,
hasScoreMeasure: false,
defaultTargetReps: 10,
);
final programRepository = _FakeProgramRepository()
..programs.add(
Program(
metadata: _metadata('program-1'),
name: 'Jambes',
defaultRestSeconds: 60,
),
);
final useCase = ProgramUseCases(
programRepository: programRepository,
exerciseRepository: exerciseRepository,
templateRepository: _FakeWorkoutTemplateRepository(),
clock: _FakeClock(DateTime.utc(2026, 7, 17, 12)),
ids: _FakeIds(),
originDeviceId: 'device-1',
);
final snapshot = await useCase.addExercise(
programId: 'program-1',
exerciseId: 'exercise-1',
position: 0,
setsCount: 3,
enabledMeasures: const {WorkoutMeasure.reps},
);
expect(snapshot.exerciseImageMediaIdSnapshot, 'media-1');
expect(snapshot.exerciseImageMediaIdsSnapshot, [
'media-1',
'media-2',
'media-3',
]);
expect(snapshot.exerciseVideoMediaIdSnapshot, 'video-1');
expect(snapshot.toSnapshotJson()['imageMediaIdsSnapshot'], [
'media-1',
'media-2',
'media-3',
]);
expect(
programRepository
.programs
.single
.exercises
.single
.exerciseImageMediaIdsSnapshot,
snapshot.exerciseImageMediaIdsSnapshot,
);
});
test('delete exercise removes it from referencing programs', () async { test('delete exercise removes it from referencing programs', () async {
final exerciseRepository = _FakeExerciseRepository() final exerciseRepository = _FakeExerciseRepository()
..exercise = Exercise( ..exercise = Exercise(

View File

@ -116,6 +116,131 @@ void main() {
expect(find.text('Pas de repos'), findsOneWidget); expect(find.text('Pas de repos'), findsOneWidget);
}); });
testWidgets('le bouton médias est absent sans média', (tester) async {
final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
final session = ActiveWorkoutSession(
metadata: _metadata('session-1'),
sourceWorkoutTemplateId: 'template-1',
status: ActiveWorkoutStatus.running,
startedAt: DateTime.utc(2026, 7, 17, 12),
lastPersistedAt: DateTime.utc(2026, 7, 17, 12),
elapsedActiveMs: 0,
currentProgramIndex: 0,
currentExerciseIndex: 0,
currentSetIndex: 0,
resolvedTemplateSnapshotJson: _sessionSnapshot(),
);
await tester.pumpWidget(
MaterialApp(
home: WorkoutExecutionScreen(
initialSession: session,
activeUseCases: _activeUseCases(
_FakeActiveSessionRepository(),
clock,
),
closeUseCase: _closeUseCase(_FakeActiveSessionRepository(), clock),
historyUseCases: _historyUseCases(clock),
workoutTemplateUseCases: _workoutTemplateUseCases(),
),
),
);
expect(find.text('Voir médias'), findsNothing);
expect(find.byTooltip('Voir les médias de lexercice'), findsNothing);
});
testWidgets('le bouton médias ouvre la sheet si médias présents', (
tester,
) async {
final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
final session = ActiveWorkoutSession(
metadata: _metadata('session-1'),
sourceWorkoutTemplateId: 'template-1',
status: ActiveWorkoutStatus.running,
startedAt: DateTime.utc(2026, 7, 17, 12),
lastPersistedAt: DateTime.utc(2026, 7, 17, 12),
elapsedActiveMs: 0,
currentProgramIndex: 0,
currentExerciseIndex: 0,
currentSetIndex: 0,
resolvedTemplateSnapshotJson: _sessionSnapshot(
imageMediaIds: const ['image-1'],
videoMediaId: 'video-1',
),
);
await tester.pumpWidget(
MaterialApp(
home: WorkoutExecutionScreen(
initialSession: session,
activeUseCases: _activeUseCases(
_FakeActiveSessionRepository(),
clock,
),
closeUseCase: _closeUseCase(_FakeActiveSessionRepository(), clock),
historyUseCases: _historyUseCases(clock),
workoutTemplateUseCases: _workoutTemplateUseCases(),
),
),
);
expect(find.byTooltip('Voir les médias de lexercice'), findsOneWidget);
await tester.tap(find.text('Voir médias'));
await tester.pumpAndSettle();
expect(find.text('Médias de lexercice'), findsOneWidget);
expect(find.text('Images'), findsOneWidget);
expect(find.text('Vidéo'), findsOneWidget);
expect(find.text('1/1'), findsOneWidget);
expect(find.text('Image 1'), findsOneWidget);
});
testWidgets('la galerie médias navigue entre les images', (tester) async {
final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
final session = ActiveWorkoutSession(
metadata: _metadata('session-1'),
sourceWorkoutTemplateId: 'template-1',
status: ActiveWorkoutStatus.running,
startedAt: DateTime.utc(2026, 7, 17, 12),
lastPersistedAt: DateTime.utc(2026, 7, 17, 12),
elapsedActiveMs: 0,
currentProgramIndex: 0,
currentExerciseIndex: 0,
currentSetIndex: 0,
resolvedTemplateSnapshotJson: _sessionSnapshot(
imageMediaIds: const ['image-1', 'image-2', 'image-3'],
),
);
await tester.pumpWidget(
MaterialApp(
home: WorkoutExecutionScreen(
initialSession: session,
activeUseCases: _activeUseCases(
_FakeActiveSessionRepository(),
clock,
),
closeUseCase: _closeUseCase(_FakeActiveSessionRepository(), clock),
historyUseCases: _historyUseCases(clock),
workoutTemplateUseCases: _workoutTemplateUseCases(),
),
),
);
await tester.tap(find.text('Voir médias'));
await tester.pumpAndSettle();
expect(find.text('1/3'), findsOneWidget);
await tester.drag(find.byType(PageView), const Offset(-400, 0));
await tester.pumpAndSettle();
expect(find.text('2/3'), findsOneWidget);
expect(find.text('Image 2'), findsOneWidget);
});
testWidgets('un repos actif est repris avec le temps restant recalculé', ( testWidgets('un repos actif est repris avec le temps restant recalculé', (
tester, tester,
) async { ) async {
@ -559,6 +684,8 @@ String _sessionSnapshot({
double targetScore = 80, double targetScore = 80,
String scoreUnit = 'kg', String scoreUnit = 'kg',
ScoreInputMode scoreInputMode = ScoreInputMode.manual, ScoreInputMode scoreInputMode = ScoreInputMode.manual,
List<String> imageMediaIds = const [],
String? videoMediaId,
}) { }) {
return jsonEncode({ return jsonEncode({
'name': 'Séance jambes', 'name': 'Séance jambes',
@ -582,6 +709,10 @@ String _sessionSnapshot({
'scoreLabelSnapshot': 'Score', 'scoreLabelSnapshot': 'Score',
'scoreUnitSnapshot': scoreUnit, 'scoreUnitSnapshot': scoreUnit,
'restSecondsOverride': restSeconds, 'restSecondsOverride': restSeconds,
'exerciseImageMediaIdsSnapshot': imageMediaIds,
'imageMediaIdsSnapshot': imageMediaIds,
'exerciseVideoMediaIdSnapshot': videoMediaId,
'videoMediaIdSnapshot': videoMediaId,
}, },
], ],
}), }),