merge(main): sélecteur de média pour les exercices (ticket #15)
Fusionne feature/#15-selecteur-media — analyze propre (1 warning mineur sans impact), 27/27 tests verts, build APK debug validé. Clôture le lot de correctifs post-QA (tickets #13, #14, #15). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
issueRef: "#15"
|
issueRef: "#15"
|
||||||
version: 2
|
version: 3
|
||||||
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
updatedAt: 1784319493575
|
updatedAt: 1784319755572
|
||||||
---
|
---
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
id: "3463569f-e837-4af9-8156-4968e178d4e1"
|
id: "3463569f-e837-4af9-8156-4968e178d4e1"
|
||||||
number: 15
|
number: 15
|
||||||
title: "[DevFrontend] Sélecteur de fichier pour l'import média (remplace la saisie manuelle de chemin)"
|
title: "[DevFrontend] Sélecteur de fichier pour l'import média (remplace la saisie manuelle de chemin)"
|
||||||
status: "inProgress"
|
status: "closed"
|
||||||
priority: "medium"
|
priority: "medium"
|
||||||
sprint: null
|
sprint: null
|
||||||
links: [{"target":"#6","kind":"relatesTo"}]
|
links: [{"target":"#6","kind":"relatesTo"}]
|
||||||
@ -10,7 +10,7 @@ agentRefs: [{"agentId":"9933c93a-b8a1-4164-a3bb-7063fdad747d","role":"assigned"}
|
|||||||
createdBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
createdBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
createdAt: 1784318999121
|
createdAt: 1784318999121
|
||||||
updatedAt: 1784319493575
|
updatedAt: 1784319755572
|
||||||
version: 2
|
version: 3
|
||||||
---
|
---
|
||||||
L'écran de création/édition d'exercice (lib/presentation/exercise_library_screen.dart) fait actuellement saisir manuellement un chemin de fichier pour importer une image/vidéo. Remplacer par un vrai sélecteur de fichiers via le package `image_picker` (à ajouter en dépendance dans pubspec.yaml — nouvelle dépendance, signalée et acceptée par l'utilisateur). Garder MediaUseCases.importMedia tel quel côté logique métier, seul le déclenchement du chemin source change (sélection graphique au lieu de TextField).
|
L'écran de création/édition d'exercice (lib/presentation/exercise_library_screen.dart) fait actuellement saisir manuellement un chemin de fichier pour importer une image/vidéo. Remplacer par un vrai sélecteur de fichiers via le package `image_picker` (à ajouter en dépendance dans pubspec.yaml — nouvelle dépendance, signalée et acceptée par l'utilisateur). Garder MediaUseCases.importMedia tel quel côté logique métier, seul le déclenchement du chemin source change (sélection graphique au lieu de TextField).
|
||||||
@ -173,13 +173,13 @@
|
|||||||
"issueRef": "#15",
|
"issueRef": "#15",
|
||||||
"path": "15",
|
"path": "15",
|
||||||
"title": "[DevFrontend] Sélecteur de fichier pour l'import média (remplace la saisie manuelle de chemin)",
|
"title": "[DevFrontend] Sélecteur de fichier pour l'import média (remplace la saisie manuelle de chemin)",
|
||||||
"status": "inProgress",
|
"status": "closed",
|
||||||
"priority": "medium",
|
"priority": "medium",
|
||||||
"sprint": null,
|
"sprint": null,
|
||||||
"assignedAgentIds": [
|
"assignedAgentIds": [
|
||||||
"9933c93a-b8a1-4164-a3bb-7063fdad747d"
|
"9933c93a-b8a1-4164-a3bb-7063fdad747d"
|
||||||
],
|
],
|
||||||
"updatedAt": 1784319493575
|
"updatedAt": 1784319755572
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -1,8 +1,27 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
|
||||||
import '../application/application.dart';
|
import '../application/application.dart';
|
||||||
import '../domain/domain.dart';
|
import '../domain/domain.dart';
|
||||||
|
|
||||||
|
abstract interface class MediaSourcePicker {
|
||||||
|
Future<String?> pickPath(MediaKind kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
final class ImagePickerMediaSourcePicker implements MediaSourcePicker {
|
||||||
|
const ImagePickerMediaSourcePicker();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String?> pickPath(MediaKind kind) async {
|
||||||
|
final picker = ImagePicker();
|
||||||
|
final file = switch (kind) {
|
||||||
|
MediaKind.image => await picker.pickImage(source: ImageSource.gallery),
|
||||||
|
MediaKind.video => await picker.pickVideo(source: ImageSource.gallery),
|
||||||
|
};
|
||||||
|
return file?.path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final class ExerciseLibraryScreen extends StatefulWidget {
|
final class ExerciseLibraryScreen extends StatefulWidget {
|
||||||
const ExerciseLibraryScreen({
|
const ExerciseLibraryScreen({
|
||||||
required this.exerciseUseCases,
|
required this.exerciseUseCases,
|
||||||
@ -248,12 +267,14 @@ final class ExerciseFormScreen extends StatefulWidget {
|
|||||||
const ExerciseFormScreen({
|
const ExerciseFormScreen({
|
||||||
required this.exerciseUseCases,
|
required this.exerciseUseCases,
|
||||||
required this.mediaUseCases,
|
required this.mediaUseCases,
|
||||||
|
this.mediaPicker = const ImagePickerMediaSourcePicker(),
|
||||||
this.exercise,
|
this.exercise,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
final ExerciseUseCases exerciseUseCases;
|
final ExerciseUseCases exerciseUseCases;
|
||||||
final MediaUseCases mediaUseCases;
|
final MediaUseCases mediaUseCases;
|
||||||
|
final MediaSourcePicker mediaPicker;
|
||||||
final Exercise? exercise;
|
final Exercise? exercise;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -266,10 +287,10 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
|
|||||||
late final TextEditingController _descriptionController;
|
late final TextEditingController _descriptionController;
|
||||||
late final TextEditingController _scoreLabelController;
|
late final TextEditingController _scoreLabelController;
|
||||||
late final TextEditingController _scoreUnitController;
|
late final TextEditingController _scoreUnitController;
|
||||||
final _imagePathController = TextEditingController();
|
|
||||||
final _videoPathController = TextEditingController();
|
|
||||||
String? _imageMediaId;
|
String? _imageMediaId;
|
||||||
String? _videoMediaId;
|
String? _videoMediaId;
|
||||||
|
String? _selectedImageName;
|
||||||
|
String? _selectedVideoName;
|
||||||
var _hasTime = true;
|
var _hasTime = true;
|
||||||
var _hasReps = false;
|
var _hasReps = false;
|
||||||
var _hasScore = false;
|
var _hasScore = false;
|
||||||
@ -302,8 +323,6 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
|
|||||||
_descriptionController.dispose();
|
_descriptionController.dispose();
|
||||||
_scoreLabelController.dispose();
|
_scoreLabelController.dispose();
|
||||||
_scoreUnitController.dispose();
|
_scoreUnitController.dispose();
|
||||||
_imagePathController.dispose();
|
|
||||||
_videoPathController.dispose();
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -346,19 +365,21 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
_MediaImportField(
|
_MediaImportField(
|
||||||
controller: _imagePathController,
|
label: 'Image',
|
||||||
label: 'Chemin de l’image',
|
selectedFileName: _selectedImageName,
|
||||||
imported: _imageMediaId != null,
|
imported: _imageMediaId != null,
|
||||||
importing: _importingImage,
|
importing: _importingImage,
|
||||||
onImport: () => _importMedia(MediaKind.image),
|
actionLabel: 'Choisir une image',
|
||||||
|
onPick: () => _importMedia(MediaKind.image),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
_MediaImportField(
|
_MediaImportField(
|
||||||
controller: _videoPathController,
|
label: 'Vidéo',
|
||||||
label: 'Chemin de la vidéo',
|
selectedFileName: _selectedVideoName,
|
||||||
imported: _videoMediaId != null,
|
imported: _videoMediaId != null,
|
||||||
importing: _importingVideo,
|
importing: _importingVideo,
|
||||||
onImport: () => _importMedia(MediaKind.video),
|
actionLabel: 'Choisir une vidéo',
|
||||||
|
onPick: () => _importMedia(MediaKind.video),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
Text(
|
Text(
|
||||||
@ -461,19 +482,18 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _importMedia(MediaKind kind) async {
|
Future<void> _importMedia(MediaKind kind) async {
|
||||||
final controller = kind == MediaKind.image
|
final sourcePath = await widget.mediaPicker.pickPath(kind);
|
||||||
? _imagePathController
|
if (sourcePath == null) {
|
||||||
: _videoPathController;
|
|
||||||
final sourcePath = controller.text.trim();
|
|
||||||
if (sourcePath.isEmpty) {
|
|
||||||
_showSnackBar('Indique un chemin de fichier à importer.');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
final fileName = _fileNameFromPath(sourcePath);
|
||||||
setState(() {
|
setState(() {
|
||||||
if (kind == MediaKind.image) {
|
if (kind == MediaKind.image) {
|
||||||
_importingImage = true;
|
_importingImage = true;
|
||||||
|
_selectedImageName = fileName;
|
||||||
} else {
|
} else {
|
||||||
_importingVideo = true;
|
_importingVideo = true;
|
||||||
|
_selectedVideoName = fileName;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
@ -580,6 +600,11 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
|
|||||||
return text.isEmpty ? null : text;
|
return text.isEmpty ? null : text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _fileNameFromPath(String path) {
|
||||||
|
final parts = path.split(RegExp(r'[/\\]'));
|
||||||
|
return parts.isEmpty ? path : parts.last;
|
||||||
|
}
|
||||||
|
|
||||||
void _showSnackBar(String message) {
|
void _showSnackBar(String message) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
ScaffoldMessenger.of(
|
ScaffoldMessenger.of(
|
||||||
@ -635,47 +660,54 @@ final class _MeasureSwitch extends StatelessWidget {
|
|||||||
|
|
||||||
final class _MediaImportField extends StatelessWidget {
|
final class _MediaImportField extends StatelessWidget {
|
||||||
const _MediaImportField({
|
const _MediaImportField({
|
||||||
required this.controller,
|
|
||||||
required this.label,
|
required this.label,
|
||||||
|
required this.selectedFileName,
|
||||||
required this.imported,
|
required this.imported,
|
||||||
required this.importing,
|
required this.importing,
|
||||||
required this.onImport,
|
required this.actionLabel,
|
||||||
|
required this.onPick,
|
||||||
});
|
});
|
||||||
|
|
||||||
final TextEditingController controller;
|
|
||||||
final String label;
|
final String label;
|
||||||
|
final String? selectedFileName;
|
||||||
final bool imported;
|
final bool imported;
|
||||||
final bool importing;
|
final bool importing;
|
||||||
final VoidCallback onImport;
|
final String actionLabel;
|
||||||
|
final VoidCallback onPick;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return TextField(
|
final fileName = selectedFileName;
|
||||||
controller: controller,
|
final status = fileName == null
|
||||||
|
? imported
|
||||||
|
? 'Média déjà importé'
|
||||||
|
: 'Aucun fichier sélectionné'
|
||||||
|
: fileName;
|
||||||
|
return InputDecorator(
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: label,
|
labelText: label,
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
suffixIcon: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
if (imported)
|
|
||||||
const Tooltip(
|
|
||||||
message: 'Média importé',
|
|
||||||
child: Icon(Icons.check_circle_outline),
|
|
||||||
),
|
),
|
||||||
IconButton(
|
child: Row(
|
||||||
tooltip: 'Importer',
|
children: [
|
||||||
onPressed: importing ? null : onImport,
|
Icon(imported ? Icons.check_circle_outline : Icons.perm_media),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Text(status, maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
OutlinedButton.icon(
|
||||||
|
onPressed: importing ? null : onPick,
|
||||||
icon: importing
|
icon: importing
|
||||||
? const SizedBox.square(
|
? const SizedBox.square(
|
||||||
dimension: 18,
|
dimension: 16,
|
||||||
child: CircularProgressIndicator(strokeWidth: 2),
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
)
|
)
|
||||||
: const Icon(Icons.upload_file),
|
: const Icon(Icons.upload_file),
|
||||||
|
label: Text(actionLabel),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
129
pubspec.lock
129
pubspec.lock
@ -153,6 +153,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.2"
|
version: "3.1.2"
|
||||||
|
cross_file:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: cross_file
|
||||||
|
sha256: "92c9c43c383bfa1c32079d3bc492d55d6d4318044b7b47edaff8971cbb555c51"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.3.5+4"
|
||||||
crypto:
|
crypto:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -225,6 +233,38 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "7.0.1"
|
version: "7.0.1"
|
||||||
|
file_selector_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: file_selector_linux
|
||||||
|
sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.9.4"
|
||||||
|
file_selector_macos:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: file_selector_macos
|
||||||
|
sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.9.5"
|
||||||
|
file_selector_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: file_selector_platform_interface
|
||||||
|
sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.7.0"
|
||||||
|
file_selector_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: file_selector_windows
|
||||||
|
sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.9.3+5"
|
||||||
fixnum:
|
fixnum:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -246,11 +286,24 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.0.0"
|
version: "6.0.0"
|
||||||
|
flutter_plugin_android_lifecycle:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_plugin_android_lifecycle
|
||||||
|
sha256: "3854fe5e3bff0b113c658f260b90c95dea17c92db0f2addeac2e343dd9969785"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.35"
|
||||||
flutter_test:
|
flutter_test:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
|
flutter_web_plugins:
|
||||||
|
dependency: transitive
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
glob:
|
glob:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -275,6 +328,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.2"
|
version: "2.0.2"
|
||||||
|
http:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: http
|
||||||
|
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.6.0"
|
||||||
http_multi_server:
|
http_multi_server:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -291,6 +352,70 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.1.2"
|
version: "4.1.2"
|
||||||
|
image_picker:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: image_picker
|
||||||
|
sha256: d8402284df184bc05f4a2210c6c23983b0720f4cd87cbd05c5390a78af602667
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.3"
|
||||||
|
image_picker_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_android
|
||||||
|
sha256: "6f3a1995eafb000333174fae92202622033b0ee7fd917a6cd3730295264df84a"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.8.13+19"
|
||||||
|
image_picker_for_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_for_web
|
||||||
|
sha256: "66257a3191ab360d23a55c8241c91a6e329d31e94efa7be9cf7a212e65850214"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.1"
|
||||||
|
image_picker_ios:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_ios
|
||||||
|
sha256: b9c4a438a9ff4f60808c9cf0039b93a42bb6c2211ef6ebb647394b2b3fa84588
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.8.13+6"
|
||||||
|
image_picker_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_linux
|
||||||
|
sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.2"
|
||||||
|
image_picker_macos:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_macos
|
||||||
|
sha256: "86f0f15a309de7e1a552c12df9ce5b59fe927e71385329355aec4776c6a8ec91"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.2+1"
|
||||||
|
image_picker_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_platform_interface
|
||||||
|
sha256: "567e056716333a1647c64bb6bd873cff7622233a5c3f694be28a583d4715690c"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.11.1"
|
||||||
|
image_picker_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_windows
|
||||||
|
sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.2"
|
||||||
io:
|
io:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -721,5 +846,5 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.3"
|
version: "3.1.3"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.11.0 <4.0.0"
|
dart: ">=3.12.0 <4.0.0"
|
||||||
flutter: ">=3.38.4"
|
flutter: ">=3.44.0"
|
||||||
|
|||||||
@ -13,6 +13,7 @@ dependencies:
|
|||||||
cupertino_icons: ^1.0.8
|
cupertino_icons: ^1.0.8
|
||||||
drift: ^2.34.2
|
drift: ^2.34.2
|
||||||
drift_flutter: ^0.3.1
|
drift_flutter: ^0.3.1
|
||||||
|
image_picker: ^1.2.3
|
||||||
path: ^1.9.1
|
path: ^1.9.1
|
||||||
path_provider: ^2.1.5
|
path_provider: ^2.1.5
|
||||||
sqlite3_flutter_libs: ^0.6.0+eol
|
sqlite3_flutter_libs: ^0.6.0+eol
|
||||||
|
|||||||
@ -65,6 +65,34 @@ void main() {
|
|||||||
expect(find.text('Répétitions'), findsOneWidget);
|
expect(find.text('Répétitions'), findsOneWidget);
|
||||||
expect(find.text('Score (kg)'), findsOneWidget);
|
expect(find.text('Score (kg)'), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'sélectionner une image déclenche l’import et confirme le fichier',
|
||||||
|
(tester) async {
|
||||||
|
final exerciseRepository = _FakeExerciseRepository();
|
||||||
|
final storage = _FakeMediaStorage();
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: ExerciseFormScreen(
|
||||||
|
exerciseUseCases: _exerciseUseCases(exerciseRepository),
|
||||||
|
mediaUseCases: _mediaUseCases(exerciseRepository, storage: storage),
|
||||||
|
mediaPicker: const _FakeMediaPicker(
|
||||||
|
imagePath: '/tmp/imports/photo.png',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.tap(find.text('Choisir 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);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ExerciseUseCases _exerciseUseCases(_FakeExerciseRepository repository) {
|
ExerciseUseCases _exerciseUseCases(_FakeExerciseRepository repository) {
|
||||||
@ -76,17 +104,35 @@ ExerciseUseCases _exerciseUseCases(_FakeExerciseRepository repository) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
MediaUseCases _mediaUseCases(_FakeExerciseRepository exerciseRepository) {
|
MediaUseCases _mediaUseCases(
|
||||||
|
_FakeExerciseRepository exerciseRepository, {
|
||||||
|
_FakeMediaStorage? storage,
|
||||||
|
}) {
|
||||||
return MediaUseCases(
|
return MediaUseCases(
|
||||||
mediaRepository: _FakeMediaAssetRepository(),
|
mediaRepository: _FakeMediaAssetRepository(),
|
||||||
exerciseRepository: exerciseRepository,
|
exerciseRepository: exerciseRepository,
|
||||||
storage: _FakeMediaStorage(),
|
storage: storage ?? _FakeMediaStorage(),
|
||||||
clock: _FakeClock(DateTime.utc(2026, 7, 17, 12)),
|
clock: _FakeClock(DateTime.utc(2026, 7, 17, 12)),
|
||||||
ids: _FakeIds(),
|
ids: _FakeIds(),
|
||||||
originDeviceId: 'device-1',
|
originDeviceId: 'device-1',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final class _FakeMediaPicker implements MediaSourcePicker {
|
||||||
|
const _FakeMediaPicker({this.imagePath, this.videoPath});
|
||||||
|
|
||||||
|
final String? imagePath;
|
||||||
|
final String? videoPath;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String?> pickPath(MediaKind kind) async {
|
||||||
|
return switch (kind) {
|
||||||
|
MediaKind.image => imagePath,
|
||||||
|
MediaKind.video => videoPath,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
EntityMetadata _metadata(String id) {
|
EntityMetadata _metadata(String id) {
|
||||||
return EntityMetadata(
|
return EntityMetadata(
|
||||||
id: id,
|
id: id,
|
||||||
@ -167,6 +213,8 @@ final class _FakeMediaAssetRepository implements MediaAssetRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final class _FakeMediaStorage implements LocalMediaStorage {
|
final class _FakeMediaStorage implements LocalMediaStorage {
|
||||||
|
final imports = <_MediaImportCall>[];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> deleteByLocalUri(String localUri) async {}
|
Future<void> deleteByLocalUri(String localUri) async {}
|
||||||
|
|
||||||
@ -176,6 +224,13 @@ final class _FakeMediaStorage implements LocalMediaStorage {
|
|||||||
required MediaKind kind,
|
required MediaKind kind,
|
||||||
required String stableFileName,
|
required String stableFileName,
|
||||||
}) async {
|
}) async {
|
||||||
|
imports.add(
|
||||||
|
_MediaImportCall(
|
||||||
|
sourcePath: sourcePath,
|
||||||
|
kind: kind,
|
||||||
|
stableFileName: stableFileName,
|
||||||
|
),
|
||||||
|
);
|
||||||
return StoredMediaFile(
|
return StoredMediaFile(
|
||||||
localUri: 'file:///tmp/$stableFileName',
|
localUri: 'file:///tmp/$stableFileName',
|
||||||
mimeType: kind == MediaKind.image ? 'image/png' : 'video/mp4',
|
mimeType: kind == MediaKind.image ? 'image/png' : 'video/mp4',
|
||||||
@ -186,3 +241,15 @@ final class _FakeMediaStorage implements LocalMediaStorage {
|
|||||||
@override
|
@override
|
||||||
Future<Set<String>> listManagedLocalUris() async => const {};
|
Future<Set<String>> listManagedLocalUris() async => const {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final class _MediaImportCall {
|
||||||
|
const _MediaImportCall({
|
||||||
|
required this.sourcePath,
|
||||||
|
required this.kind,
|
||||||
|
required this.stableFileName,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String sourcePath;
|
||||||
|
final MediaKind kind;
|
||||||
|
final String stableFileName;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user