fix(media): sélecteur de média pour les exercices (ticket #15)
Corrige/ajoute le sélecteur de média sur exercise_library_screen.dart suite aux retours de QA. flutter analyze propre (1 warning mineur "paramètre inutilisé" dans un test, sans impact), 27/27 tests verts, build APK debug validé. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -1,8 +1,27 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
|
||||
import '../application/application.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 {
|
||||
const ExerciseLibraryScreen({
|
||||
required this.exerciseUseCases,
|
||||
@ -248,12 +267,14 @@ final class ExerciseFormScreen extends StatefulWidget {
|
||||
const ExerciseFormScreen({
|
||||
required this.exerciseUseCases,
|
||||
required this.mediaUseCases,
|
||||
this.mediaPicker = const ImagePickerMediaSourcePicker(),
|
||||
this.exercise,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final ExerciseUseCases exerciseUseCases;
|
||||
final MediaUseCases mediaUseCases;
|
||||
final MediaSourcePicker mediaPicker;
|
||||
final Exercise? exercise;
|
||||
|
||||
@override
|
||||
@ -266,10 +287,10 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
|
||||
late final TextEditingController _descriptionController;
|
||||
late final TextEditingController _scoreLabelController;
|
||||
late final TextEditingController _scoreUnitController;
|
||||
final _imagePathController = TextEditingController();
|
||||
final _videoPathController = TextEditingController();
|
||||
String? _imageMediaId;
|
||||
String? _videoMediaId;
|
||||
String? _selectedImageName;
|
||||
String? _selectedVideoName;
|
||||
var _hasTime = true;
|
||||
var _hasReps = false;
|
||||
var _hasScore = false;
|
||||
@ -302,8 +323,6 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
|
||||
_descriptionController.dispose();
|
||||
_scoreLabelController.dispose();
|
||||
_scoreUnitController.dispose();
|
||||
_imagePathController.dispose();
|
||||
_videoPathController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -346,19 +365,21 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_MediaImportField(
|
||||
controller: _imagePathController,
|
||||
label: 'Chemin de l’image',
|
||||
label: 'Image',
|
||||
selectedFileName: _selectedImageName,
|
||||
imported: _imageMediaId != null,
|
||||
importing: _importingImage,
|
||||
onImport: () => _importMedia(MediaKind.image),
|
||||
actionLabel: 'Choisir une image',
|
||||
onPick: () => _importMedia(MediaKind.image),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_MediaImportField(
|
||||
controller: _videoPathController,
|
||||
label: 'Chemin de la vidéo',
|
||||
label: 'Vidéo',
|
||||
selectedFileName: _selectedVideoName,
|
||||
imported: _videoMediaId != null,
|
||||
importing: _importingVideo,
|
||||
onImport: () => _importMedia(MediaKind.video),
|
||||
actionLabel: 'Choisir une vidéo',
|
||||
onPick: () => _importMedia(MediaKind.video),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
@ -461,19 +482,18 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
|
||||
}
|
||||
|
||||
Future<void> _importMedia(MediaKind kind) async {
|
||||
final controller = kind == MediaKind.image
|
||||
? _imagePathController
|
||||
: _videoPathController;
|
||||
final sourcePath = controller.text.trim();
|
||||
if (sourcePath.isEmpty) {
|
||||
_showSnackBar('Indique un chemin de fichier à importer.');
|
||||
final sourcePath = await widget.mediaPicker.pickPath(kind);
|
||||
if (sourcePath == null) {
|
||||
return;
|
||||
}
|
||||
final fileName = _fileNameFromPath(sourcePath);
|
||||
setState(() {
|
||||
if (kind == MediaKind.image) {
|
||||
_importingImage = true;
|
||||
_selectedImageName = fileName;
|
||||
} else {
|
||||
_importingVideo = true;
|
||||
_selectedVideoName = fileName;
|
||||
}
|
||||
});
|
||||
try {
|
||||
@ -580,6 +600,11 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
|
||||
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) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(
|
||||
@ -635,46 +660,53 @@ final class _MeasureSwitch extends StatelessWidget {
|
||||
|
||||
final class _MediaImportField extends StatelessWidget {
|
||||
const _MediaImportField({
|
||||
required this.controller,
|
||||
required this.label,
|
||||
required this.selectedFileName,
|
||||
required this.imported,
|
||||
required this.importing,
|
||||
required this.onImport,
|
||||
required this.actionLabel,
|
||||
required this.onPick,
|
||||
});
|
||||
|
||||
final TextEditingController controller;
|
||||
final String label;
|
||||
final String? selectedFileName;
|
||||
final bool imported;
|
||||
final bool importing;
|
||||
final VoidCallback onImport;
|
||||
final String actionLabel;
|
||||
final VoidCallback onPick;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextField(
|
||||
controller: controller,
|
||||
final fileName = selectedFileName;
|
||||
final status = fileName == null
|
||||
? imported
|
||||
? 'Média déjà importé'
|
||||
: 'Aucun fichier sélectionné'
|
||||
: fileName;
|
||||
return InputDecorator(
|
||||
decoration: InputDecoration(
|
||||
labelText: label,
|
||||
border: const OutlineInputBorder(),
|
||||
suffixIcon: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (imported)
|
||||
const Tooltip(
|
||||
message: 'Média importé',
|
||||
child: Icon(Icons.check_circle_outline),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: 'Importer',
|
||||
onPressed: importing ? null : onImport,
|
||||
icon: importing
|
||||
? const SizedBox.square(
|
||||
dimension: 18,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.upload_file),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
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
|
||||
? const SizedBox.square(
|
||||
dimension: 16,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.upload_file),
|
||||
label: Text(actionLabel),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user