mirror of
https://github.com/wger-project/flutter.git
synced 2026-02-18 00:17:48 +01:00
added better comments
This commit is contained in:
@@ -1,10 +1,24 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Form for collecting image metadata including license information
|
||||
/// Form for collecting CC BY-SA 4.0 license metadata for exercise images
|
||||
///
|
||||
/// Displayed after image selection in Step 5 of exercise creation
|
||||
/// Collects: title, source URL, author info, derivative work info, and image style
|
||||
/// This form is displayed after image selection in Step 5 of exercise creation.
|
||||
/// It collects all required and optional license attribution fields:
|
||||
///
|
||||
/// Required by CC BY-SA 4.0:
|
||||
/// - Author name
|
||||
/// - License type (implicitly CC BY-SA 4.0)
|
||||
///
|
||||
/// Optional but recommended:
|
||||
/// - Title (helps identify the image)
|
||||
/// - Source URL (where image was found)
|
||||
/// - Author URL (author's website/profile)
|
||||
/// - Derivative source URL (if modified from another work)
|
||||
/// - Image style (PHOTO, 3D, LINE, LOW-POLY, OTHER)
|
||||
///
|
||||
/// All metadata is sent to the API's /exerciseimage endpoint along with
|
||||
/// the image file when the exercise is submitted.
|
||||
class ImageDetailsForm extends StatefulWidget {
|
||||
final File imageFile;
|
||||
final Function(File image, Map<String, String> details) onAdd;
|
||||
@@ -24,12 +38,15 @@ class ImageDetailsForm extends StatefulWidget {
|
||||
class _ImageDetailsFormState extends State<ImageDetailsForm> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
|
||||
// Text controllers for license metadata fields
|
||||
final _titleController = TextEditingController();
|
||||
final _sourceLinkController = TextEditingController();
|
||||
final _authorController = TextEditingController();
|
||||
final _authorLinkController = TextEditingController();
|
||||
final _originalSourceController = TextEditingController();
|
||||
final _sourceLinkController = TextEditingController(); // license_object_url in API
|
||||
final _authorController = TextEditingController(); // license_author in API
|
||||
final _authorLinkController = TextEditingController(); // license_author_url in API
|
||||
final _originalSourceController = TextEditingController(); // license_derivative_source_url in API
|
||||
|
||||
/// Currently selected image type
|
||||
/// Maps to API 'style' field: PHOTO=1, 3D=2, LINE=3, LOW-POLY=4, OTHER=5
|
||||
String _selectedImageType = 'PHOTO';
|
||||
|
||||
final List<Map<String, dynamic>> _imageTypes = [
|
||||
@@ -50,8 +67,14 @@ class _ImageDetailsFormState extends State<ImageDetailsForm> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// Maps UI image type selection to API style value
|
||||
/// PHOTO=1, 3D=2, LINE=3, LOW-POLY=4, OTHER=5
|
||||
/// Maps UI image type selection to API 'style' field value
|
||||
///
|
||||
/// API expects numeric string:
|
||||
/// - PHOTO = '1'
|
||||
/// - 3D = '2'
|
||||
/// - LINE = '3'
|
||||
/// - LOW-POLY = '4'
|
||||
/// - OTHER = '5'
|
||||
String _getStyleValue() {
|
||||
switch (_selectedImageType) {
|
||||
case 'PHOTO':
|
||||
@@ -65,7 +88,7 @@ class _ImageDetailsFormState extends State<ImageDetailsForm> {
|
||||
case 'OTHER':
|
||||
return '5';
|
||||
default:
|
||||
return '1';
|
||||
return '1'; // Default to PHOTO if unknown
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,6 +113,7 @@ class _ImageDetailsFormState extends State<ImageDetailsForm> {
|
||||
_buildImagePreview(),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// License title field - helps identify the image
|
||||
_buildTextField(
|
||||
controller: _titleController,
|
||||
label: 'Title',
|
||||
@@ -97,6 +121,7 @@ class _ImageDetailsFormState extends State<ImageDetailsForm> {
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Source URL - where the image was found (license_object_url in API)
|
||||
_buildTextField(
|
||||
controller: _sourceLinkController,
|
||||
label: 'Link to the source website, if available',
|
||||
@@ -105,6 +130,7 @@ class _ImageDetailsFormState extends State<ImageDetailsForm> {
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Author name - required for proper CC BY-SA attribution
|
||||
_buildTextField(
|
||||
controller: _authorController,
|
||||
label: 'Author(s)',
|
||||
@@ -112,6 +138,7 @@ class _ImageDetailsFormState extends State<ImageDetailsForm> {
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Author's website/profile URL
|
||||
_buildTextField(
|
||||
controller: _authorLinkController,
|
||||
label: 'Link to author website or profile, if available',
|
||||
@@ -120,6 +147,7 @@ class _ImageDetailsFormState extends State<ImageDetailsForm> {
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Original source if this is a derivative work (modified from another image)
|
||||
_buildTextField(
|
||||
controller: _originalSourceController,
|
||||
label: 'Link to the original source, if this is a derivative work',
|
||||
@@ -202,7 +230,10 @@ class _ImageDetailsFormState extends State<ImageDetailsForm> {
|
||||
);
|
||||
}
|
||||
|
||||
/// Info box explaining what constitutes a derivative work
|
||||
/// Informational box explaining what constitutes a derivative work
|
||||
///
|
||||
/// Important for CC BY-SA compliance - users need to understand when
|
||||
/// they must cite the original work they modified
|
||||
Widget _buildDerivativeWorkNote() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
@@ -234,7 +265,10 @@ class _ImageDetailsFormState extends State<ImageDetailsForm> {
|
||||
);
|
||||
}
|
||||
|
||||
/// Selector for image style (PHOTO, 3D, LINE, LOW-POLY, OTHER)
|
||||
/// Visual selector for image style/type
|
||||
///
|
||||
/// Allows user to categorize the image as PHOTO, 3D render, LINE drawing,
|
||||
/// LOW-POLY art, or OTHER. This helps users find appropriate exercise images.
|
||||
Widget _buildImageTypeSelector() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -298,7 +332,10 @@ class _ImageDetailsFormState extends State<ImageDetailsForm> {
|
||||
);
|
||||
}
|
||||
|
||||
/// Warning box about CC BY-SA 4.0 license requirements
|
||||
/// Legal notice about CC BY-SA 4.0 license
|
||||
///
|
||||
/// Critical for compliance - informs user that by uploading, they're
|
||||
/// releasing the image under CC BY-SA 4.0 and must have rights to do so
|
||||
Widget _buildLicenseInfo() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
@@ -359,11 +396,12 @@ class _ImageDetailsFormState extends State<ImageDetailsForm> {
|
||||
onPressed: () {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
// Build details map with API field names
|
||||
// Style is always included, other fields only if non-empty
|
||||
final details = <String, String>{
|
||||
'style': _getStyleValue(),
|
||||
};
|
||||
|
||||
// Add only non-empty fields
|
||||
// Add optional fields only if user provided values
|
||||
final title = _titleController.text.trim();
|
||||
if (title.isNotEmpty) {
|
||||
details['license_title'] = title;
|
||||
@@ -389,6 +427,7 @@ class _ImageDetailsFormState extends State<ImageDetailsForm> {
|
||||
details['license_derivative_source_url'] = derivativeUrl;
|
||||
}
|
||||
|
||||
// Pass image and metadata back to parent
|
||||
widget.onAdd(widget.imageFile, details);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -8,6 +8,17 @@ import 'package:wger/widgets/add_exercise/mixins/image_picker_mixin.dart';
|
||||
import 'package:wger/widgets/add_exercise/preview_images.dart';
|
||||
import 'package:wger/widgets/add_exercise/image_details_form.dart';
|
||||
|
||||
/// Step 5 of exercise creation wizard - Image upload with license metadata
|
||||
///
|
||||
/// This step allows users to add exercise images with proper CC BY-SA 4.0 license
|
||||
/// attribution. Unlike the previous implementation that uploaded images directly,
|
||||
/// this version collects license metadata (title, author, URLs) before adding images.
|
||||
///
|
||||
/// Flow:
|
||||
/// 1. User picks image from camera/gallery
|
||||
/// 2. ImageDetailsForm is shown to collect license metadata
|
||||
/// 3. Image + metadata is stored in AddExerciseProvider
|
||||
/// 4. Final upload happens in Step 6 when user clicks "Submit"
|
||||
class Step5Images extends StatefulWidget {
|
||||
final GlobalKey<FormState> formkey;
|
||||
const Step5Images({required this.formkey});
|
||||
@@ -17,9 +28,16 @@ class Step5Images extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _Step5ImagesState extends State<Step5Images> with ExerciseImagePickerMixin {
|
||||
/// Currently selected image waiting for metadata input
|
||||
/// When non-null, ImageDetailsForm is displayed instead of image picker
|
||||
File? _currentImageToAdd;
|
||||
|
||||
/// Pick image and show details form for license metadata
|
||||
/// Pick image from camera or gallery and show metadata collection form
|
||||
///
|
||||
/// Validates file format (jpg, jpeg, png, webp) and size (<20MB) before
|
||||
/// showing the form. Invalid files are rejected with a snackbar message.
|
||||
///
|
||||
/// [pickFromCamera] - If true, opens camera; otherwise opens gallery
|
||||
void _pickAndShowImageDetails(BuildContext context, {bool pickFromCamera = false}) async {
|
||||
final imagePicker = ImagePicker();
|
||||
|
||||
@@ -33,7 +51,7 @@ class _Step5ImagesState extends State<Step5Images> with ExerciseImagePickerMixin
|
||||
if (selectedImage != null) {
|
||||
final imageFile = File(selectedImage.path);
|
||||
|
||||
// Validate file type and size
|
||||
// Validate file type - only common image formats accepted
|
||||
bool isFileValid = true;
|
||||
String errorMessage = '';
|
||||
|
||||
@@ -44,6 +62,7 @@ class _Step5ImagesState extends State<Step5Images> with ExerciseImagePickerMixin
|
||||
errorMessage = "Select only 'jpg', 'jpeg', 'png', 'webp' files";
|
||||
}
|
||||
|
||||
// Validate file size - 20MB limit matches server-side restriction
|
||||
final fileSizeInMB = imageFile.lengthSync() / 1024 / 1024;
|
||||
if (fileSizeInMB > 20) {
|
||||
isFileValid = false;
|
||||
@@ -59,17 +78,25 @@ class _Step5ImagesState extends State<Step5Images> with ExerciseImagePickerMixin
|
||||
return;
|
||||
}
|
||||
|
||||
// Show details form for valid image
|
||||
// Show metadata collection form for valid image
|
||||
setState(() {
|
||||
_currentImageToAdd = imageFile;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Add image with license metadata to provider
|
||||
/// Add image with its license metadata to the provider
|
||||
///
|
||||
/// Called when user clicks "ADD" in ImageDetailsForm. The image and metadata
|
||||
/// are stored locally in AddExerciseProvider and will be uploaded together
|
||||
/// when the exercise is submitted in Step 6.
|
||||
///
|
||||
/// [image] - The image file to add
|
||||
/// [details] - Map containing license fields (license_title, license_author, etc.)
|
||||
void _addImageWithDetails(File image, Map<String, String> details) {
|
||||
final provider = context.read<AddExerciseProvider>();
|
||||
|
||||
// Store image with metadata - actual upload happens in addExercise()
|
||||
provider.addExerciseImages(
|
||||
[image],
|
||||
title: details['license_title'],
|
||||
@@ -80,6 +107,7 @@ class _Step5ImagesState extends State<Step5Images> with ExerciseImagePickerMixin
|
||||
style: details['style'] ?? '1',
|
||||
);
|
||||
|
||||
// Reset form state
|
||||
setState(() {
|
||||
_currentImageToAdd = null;
|
||||
});
|
||||
@@ -93,6 +121,7 @@ class _Step5ImagesState extends State<Step5Images> with ExerciseImagePickerMixin
|
||||
);
|
||||
}
|
||||
|
||||
/// Cancel metadata input and return to image picker
|
||||
void _cancelImageAdd() {
|
||||
setState(() {
|
||||
_currentImageToAdd = null;
|
||||
@@ -105,7 +134,7 @@ class _Step5ImagesState extends State<Step5Images> with ExerciseImagePickerMixin
|
||||
key: widget.formkey,
|
||||
child: Column(
|
||||
children: [
|
||||
// Show license notice when not adding image
|
||||
// License notice - shown when not entering metadata
|
||||
if (_currentImageToAdd == null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||
@@ -116,7 +145,7 @@ class _Step5ImagesState extends State<Step5Images> with ExerciseImagePickerMixin
|
||||
),
|
||||
),
|
||||
|
||||
// Show image details form when image is selected
|
||||
// Metadata collection form - shown when image is selected
|
||||
if (_currentImageToAdd != null)
|
||||
ImageDetailsForm(
|
||||
imageFile: _currentImageToAdd!,
|
||||
@@ -124,12 +153,12 @@ class _Step5ImagesState extends State<Step5Images> with ExerciseImagePickerMixin
|
||||
onCancel: _cancelImageAdd,
|
||||
),
|
||||
|
||||
// Show image picker or preview when no image is being added
|
||||
// Image picker or preview - shown when not entering metadata
|
||||
if (_currentImageToAdd == null)
|
||||
Consumer<AddExerciseProvider>(
|
||||
builder: (ctx, provider, __) {
|
||||
if (provider.exerciseImages.isNotEmpty) {
|
||||
// Show preview of existing images
|
||||
// Show preview of images that have been added with metadata
|
||||
return Column(
|
||||
children: [
|
||||
PreviewExerciseImages(
|
||||
@@ -145,7 +174,7 @@ class _Step5ImagesState extends State<Step5Images> with ExerciseImagePickerMixin
|
||||
);
|
||||
}
|
||||
|
||||
// Show empty state with camera/gallery buttons
|
||||
// Empty state - no images added yet
|
||||
return Column(
|
||||
children: [
|
||||
const SizedBox(height: 20),
|
||||
@@ -162,6 +191,7 @@ class _Step5ImagesState extends State<Step5Images> with ExerciseImagePickerMixin
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
// Camera and Gallery buttons
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
|
||||
Reference in New Issue
Block a user