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>
This commit is contained in:
2026-07-18 14:25:26 +02:00
parent c92bded211
commit bdd7877ea5
10 changed files with 589 additions and 27 deletions

View File

@ -126,6 +126,7 @@ final class Exercise {
this.description,
String? imageMediaId,
List<String> imageMediaIds = const [],
String? iconMediaId,
this.videoMediaId,
required this.hasTimeMeasure,
required this.hasRepsMeasure,
@ -139,10 +140,13 @@ final class Exercise {
this.defaultTargetScoreTimeMs,
this.archivedAt,
}) : name = _nonBlank(name, 'Exercise name'),
imageMediaIds = _validatedImageMediaIds(
imageMediaIds.isEmpty && imageMediaId != null
? [imageMediaId]
: imageMediaIds,
imageMediaIds = _resolvedExerciseImageMediaIds(
imageMediaId,
imageMediaIds,
),
iconMediaId = _validatedIconMediaId(
iconMediaId,
_resolvedExerciseImageMediaIds(imageMediaId, imageMediaIds),
) {
_requireAtLeastOneMeasure(
hasTime: hasTimeMeasure,
@ -174,6 +178,7 @@ final class Exercise {
final String name;
final String? description;
final List<String> imageMediaIds;
final String? iconMediaId;
final String? videoMediaId;
final bool hasTimeMeasure;
final bool hasRepsMeasure;
@ -206,6 +211,7 @@ final class Exercise {
Object? description = _unchanged,
Object? imageMediaId = _unchanged,
Object? imageMediaIds = _unchanged,
Object? iconMediaId = _unchanged,
Object? videoMediaId = _unchanged,
bool? hasTimeMeasure,
bool? hasRepsMeasure,
@ -219,17 +225,25 @@ final class Exercise {
Object? defaultTargetScoreTimeMs = _unchanged,
Object? archivedAt = _unchanged,
}) {
final resolvedImageMediaIds = imageMediaIds == _unchanged
? imageMediaId == _unchanged
? this.imageMediaIds
: [if (imageMediaId != null) imageMediaId as String]
: imageMediaIds as List<String>;
final resolvedIconMediaId = iconMediaId == _unchanged
? this.iconMediaId != null &&
resolvedImageMediaIds.contains(this.iconMediaId)
? this.iconMediaId
: null
: iconMediaId as String?;
return Exercise(
metadata: metadata ?? this.metadata,
name: name ?? this.name,
description: description == _unchanged
? this.description
: description as String?,
imageMediaIds: imageMediaIds == _unchanged
? imageMediaId == _unchanged
? this.imageMediaIds
: [if (imageMediaId != null) imageMediaId as String]
: imageMediaIds as List<String>,
imageMediaIds: resolvedImageMediaIds,
iconMediaId: resolvedIconMediaId,
videoMediaId: videoMediaId == _unchanged
? this.videoMediaId
: videoMediaId as String?,
@ -957,6 +971,30 @@ List<String> _validatedImageMediaIds(List<String> ids) {
return normalized;
}
List<String> _resolvedExerciseImageMediaIds(
String? imageMediaId,
List<String> imageMediaIds,
) {
return _validatedImageMediaIds(
imageMediaIds.isEmpty && imageMediaId != null
? [imageMediaId]
: imageMediaIds,
);
}
String? _validatedIconMediaId(String? iconMediaId, List<String> imageMediaIds) {
if (iconMediaId == null) {
return null;
}
final normalized = _nonBlank(iconMediaId, 'Icon media id');
if (!imageMediaIds.contains(normalized)) {
throw const DomainException(
'Exercise icon must be one of the exercise images.',
);
}
return normalized;
}
void _requireAtLeastOneMeasure({
required bool hasTime,
required bool hasReps,