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

@ -25,6 +25,7 @@ final class ExerciseUseCases {
String? description,
String? imageMediaId,
List<String> imageMediaIds = const [],
String? iconMediaId,
String? videoMediaId,
required bool hasTimeMeasure,
required bool hasRepsMeasure,
@ -53,6 +54,7 @@ final class ExerciseUseCases {
name: name,
description: description,
imageMediaIds: _resolveExerciseImageIds(imageMediaId, imageMediaIds),
iconMediaId: iconMediaId,
videoMediaId: videoMediaId,
hasTimeMeasure: hasTimeMeasure,
hasRepsMeasure: hasRepsMeasure,
@ -83,6 +85,7 @@ final class ExerciseUseCases {
String? description,
String? imageMediaId,
Object? imageMediaIds = _useCaseUnchanged,
Object? iconMediaId = _useCaseUnchanged,
String? videoMediaId,
required bool hasTimeMeasure,
required bool hasRepsMeasure,
@ -123,13 +126,21 @@ final class ExerciseUseCases {
defaultTargetScore: resolvedDefaultTargetScore,
defaultTargetScoreTimeMs: resolvedDefaultTargetScoreTimeMs,
);
final resolvedImageMediaIds = imageMediaIds == _useCaseUnchanged
? _resolveExerciseImageIds(imageMediaId, exercise.imageMediaIds)
: imageMediaIds as List<String>;
final resolvedIconMediaId = iconMediaId == _useCaseUnchanged
? exercise.iconMediaId != null &&
resolvedImageMediaIds.contains(exercise.iconMediaId)
? exercise.iconMediaId
: null
: iconMediaId as String?;
final updated = exercise.copyWith(
metadata: exercise.metadata.touch(clock.now()),
name: name,
description: description,
imageMediaIds: imageMediaIds == _useCaseUnchanged
? _resolveExerciseImageIds(imageMediaId, exercise.imageMediaIds)
: imageMediaIds as List<String>,
imageMediaIds: resolvedImageMediaIds,
iconMediaId: resolvedIconMediaId,
videoMediaId: videoMediaId,
hasTimeMeasure: hasTimeMeasure,
hasRepsMeasure: hasRepsMeasure,
@ -190,6 +201,9 @@ final class ExerciseUseCases {
imageMediaIds: exercise.imageMediaIds
.where((imageId) => imageId != mediaAssetId)
.toList(),
iconMediaId: exercise.iconMediaId == mediaAssetId
? null
: exercise.iconMediaId,
metadata: exercise.metadata.touch(clock.now()),
);
await repository.save(updated);
@ -217,6 +231,22 @@ final class ExerciseUseCases {
return updated;
}
Future<Exercise> setIcon({
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(
iconMediaId: mediaAssetId,
metadata: exercise.metadata.touch(clock.now()),
);
await repository.save(updated);
return updated;
}
Future<Exercise> attachVideo({
required String exerciseId,
required String mediaAssetId,