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

@ -132,23 +132,90 @@ void main() {
home: ExerciseFormScreen(
exerciseUseCases: _exerciseUseCases(exerciseRepository),
mediaUseCases: _mediaUseCases(exerciseRepository, storage: storage),
mediaPicker: const _FakeMediaPicker(
imagePath: '/tmp/imports/photo.png',
),
mediaPicker: _FakeMediaPicker(imagePath: '/tmp/imports/photo.png'),
),
),
);
await tester.tap(find.text('Choisir une image'));
await tester.tap(find.text('Ajouter une image'));
await tester.pump();
expect(storage.imports.single.sourcePath, '/tmp/imports/photo.png');
expect(storage.imports.single.kind, MediaKind.image);
expect(find.text('photo.png'), findsOneWidget);
expect(find.text('Image importée.'), findsOneWidget);
expect(find.text('Image ajoutée.'), findsOneWidget);
},
);
testWidgets('ajouter des images bloque la sixième image', (tester) async {
final exerciseRepository = _FakeExerciseRepository();
final storage = _FakeMediaStorage();
await tester.binding.setSurfaceSize(const Size(400, 1200));
addTearDown(() => tester.binding.setSurfaceSize(null));
await tester.pumpWidget(
MaterialApp(
home: ExerciseFormScreen(
exerciseUseCases: _exerciseUseCases(exerciseRepository),
mediaUseCases: _mediaUseCases(exerciseRepository, storage: storage),
mediaPicker: _FakeMediaPicker(
imagePaths: const [
'/tmp/imports/photo-1.png',
'/tmp/imports/photo-2.png',
'/tmp/imports/photo-3.png',
'/tmp/imports/photo-4.png',
'/tmp/imports/photo-5.png',
'/tmp/imports/photo-6.png',
],
),
),
),
);
for (var i = 0; i < 5; i++) {
await tester.tap(find.text('Ajouter une image'));
await tester.pump();
}
expect(storage.imports, hasLength(5));
expect(find.text('photo-5.png'), findsOneWidget);
expect(find.text('Maximum 5 images par exercice.'), findsOneWidget);
await tester.tap(find.text('Ajouter une image'));
await tester.pump();
expect(storage.imports, hasLength(5));
expect(find.text('photo-6.png'), findsNothing);
expect(find.text('Maximum 5 images par exercice.'), findsWidgets);
});
testWidgets('supprimer une image la retire de la galerie', (tester) async {
final exerciseRepository = _FakeExerciseRepository();
final storage = _FakeMediaStorage();
await tester.pumpWidget(
MaterialApp(
home: ExerciseFormScreen(
exerciseUseCases: _exerciseUseCases(exerciseRepository),
mediaUseCases: _mediaUseCases(exerciseRepository, storage: storage),
mediaPicker: _FakeMediaPicker(imagePath: '/tmp/imports/photo.png'),
),
),
);
await tester.tap(find.text('Ajouter une image'));
await tester.pump();
expect(find.text('photo.png'), findsOneWidget);
await tester.tap(find.byTooltip('Supprimer limage'));
await tester.pump();
expect(find.text('photo.png'), findsNothing);
expect(find.text('Aucune image sélectionnée'), findsOneWidget);
});
testWidgets(
'supprimer un exercice demande confirmation puis le retire de la liste',
(tester) async {
@ -244,17 +311,31 @@ MediaUseCases _mediaUseCases(
}
final class _FakeMediaPicker implements MediaSourcePicker {
const _FakeMediaPicker({this.imagePath});
_FakeMediaPicker({this.imagePath, this.imagePaths = const []});
final String? imagePath;
final List<String> imagePaths;
var _nextImagePath = 0;
@override
Future<String?> pickPath(MediaKind kind) async {
return switch (kind) {
MediaKind.image => imagePath,
MediaKind.image => _pickImagePath(),
MediaKind.video => null,
};
}
String? _pickImagePath() {
if (imagePaths.isEmpty) {
return imagePath;
}
if (_nextImagePath >= imagePaths.length) {
return null;
}
final path = imagePaths[_nextImagePath];
_nextImagePath += 1;
return path;
}
}
EntityMetadata _metadata(String id) {