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:
Branislav Nohaj
2025-10-04 16:13:02 +02:00
parent 1903fb4d5e
commit c00246dedb
2 changed files with 140 additions and 74 deletions

View File

@@ -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,
),
),
);
}
}

View File

@@ -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
),
);
}
}
}