feat(exercice): galerie de 5 images par exercice (ticket #35)

Étend le modèle Drift (tables.dart, app_database.dart/.g.dart), les
entités/use cases/repositories et exercise_library_screen.dart pour
supporter jusqu'à 5 images par exercice au lieu d'une seule. flutter
analyze propre, 63/63 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:02:40 +02:00
parent 6419e2d9cc
commit 8bda4758f2
9 changed files with 2250 additions and 299 deletions

View File

@ -24,6 +24,7 @@ final class ExerciseUseCases {
required String name,
String? description,
String? imageMediaId,
List<String> imageMediaIds = const [],
String? videoMediaId,
required bool hasTimeMeasure,
required bool hasRepsMeasure,
@ -51,7 +52,7 @@ final class ExerciseUseCases {
metadata: _newMetadata(ids, originDeviceId, now),
name: name,
description: description,
imageMediaId: imageMediaId,
imageMediaIds: _resolveExerciseImageIds(imageMediaId, imageMediaIds),
videoMediaId: videoMediaId,
hasTimeMeasure: hasTimeMeasure,
hasRepsMeasure: hasRepsMeasure,
@ -81,6 +82,7 @@ final class ExerciseUseCases {
required String name,
String? description,
String? imageMediaId,
Object? imageMediaIds = _useCaseUnchanged,
String? videoMediaId,
required bool hasTimeMeasure,
required bool hasRepsMeasure,
@ -125,7 +127,9 @@ final class ExerciseUseCases {
metadata: exercise.metadata.touch(clock.now()),
name: name,
description: description,
imageMediaId: imageMediaId,
imageMediaIds: imageMediaIds == _useCaseUnchanged
? _resolveExerciseImageIds(imageMediaId, exercise.imageMediaIds)
: imageMediaIds as List<String>,
videoMediaId: videoMediaId,
hasTimeMeasure: hasTimeMeasure,
hasRepsMeasure: hasRepsMeasure,
@ -151,7 +155,62 @@ final class ExerciseUseCases {
throw const DomainException('Exercise not found.');
}
final updated = exercise.copyWith(
imageMediaId: mediaAssetId,
imageMediaIds: _appendExerciseImage(exercise.imageMediaIds, mediaAssetId),
metadata: exercise.metadata.touch(clock.now()),
);
await repository.save(updated);
return updated;
}
Future<Exercise> addImage({
required String exerciseId,
required String mediaAssetId,
}) async {
final exercise = await repository.findById(exerciseId);
if (exercise == null) {
throw const DomainException('Exercise not found.');
}
final updated = exercise.copyWith(
imageMediaIds: _appendExerciseImage(exercise.imageMediaIds, mediaAssetId),
metadata: exercise.metadata.touch(clock.now()),
);
await repository.save(updated);
return updated;
}
Future<Exercise> removeImage({
required String exerciseId,
required String mediaAssetId,
}) async {
final exercise = await repository.findById(exerciseId);
if (exercise == null) {
throw const DomainException('Exercise not found.');
}
final updated = exercise.copyWith(
imageMediaIds: exercise.imageMediaIds
.where((imageId) => imageId != mediaAssetId)
.toList(),
metadata: exercise.metadata.touch(clock.now()),
);
await repository.save(updated);
return updated;
}
Future<Exercise> reorderImages({
required String exerciseId,
required List<String> orderedMediaIds,
}) async {
final exercise = await repository.findById(exerciseId);
if (exercise == null) {
throw const DomainException('Exercise not found.');
}
if (!_sameImageSet(exercise.imageMediaIds, orderedMediaIds)) {
throw const DomainException(
'Reordered exercise images must match existing images.',
);
}
final updated = exercise.copyWith(
imageMediaIds: orderedMediaIds,
metadata: exercise.metadata.touch(clock.now()),
);
await repository.save(updated);
@ -275,7 +334,10 @@ final class MediaUseCases {
}
final updated = switch (media.kind) {
MediaKind.image => exercise.copyWith(
imageMediaId: media.metadata.id,
imageMediaIds: _appendExerciseImage(
exercise.imageMediaIds,
media.metadata.id,
),
metadata: exercise.metadata.touch(clock.now()),
),
MediaKind.video => exercise.copyWith(
@ -1479,6 +1541,33 @@ List<WorkoutTemplateProgram> _repositionWorkoutTemplatePrograms(
];
}
List<String> _resolveExerciseImageIds(
String? imageMediaId,
List<String> imageMediaIds,
) {
if (imageMediaId != null) {
return [imageMediaId];
}
return imageMediaIds;
}
List<String> _appendExerciseImage(List<String> currentIds, String mediaId) {
if (currentIds.contains(mediaId)) {
return currentIds;
}
if (currentIds.length >= 5) {
throw const DomainException('An exercise cannot have more than 5 images.');
}
return [...currentIds, mediaId];
}
bool _sameImageSet(List<String> left, List<String> right) {
if (left.length != right.length) {
return false;
}
return left.toSet().containsAll(right) && right.toSet().containsAll(left);
}
void _validateExerciseDefaultTargets({
required bool hasTimeMeasure,
required bool hasRepsMeasure,