mirror of
https://github.com/wger-project/flutter.git
synced 2026-02-18 00:17:48 +01:00
Add image source selection and preview improvements
Add dialog for camera/gallery selection and improve preview widget with edit mode toggle. Preview now supports add-more callback and can be displayed in read-only mode.
This commit is contained in:
@@ -1,73 +1,111 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:wger/providers/add_exercise.dart';
|
||||
|
||||
import 'mixins/image_picker_mixin.dart';
|
||||
|
||||
class PreviewExerciseImages extends StatelessWidget with ExerciseImagePickerMixin {
|
||||
/// Widget to preview selected exercise images
|
||||
///
|
||||
/// Displays images in a horizontal scrollable list with thumbnails.
|
||||
/// Each image shows a preview thumbnail and optionally a delete button.
|
||||
/// Can optionally include an "add more" button at the end of the list.
|
||||
class PreviewExerciseImages extends StatelessWidget {
|
||||
final List<File> selectedImages;
|
||||
final VoidCallback? onAddMore;
|
||||
final bool allowEdit;
|
||||
|
||||
const PreviewExerciseImages({super.key, required this.selectedImages, this.allowEdit = true});
|
||||
const PreviewExerciseImages({
|
||||
Key? key,
|
||||
required this.selectedImages,
|
||||
this.onAddMore,
|
||||
this.allowEdit = true,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Calculate item count: images + optional "add more" button
|
||||
final itemCount = selectedImages.length + (allowEdit && onAddMore != null ? 1 : 0);
|
||||
|
||||
return SizedBox(
|
||||
height: 300,
|
||||
child: ListView(scrollDirection: Axis.horizontal, children: [
|
||||
...selectedImages.map(
|
||||
(file) => SizedBox(
|
||||
height: 200,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Stack(
|
||||
children: [
|
||||
Image.file(file),
|
||||
if (allowEdit)
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(3.0),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.withValues(alpha: 0.5),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(20)),
|
||||
),
|
||||
child: IconButton(
|
||||
iconSize: 20,
|
||||
onPressed: () =>
|
||||
context.read<AddExerciseProvider>().removeExercise(file.path),
|
||||
color: Colors.white,
|
||||
icon: const Icon(Icons.delete),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
if (allowEdit)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Container(
|
||||
color: Colors.grey,
|
||||
height: 200,
|
||||
width: 100,
|
||||
child: Center(
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.add),
|
||||
onPressed: () => pickImages(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
]),
|
||||
height: 120,
|
||||
child: ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: itemCount,
|
||||
itemBuilder: (context, index) {
|
||||
// Show "add more" button at the end (only if editing is allowed)
|
||||
if (index == selectedImages.length) {
|
||||
return _buildAddMoreButton(context);
|
||||
}
|
||||
|
||||
// Show image thumbnail
|
||||
final image = selectedImages[index];
|
||||
return _buildImageCard(context, image);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildImageCard(BuildContext context, File image) {
|
||||
return Container(
|
||||
width: 120,
|
||||
margin: const EdgeInsets.only(right: 8),
|
||||
child: Stack(
|
||||
children: [
|
||||
// Image thumbnail
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Image.file(
|
||||
image,
|
||||
width: 120,
|
||||
height: 120,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
|
||||
// Delete button overlay (only shown if editing is allowed)
|
||||
if (allowEdit)
|
||||
Positioned(
|
||||
top: 4,
|
||||
right: 4,
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
color: Colors.white,
|
||||
iconSize: 20,
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: Colors.black.withOpacity(0.6),
|
||||
padding: const EdgeInsets.all(4),
|
||||
),
|
||||
onPressed: () {
|
||||
context.read<AddExerciseProvider>().removeExercise(image.path);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAddMoreButton(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onAddMore,
|
||||
child: Container(
|
||||
width: 120,
|
||||
height: 120,
|
||||
margin: const EdgeInsets.only(right: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceVariant,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
width: 2,
|
||||
style: BorderStyle.solid,
|
||||
),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.add,
|
||||
size: 48,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,39 @@ class _Step5ImagesState extends State<Step5Images> with ExerciseImagePickerMixin
|
||||
/// When non-null, ImageDetailsForm is displayed instead of image picker
|
||||
File? _currentImageToAdd;
|
||||
|
||||
/// Show dialog to choose between Camera and Gallery
|
||||
Future<void> _showImageSourceDialog(BuildContext context) async {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text(AppLocalizations.of(context).selectImage),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.camera_alt),
|
||||
title: Text(AppLocalizations.of(context).takePicture),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
_pickAndShowImageDetails(context, pickFromCamera: true);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.collections),
|
||||
title: Text(AppLocalizations.of(context).gallery),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
_pickAndShowImageDetails(context, pickFromCamera: false);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Pick image from camera or gallery and show metadata collection form
|
||||
///
|
||||
/// Validates file format (jpg, jpeg, png, webp) and size (<20MB) before
|
||||
@@ -105,18 +138,10 @@ class _Step5ImagesState extends State<Step5Images> with ExerciseImagePickerMixin
|
||||
style: details['style'] ?? '1',
|
||||
);
|
||||
|
||||
// Reset form state
|
||||
// Reset form state - image is now visible in preview list
|
||||
setState(() {
|
||||
_currentImageToAdd = null;
|
||||
});
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Image added with details'),
|
||||
backgroundColor: Colors.green,
|
||||
duration: Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Cancel metadata input and return to image picker
|
||||
@@ -159,12 +184,15 @@ class _Step5ImagesState extends State<Step5Images> with ExerciseImagePickerMixin
|
||||
// Show preview of images that have been added with metadata
|
||||
return Column(
|
||||
children: [
|
||||
PreviewExerciseImages(selectedImages: provider.exerciseImages),
|
||||
PreviewExerciseImages(
|
||||
selectedImages: provider.exerciseImages,
|
||||
onAddMore: () => _showImageSourceDialog(context),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () => _pickAndShowImageDetails(context),
|
||||
onPressed: () => _showImageSourceDialog(context),
|
||||
icon: const Icon(Icons.add_photo_alternate),
|
||||
label: const Text('Add Another Image'),
|
||||
label: Text(AppLocalizations.of(context).addImage),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -190,7 +218,7 @@ class _Step5ImagesState extends State<Step5Images> with ExerciseImagePickerMixin
|
||||
ElevatedButton.icon(
|
||||
onPressed: () => _pickAndShowImageDetails(context, pickFromCamera: true),
|
||||
icon: const Icon(Icons.camera_alt),
|
||||
label: const Text('Camera'),
|
||||
label: Text(AppLocalizations.of(context).takePicture),
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||||
),
|
||||
@@ -199,7 +227,7 @@ class _Step5ImagesState extends State<Step5Images> with ExerciseImagePickerMixin
|
||||
ElevatedButton.icon(
|
||||
onPressed: () => _pickAndShowImageDetails(context),
|
||||
icon: const Icon(Icons.collections),
|
||||
label: const Text('Gallery'),
|
||||
label: Text(AppLocalizations.of(context).gallery),
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||||
),
|
||||
@@ -225,4 +253,4 @@ class _Step5ImagesState extends State<Step5Images> with ExerciseImagePickerMixin
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user