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

@ -124,7 +124,8 @@ final class Exercise {
required this.metadata,
required String name,
this.description,
this.imageMediaId,
String? imageMediaId,
List<String> imageMediaIds = const [],
this.videoMediaId,
required this.hasTimeMeasure,
required this.hasRepsMeasure,
@ -137,7 +138,12 @@ final class Exercise {
this.defaultTargetScore,
this.defaultTargetScoreTimeMs,
this.archivedAt,
}) : name = _nonBlank(name, 'Exercise name') {
}) : name = _nonBlank(name, 'Exercise name'),
imageMediaIds = _validatedImageMediaIds(
imageMediaIds.isEmpty && imageMediaId != null
? [imageMediaId]
: imageMediaIds,
) {
_requireAtLeastOneMeasure(
hasTime: hasTimeMeasure,
hasReps: hasRepsMeasure,
@ -167,7 +173,7 @@ final class Exercise {
final EntityMetadata metadata;
final String name;
final String? description;
final String? imageMediaId;
final List<String> imageMediaIds;
final String? videoMediaId;
final bool hasTimeMeasure;
final bool hasRepsMeasure;
@ -187,6 +193,9 @@ final class Exercise {
if (hasScoreMeasure) WorkoutMeasure.score,
};
String? get imageMediaId =>
imageMediaIds.isEmpty ? null : imageMediaIds.first;
Exercise archive(DateTime now) {
return copyWith(archivedAt: now, metadata: metadata.touch(now));
}
@ -196,6 +205,7 @@ final class Exercise {
String? name,
Object? description = _unchanged,
Object? imageMediaId = _unchanged,
Object? imageMediaIds = _unchanged,
Object? videoMediaId = _unchanged,
bool? hasTimeMeasure,
bool? hasRepsMeasure,
@ -215,9 +225,11 @@ final class Exercise {
description: description == _unchanged
? this.description
: description as String?,
imageMediaId: imageMediaId == _unchanged
? this.imageMediaId
: imageMediaId as String?,
imageMediaIds: imageMediaIds == _unchanged
? imageMediaId == _unchanged
? this.imageMediaIds
: [if (imageMediaId != null) imageMediaId as String]
: imageMediaIds as List<String>,
videoMediaId: videoMediaId == _unchanged
? this.videoMediaId
: videoMediaId as String?,
@ -921,6 +933,19 @@ String _nonBlank(String? value, String label) {
return trimmed;
}
List<String> _validatedImageMediaIds(List<String> ids) {
if (ids.length > 5) {
throw const DomainException('An exercise cannot have more than 5 images.');
}
final normalized = ids
.map((id) => _nonBlank(id, 'Image media id'))
.toList(growable: false);
if (normalized.toSet().length != normalized.length) {
throw const DomainException('Exercise images must be unique.');
}
return normalized;
}
void _requireAtLeastOneMeasure({
required bool hasTime,
required bool hasReps,