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:
@ -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,
|
||||
|
||||
Reference in New Issue
Block a user