mirror of
https://github.com/wger-project/flutter.git
synced 2026-02-18 00:17:48 +01:00
Refactor image handling in the exercise submission process
Now the images are kept in a single list, instead of having two for the files themselves and the metadata.
This commit is contained in:
@@ -143,3 +143,6 @@ const String API_RESULTS_PAGE_SIZE = '100';
|
||||
/// Marker used for identifying interpolated values in a list, e.g. for measurements
|
||||
/// the milliseconds in the entry date are set to this value
|
||||
const INTERPOLATION_MARKER = 123;
|
||||
|
||||
/// Creative Commons license IDs
|
||||
const CC_BY_SA_4_ID = 4;
|
||||
|
||||
67
lib/models/exercises/exercise_submission_images.dart
Normal file
67
lib/models/exercises/exercise_submission_images.dart
Normal file
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* This file is part of wger Workout Manager <https://github.com/wger-project>.
|
||||
* Copyright (C) 2020, 2021 wger Team
|
||||
*
|
||||
* wger Workout Manager is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
enum ImageType {
|
||||
photo(id: 1, label: 'Photo', icon: Icons.photo_camera),
|
||||
threeD(id: 2, label: '3D', icon: Icons.view_in_ar),
|
||||
line(id: 3, label: 'Line', icon: Icons.show_chart),
|
||||
lowPoly(id: 4, label: 'Low-Poly', icon: Icons.filter_vintage),
|
||||
other(id: 5, label: 'Other', icon: Icons.more_horiz);
|
||||
|
||||
const ImageType({required this.id, required this.label, required this.icon});
|
||||
|
||||
final int id;
|
||||
final String label;
|
||||
final IconData icon;
|
||||
}
|
||||
|
||||
class ExerciseSubmissionImage {
|
||||
final File imageFile;
|
||||
|
||||
String? title;
|
||||
String? author;
|
||||
String? authorUrl;
|
||||
String? sourceUrl;
|
||||
String? derivativeSourceUrl;
|
||||
ImageType type = ImageType.photo;
|
||||
|
||||
ExerciseSubmissionImage({
|
||||
this.title,
|
||||
this.author,
|
||||
this.authorUrl,
|
||||
this.sourceUrl,
|
||||
this.derivativeSourceUrl,
|
||||
this.type = ImageType.photo,
|
||||
required this.imageFile,
|
||||
});
|
||||
|
||||
Map<String, String> toJson() {
|
||||
return {
|
||||
'title': title ?? '',
|
||||
'author': author ?? '',
|
||||
'author_url': authorUrl ?? '',
|
||||
'source_url': sourceUrl ?? '',
|
||||
'derivative_source_url': derivativeSourceUrl ?? '',
|
||||
'type': type.id.toString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,11 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:wger/helpers/consts.dart';
|
||||
import 'package:wger/models/exercises/category.dart';
|
||||
import 'package:wger/models/exercises/equipment.dart';
|
||||
import 'package:wger/models/exercises/exercise.dart';
|
||||
import 'package:wger/models/exercises/exercise_submission.dart';
|
||||
import 'package:wger/models/exercises/exercise_submission_images.dart';
|
||||
import 'package:wger/models/exercises/language.dart';
|
||||
import 'package:wger/models/exercises/muscle.dart';
|
||||
import 'package:wger/models/exercises/variation.dart';
|
||||
@@ -20,11 +18,10 @@ class AddExerciseProvider with ChangeNotifier {
|
||||
|
||||
AddExerciseProvider(this.baseProvider);
|
||||
|
||||
List<File> get exerciseImages => [..._exerciseImages];
|
||||
List<File> _exerciseImages = [];
|
||||
// Images and their metadata (license info, style)
|
||||
final List<ExerciseSubmissionImage> _exerciseImages = [];
|
||||
|
||||
// Map storing image metadata (license info, style), keyed by file path
|
||||
final Map<String, Map<String, String>> _imageDetails = {};
|
||||
List<ExerciseSubmissionImage> get exerciseImages => [..._exerciseImages];
|
||||
|
||||
String? exerciseNameEn;
|
||||
String? exerciseNameTrans;
|
||||
@@ -37,7 +34,6 @@ class AddExerciseProvider with ChangeNotifier {
|
||||
List<String> alternateNamesEn = [];
|
||||
List<String> alternateNamesTrans = [];
|
||||
ExerciseCategory? category;
|
||||
List<Exercise> _variations = [];
|
||||
List<Equipment> _equipment = [];
|
||||
List<Muscle> _primaryMuscles = [];
|
||||
List<Muscle> _secondaryMuscles = [];
|
||||
@@ -47,8 +43,7 @@ class AddExerciseProvider with ChangeNotifier {
|
||||
static const _checkLanguageUrlPath = 'check-language';
|
||||
|
||||
void clear() {
|
||||
_exerciseImages = [];
|
||||
_imageDetails.clear();
|
||||
_exerciseImages.clear();
|
||||
languageTranslation = null;
|
||||
category = null;
|
||||
exerciseNameEn = null;
|
||||
@@ -57,7 +52,6 @@ class AddExerciseProvider with ChangeNotifier {
|
||||
descriptionTrans = null;
|
||||
alternateNamesEn = [];
|
||||
alternateNamesTrans = [];
|
||||
_variations = [];
|
||||
_equipment = [];
|
||||
_primaryMuscles = [];
|
||||
_secondaryMuscles = [];
|
||||
@@ -142,56 +136,14 @@ class AddExerciseProvider with ChangeNotifier {
|
||||
}
|
||||
|
||||
/// Add images with optional license metadata
|
||||
///
|
||||
/// [images] - List of image files to add
|
||||
/// [title] - License title
|
||||
/// [author] - Author name
|
||||
/// [authorUrl] - Author's URL
|
||||
/// [sourceUrl] - Source/object URL
|
||||
/// [derivativeSourceUrl] - Derivative source URL
|
||||
/// [style] - Image style: 1=PHOTO, 2=3D, 3=LINE, 4=LOW-POLY, 5=OTHER
|
||||
void addExerciseImages(
|
||||
List<File> images, {
|
||||
String? title,
|
||||
String? author,
|
||||
String? authorUrl,
|
||||
String? sourceUrl,
|
||||
String? derivativeSourceUrl,
|
||||
String style = '1',
|
||||
}) {
|
||||
void addExerciseImages(List<ExerciseSubmissionImage> images) {
|
||||
_exerciseImages.addAll(images);
|
||||
|
||||
// Store metadata for each image
|
||||
for (final image in images) {
|
||||
final details = <String, String>{'style': style};
|
||||
|
||||
// Only add non-empty fields
|
||||
if (title != null && title.isNotEmpty) {
|
||||
details['license_title'] = title;
|
||||
}
|
||||
if (author != null && author.isNotEmpty) {
|
||||
details['license_author'] = author;
|
||||
}
|
||||
if (authorUrl != null && authorUrl.isNotEmpty) {
|
||||
details['license_author_url'] = authorUrl;
|
||||
}
|
||||
if (sourceUrl != null && sourceUrl.isNotEmpty) {
|
||||
details['license_object_url'] = sourceUrl;
|
||||
}
|
||||
if (derivativeSourceUrl != null && derivativeSourceUrl.isNotEmpty) {
|
||||
details['license_derivative_source_url'] = derivativeSourceUrl;
|
||||
}
|
||||
|
||||
_imageDetails[image.path] = details;
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void removeExercise(String path) {
|
||||
final file = _exerciseImages.where((element) => element.path == path).first;
|
||||
void removeImage(String path) {
|
||||
final file = _exerciseImages.where((element) => element.imageFile.path == path).first;
|
||||
_exerciseImages.remove(file);
|
||||
_imageDetails.remove(path);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -199,16 +151,14 @@ class AddExerciseProvider with ChangeNotifier {
|
||||
///
|
||||
/// Returns the ID of the created exercise
|
||||
/// Throws exception if submission fails
|
||||
Future<int> addExercise() async {
|
||||
Future<int> postExerciseToServer() async {
|
||||
try {
|
||||
// 1. Create the exercise
|
||||
final exerciseId = await addExerciseSubmission();
|
||||
|
||||
|
||||
// 2. Upload images if any exist
|
||||
if (_exerciseImages.isNotEmpty) {
|
||||
await addImages(exerciseId);
|
||||
|
||||
}
|
||||
|
||||
// 3. Clear all data after successful upload
|
||||
@@ -216,7 +166,6 @@ class AddExerciseProvider with ChangeNotifier {
|
||||
|
||||
return exerciseId;
|
||||
} catch (e) {
|
||||
|
||||
// Don't clear on error so user can retry
|
||||
rethrow;
|
||||
}
|
||||
@@ -233,25 +182,20 @@ class AddExerciseProvider with ChangeNotifier {
|
||||
}
|
||||
|
||||
/// Upload exercise images with license metadata
|
||||
///
|
||||
/// For each image:
|
||||
/// - Sends multipart request with image file
|
||||
/// - Includes license fields from _imageDetails map
|
||||
Future<void> addImages(int exerciseId) async {
|
||||
for (final image in _exerciseImages) {
|
||||
final request = http.MultipartRequest('POST', baseProvider.makeUrl(_imagesUrlPath));
|
||||
request.headers.addAll(baseProvider.getDefaultHeaders(includeAuth: true));
|
||||
|
||||
request.files.add(await http.MultipartFile.fromPath('image', image.path));
|
||||
request.files.add(await http.MultipartFile.fromPath('image', image.imageFile.path));
|
||||
request.fields['exercise'] = exerciseId.toString();
|
||||
request.fields['license'] = '1';
|
||||
request.fields['license'] = CC_BY_SA_4_ID.toString();
|
||||
request.fields['is_main'] = 'false';
|
||||
request.fields['style'] = ImageType.photo.id.toString();
|
||||
|
||||
final details = _imageDetails[image.path];
|
||||
if (details != null && details.isNotEmpty) {
|
||||
final details = image.toJson();
|
||||
if (details.isNotEmpty) {
|
||||
request.fields.addAll(details);
|
||||
} else {
|
||||
request.fields['style'] = '1';
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -280,4 +224,4 @@ class AddExerciseProvider with ChangeNotifier {
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,10 +85,8 @@ class _AddExerciseStepperState extends State<AddExerciseStepper> {
|
||||
|
||||
Exercise? exercise;
|
||||
try {
|
||||
final exerciseId = await addExerciseProvider.addExercise();
|
||||
await addExerciseProvider.addImages(exerciseId);
|
||||
final exerciseId = await addExerciseProvider.postExerciseToServer();
|
||||
exercise = await exerciseProvider.fetchAndSetExercise(exerciseId);
|
||||
addExerciseProvider.clear();
|
||||
} on WgerHttpException catch (error) {
|
||||
if (context.mounted) {
|
||||
setState(() {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:wger/l10n/generated/app_localizations.dart';
|
||||
import 'package:wger/models/exercises/exercise_submission_images.dart';
|
||||
import 'package:wger/widgets/add_exercise/license_info_widget.dart';
|
||||
|
||||
/// Form for collecting CC BY-SA 4.0 license metadata for exercise images
|
||||
@@ -23,13 +22,13 @@ import 'package:wger/widgets/add_exercise/license_info_widget.dart';
|
||||
/// 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;
|
||||
final Function(ExerciseSubmissionImage image) onAdd;
|
||||
final VoidCallback onCancel;
|
||||
final ExerciseSubmissionImage submissionImage;
|
||||
|
||||
const ImageDetailsForm({
|
||||
super.key,
|
||||
required this.imageFile,
|
||||
required this.submissionImage,
|
||||
required this.onAdd,
|
||||
required this.onCancel,
|
||||
});
|
||||
@@ -43,22 +42,13 @@ class _ImageDetailsFormState extends State<ImageDetailsForm> {
|
||||
|
||||
// Text controllers for license metadata fields
|
||||
final _titleController = 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
|
||||
final _sourceLinkController = TextEditingController();
|
||||
final _authorController = TextEditingController();
|
||||
final _authorLinkController = TextEditingController();
|
||||
final _originalSourceController = TextEditingController();
|
||||
|
||||
/// 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 = [
|
||||
{'type': 'PHOTO', 'icon': Icons.photo_camera, 'label': 'PHOTO'},
|
||||
{'type': '3D', 'icon': Icons.view_in_ar, 'label': '3D'},
|
||||
{'type': 'LINE', 'icon': Icons.show_chart, 'label': 'LINE'},
|
||||
{'type': 'LOW-POLY', 'icon': Icons.filter_vintage, 'label': 'LOW-POLY'},
|
||||
{'type': 'OTHER', 'icon': Icons.more_horiz, 'label': 'OTHER'},
|
||||
];
|
||||
ImageType _selectedImageType = ImageType.photo;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
@@ -216,7 +206,7 @@ class _ImageDetailsFormState extends State<ImageDetailsForm> {
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Image.file(widget.imageFile, fit: BoxFit.contain),
|
||||
child: Image.file(widget.submissionImage.imageFile, fit: BoxFit.contain),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -269,12 +259,12 @@ class _ImageDetailsFormState extends State<ImageDetailsForm> {
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: _imageTypes.map((type) {
|
||||
final isSelected = _selectedImageType == type['type'];
|
||||
children: ImageType.values.map((type) {
|
||||
final isSelected = _selectedImageType == type;
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_selectedImageType = type['type'];
|
||||
_selectedImageType = type;
|
||||
});
|
||||
},
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
@@ -291,7 +281,7 @@ class _ImageDetailsFormState extends State<ImageDetailsForm> {
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
type['icon'],
|
||||
type.icon,
|
||||
size: 32,
|
||||
color: isSelected
|
||||
? theme.buttonTheme.colorScheme!.onPrimary
|
||||
@@ -299,7 +289,7 @@ class _ImageDetailsFormState extends State<ImageDetailsForm> {
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
type['label'],
|
||||
type.label,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: isSelected ? Colors.blue : Colors.grey.shade700,
|
||||
@@ -328,40 +318,42 @@ class _ImageDetailsFormState extends State<ImageDetailsForm> {
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton(
|
||||
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 optional fields only if user provided values
|
||||
final title = _titleController.text.trim();
|
||||
if (title.isNotEmpty) {
|
||||
details['license_title'] = title;
|
||||
}
|
||||
|
||||
final author = _authorController.text.trim();
|
||||
if (author.isNotEmpty) {
|
||||
details['license_author'] = author;
|
||||
}
|
||||
|
||||
final sourceUrl = _sourceLinkController.text.trim();
|
||||
if (sourceUrl.isNotEmpty) {
|
||||
details['license_object_url'] = sourceUrl;
|
||||
}
|
||||
|
||||
final authorUrl = _authorLinkController.text.trim();
|
||||
if (authorUrl.isNotEmpty) {
|
||||
details['license_author_url'] = authorUrl;
|
||||
}
|
||||
|
||||
final derivativeUrl = _originalSourceController.text.trim();
|
||||
if (derivativeUrl.isNotEmpty) {
|
||||
details['license_derivative_source_url'] = derivativeUrl;
|
||||
}
|
||||
|
||||
// Pass image and metadata back to parent
|
||||
widget.onAdd(widget.imageFile, details);
|
||||
if (!_formKey.currentState!.validate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Build details map with API field names
|
||||
// Style is always included, other fields only if non-empty
|
||||
final details = <String, String>{'style': _getStyleValue()};
|
||||
|
||||
// Add optional fields only if user provided values
|
||||
final title = _titleController.text.trim();
|
||||
if (title.isNotEmpty) {
|
||||
widget.submissionImage.title = title;
|
||||
}
|
||||
|
||||
final author = _authorController.text.trim();
|
||||
if (author.isNotEmpty) {
|
||||
widget.submissionImage.author = author;
|
||||
}
|
||||
|
||||
final sourceUrl = _sourceLinkController.text.trim();
|
||||
if (sourceUrl.isNotEmpty) {
|
||||
widget.submissionImage.sourceUrl = sourceUrl;
|
||||
}
|
||||
|
||||
final authorUrl = _authorLinkController.text.trim();
|
||||
if (authorUrl.isNotEmpty) {
|
||||
widget.submissionImage.authorUrl = authorUrl;
|
||||
}
|
||||
|
||||
final derivativeUrl = _originalSourceController.text.trim();
|
||||
if (derivativeUrl.isNotEmpty) {
|
||||
widget.submissionImage.derivativeSourceUrl = derivativeUrl;
|
||||
}
|
||||
|
||||
// Pass image and metadata back to parent
|
||||
widget.onAdd(widget.submissionImage);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:wger/models/exercises/exercise_submission_images.dart';
|
||||
import 'package:wger/providers/add_exercise.dart';
|
||||
|
||||
const validFileExtensions = ['jpg', 'jpeg', 'png', 'webp'];
|
||||
@@ -31,19 +32,21 @@ mixin ExerciseImagePickerMixin {
|
||||
images = await imagePicker.pickMultiImage();
|
||||
}
|
||||
|
||||
final selectedImages = <File>[];
|
||||
final selectedImages = <ExerciseSubmissionImage>[];
|
||||
if (images != null) {
|
||||
selectedImages.addAll(images.map((e) => File(e.path)).toList());
|
||||
selectedImages.addAll(
|
||||
images.map((e) => ExerciseSubmissionImage(imageFile: File(e.path))).toList(),
|
||||
);
|
||||
|
||||
for (final image in selectedImages) {
|
||||
bool isFileValid = true;
|
||||
String errorMessage = '';
|
||||
|
||||
if (!_validateFileType(image)) {
|
||||
if (!_validateFileType(image.imageFile)) {
|
||||
isFileValid = false;
|
||||
errorMessage = "Select only 'jpg', 'jpeg', 'png', 'webp' files";
|
||||
}
|
||||
if (_validateFileSize(image.lengthSync())) {
|
||||
if (_validateFileSize(image.imageFile.lengthSync())) {
|
||||
isFileValid = true;
|
||||
errorMessage = 'File Size should not be greater than 20 mb';
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:wger/models/exercises/exercise_submission_images.dart';
|
||||
import 'package:wger/providers/add_exercise.dart';
|
||||
|
||||
/// Widget to preview selected exercise images
|
||||
@@ -9,16 +11,16 @@ import 'package:wger/providers/add_exercise.dart';
|
||||
/// 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 List<ExerciseSubmissionImage> selectedImages;
|
||||
final VoidCallback? onAddMore;
|
||||
final bool allowEdit;
|
||||
|
||||
const PreviewExerciseImages({
|
||||
Key? key,
|
||||
super.key,
|
||||
required this.selectedImages,
|
||||
this.onAddMore,
|
||||
this.allowEdit = true,
|
||||
}) : super(key: key);
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -38,7 +40,7 @@ class PreviewExerciseImages extends StatelessWidget {
|
||||
|
||||
// Show image thumbnail
|
||||
final image = selectedImages[index];
|
||||
return _buildImageCard(context, image);
|
||||
return _buildImageCard(context, image.imageFile);
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -53,12 +55,7 @@ class PreviewExerciseImages extends StatelessWidget {
|
||||
// Image thumbnail
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Image.file(
|
||||
image,
|
||||
width: 120,
|
||||
height: 120,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
child: Image.file(image, width: 120, height: 120, fit: BoxFit.cover),
|
||||
),
|
||||
|
||||
// Delete button overlay (only shown if editing is allowed)
|
||||
@@ -75,7 +72,7 @@ class PreviewExerciseImages extends StatelessWidget {
|
||||
padding: const EdgeInsets.all(4),
|
||||
),
|
||||
onPressed: () {
|
||||
context.read<AddExerciseProvider>().removeExercise(image.path);
|
||||
context.read<AddExerciseProvider>().removeImage(image.path);
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -100,12 +97,8 @@ class PreviewExerciseImages extends StatelessWidget {
|
||||
style: BorderStyle.solid,
|
||||
),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.add,
|
||||
size: 48,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
child: Icon(Icons.add, size: 48, color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:wger/l10n/generated/app_localizations.dart';
|
||||
import 'package:wger/models/exercises/exercise_submission_images.dart';
|
||||
import 'package:wger/providers/add_exercise.dart';
|
||||
import 'package:wger/widgets/add_exercise/image_details_form.dart';
|
||||
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
|
||||
///
|
||||
@@ -21,6 +23,7 @@ import 'package:wger/widgets/add_exercise/image_details_form.dart';
|
||||
/// 4. Final upload happens in Step 6 when user clicks "Submit"
|
||||
class Step5Images extends StatefulWidget {
|
||||
final GlobalKey<FormState> formkey;
|
||||
|
||||
const Step5Images({required this.formkey});
|
||||
|
||||
@override
|
||||
@@ -30,7 +33,7 @@ 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;
|
||||
ExerciseSubmissionImage? _currentImageToAddNew;
|
||||
|
||||
/// Show dialog to choose between Camera and Gallery
|
||||
Future<void> _showImageSourceDialog(BuildContext context) async {
|
||||
@@ -111,7 +114,7 @@ class _Step5ImagesState extends State<Step5Images> with ExerciseImagePickerMixin
|
||||
|
||||
// Show metadata collection form for valid image
|
||||
setState(() {
|
||||
_currentImageToAdd = imageFile;
|
||||
_currentImageToAddNew = ExerciseSubmissionImage(imageFile: imageFile);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -124,30 +127,22 @@ class _Step5ImagesState extends State<Step5Images> with ExerciseImagePickerMixin
|
||||
///
|
||||
/// [image] - The image file to add
|
||||
/// [details] - Map containing license fields (license_title, license_author, etc.)
|
||||
void _addImageWithDetails(File image, Map<String, String> details) {
|
||||
void _addImageWithDetails(ExerciseSubmissionImage image) {
|
||||
final provider = context.read<AddExerciseProvider>();
|
||||
|
||||
// Store image with metadata - actual upload happens in addExercise()
|
||||
provider.addExerciseImages(
|
||||
[image],
|
||||
title: details['license_title'],
|
||||
author: details['license_author'],
|
||||
authorUrl: details['license_author_url'],
|
||||
sourceUrl: details['license_object_url'],
|
||||
derivativeSourceUrl: details['license_derivative_source_url'],
|
||||
style: details['style'] ?? '1',
|
||||
);
|
||||
provider.addExerciseImages([image]);
|
||||
|
||||
// Reset form state - image is now visible in preview list
|
||||
setState(() {
|
||||
_currentImageToAdd = null;
|
||||
_currentImageToAddNew = null;
|
||||
});
|
||||
}
|
||||
|
||||
/// Cancel metadata input and return to image picker
|
||||
void _cancelImageAdd() {
|
||||
setState(() {
|
||||
_currentImageToAdd = null;
|
||||
_currentImageToAddNew = null;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -158,7 +153,7 @@ class _Step5ImagesState extends State<Step5Images> with ExerciseImagePickerMixin
|
||||
child: Column(
|
||||
children: [
|
||||
// License notice - shown when not entering metadata
|
||||
if (_currentImageToAdd == null)
|
||||
if (_currentImageToAddNew == null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||
child: Text(
|
||||
@@ -169,15 +164,15 @@ class _Step5ImagesState extends State<Step5Images> with ExerciseImagePickerMixin
|
||||
),
|
||||
|
||||
// Metadata collection form - shown when image is selected
|
||||
if (_currentImageToAdd != null)
|
||||
if (_currentImageToAddNew != null)
|
||||
ImageDetailsForm(
|
||||
imageFile: _currentImageToAdd!,
|
||||
submissionImage: _currentImageToAddNew!,
|
||||
onAdd: _addImageWithDetails,
|
||||
onCancel: _cancelImageAdd,
|
||||
),
|
||||
|
||||
// Image picker or preview - shown when not entering metadata
|
||||
if (_currentImageToAdd == null)
|
||||
if (_currentImageToAddNew == null)
|
||||
Consumer<AddExerciseProvider>(
|
||||
builder: (ctx, provider, __) {
|
||||
if (provider.exerciseImages.isNotEmpty) {
|
||||
@@ -253,4 +248,4 @@ class _Step5ImagesState extends State<Step5Images> with ExerciseImagePickerMixin
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:wger/providers/add_exercise.dart';
|
||||
|
||||
import '../core/settings_test.mocks.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mockito/annotations.dart';
|
||||
import 'package:wger/models/exercises/exercise_submission_images.dart';
|
||||
import 'package:wger/providers/add_exercise.dart';
|
||||
import 'package:wger/providers/base_provider.dart';
|
||||
|
||||
import 'contribute_exercise_image_test.mocks.dart';
|
||||
|
||||
/// Unit tests for AddExerciseProvider image metadata handling
|
||||
///
|
||||
@@ -14,6 +18,8 @@ import '../core/settings_test.mocks.dart';
|
||||
/// - Edge cases (empty lists, duplicates, special characters)
|
||||
/// - State management (clear, remove)
|
||||
/// - Image ordering and batch operations
|
||||
|
||||
@GenerateMocks([AddExerciseProvider, WgerBaseProvider])
|
||||
void main() {
|
||||
late MockWgerBaseProvider mockBaseProvider;
|
||||
late AddExerciseProvider provider;
|
||||
@@ -29,18 +35,20 @@ void main() {
|
||||
test('should store image with all license fields', () {
|
||||
final mockFile = File('test.jpg');
|
||||
|
||||
provider.addExerciseImages(
|
||||
[mockFile],
|
||||
title: 'Test Title',
|
||||
author: 'Test Author',
|
||||
authorUrl: 'https://test.com/author',
|
||||
sourceUrl: 'https://source.com',
|
||||
derivativeSourceUrl: 'https://derivative.com',
|
||||
style: '4', // LOW-POLY
|
||||
);
|
||||
provider.addExerciseImages([
|
||||
ExerciseSubmissionImage(
|
||||
imageFile: mockFile,
|
||||
title: 'Test Title',
|
||||
author: 'Test Author',
|
||||
authorUrl: 'https://test.com/author',
|
||||
sourceUrl: 'https://source.com',
|
||||
derivativeSourceUrl: 'https://derivative.com',
|
||||
type: ImageType.lowPoly,
|
||||
),
|
||||
]);
|
||||
|
||||
expect(provider.exerciseImages.length, 1);
|
||||
expect(provider.exerciseImages.first.path, mockFile.path);
|
||||
expect(provider.exerciseImages.first.imageFile.path, mockFile.path);
|
||||
});
|
||||
|
||||
/// License fields are optional - provider should handle null/empty values
|
||||
@@ -48,13 +56,9 @@ void main() {
|
||||
test('should handle empty fields gracefully', () {
|
||||
final mockFile = File('test2.jpg');
|
||||
|
||||
provider.addExerciseImages(
|
||||
[mockFile],
|
||||
title: 'Only Title',
|
||||
author: null, // null value
|
||||
authorUrl: '', // empty string
|
||||
style: '2', // 3D
|
||||
);
|
||||
provider.addExerciseImages([
|
||||
ExerciseSubmissionImage(imageFile: mockFile, title: 'Only Title', type: ImageType.threeD),
|
||||
]);
|
||||
|
||||
expect(provider.exerciseImages.length, 1);
|
||||
});
|
||||
@@ -65,33 +69,12 @@ void main() {
|
||||
final file1 = File('image1.jpg');
|
||||
final file2 = File('image2.jpg');
|
||||
|
||||
provider.addExerciseImages([file1], title: 'Image 1', author: 'Author 1', style: '1');
|
||||
provider.addExerciseImages([file2], title: 'Image 2', author: 'Author 2', style: '2');
|
||||
provider.addExerciseImages([ExerciseSubmissionImage(imageFile: file1, author: 'Author 1')]);
|
||||
provider.addExerciseImages([ExerciseSubmissionImage(imageFile: file2, author: 'Author 1')]);
|
||||
|
||||
expect(provider.exerciseImages.length, 2);
|
||||
expect(provider.exerciseImages[0].path, file1.path);
|
||||
expect(provider.exerciseImages[1].path, file2.path);
|
||||
});
|
||||
|
||||
/// Test all 5 image style types defined by the API
|
||||
/// 1=PHOTO, 2=3D, 3=LINE, 4=LOW-POLY, 5=OTHER
|
||||
test('should handle all image style types', () {
|
||||
final styles = ['1', '2', '3', '4', '5'];
|
||||
|
||||
for (var i = 0; i < styles.length; i++) {
|
||||
provider.addExerciseImages([File('image_$i.jpg')], style: styles[i]);
|
||||
}
|
||||
|
||||
expect(provider.exerciseImages.length, 5);
|
||||
});
|
||||
|
||||
/// If no style is specified, should default to '1' (PHOTO)
|
||||
test('should use default style when not specified', () {
|
||||
final mockFile = File('default.jpg');
|
||||
|
||||
provider.addExerciseImages([mockFile]);
|
||||
|
||||
expect(provider.exerciseImages.length, 1);
|
||||
expect(provider.exerciseImages[0].imageFile.path, file1.path);
|
||||
expect(provider.exerciseImages[1].imageFile.path, file2.path);
|
||||
});
|
||||
|
||||
/// Edge case: calling addExerciseImages with empty list should not crash
|
||||
@@ -101,26 +84,15 @@ void main() {
|
||||
expect(provider.exerciseImages.length, 0);
|
||||
});
|
||||
|
||||
/// Allows adding the same file multiple times with different metadata
|
||||
/// (e.g., different crops or edits of the same original image)
|
||||
test('should handle adding same file multiple times', () {
|
||||
final mockFile = File('same.jpg');
|
||||
|
||||
provider.addExerciseImages([mockFile], title: 'First');
|
||||
provider.addExerciseImages([mockFile], title: 'Second');
|
||||
|
||||
expect(provider.exerciseImages.length, 2);
|
||||
});
|
||||
|
||||
/// Removing an image should also remove its associated metadata
|
||||
/// to prevent memory leaks
|
||||
test('should remove image and its metadata', () {
|
||||
final mockFile = File('to_remove.jpg');
|
||||
provider.addExerciseImages([mockFile], title: 'Will be removed', style: '1');
|
||||
provider.addExerciseImages([ExerciseSubmissionImage(imageFile: mockFile)]);
|
||||
|
||||
expect(provider.exerciseImages.length, 1);
|
||||
|
||||
provider.removeExercise(mockFile.path);
|
||||
provider.removeImage(mockFile.path);
|
||||
|
||||
expect(provider.exerciseImages.length, 0);
|
||||
});
|
||||
@@ -128,13 +100,15 @@ void main() {
|
||||
/// Attempting to remove a non-existent image should throw StateError
|
||||
/// (from firstWhere with no orElse)
|
||||
test('should handle removing non-existent image gracefully', () {
|
||||
expect(() => provider.removeExercise('nonexistent.jpg'), throwsStateError);
|
||||
expect(() => provider.removeImage('nonexistent.jpg'), throwsStateError);
|
||||
});
|
||||
|
||||
/// clear() should reset all state including images and metadata
|
||||
test('should clear all images and metadata', () {
|
||||
provider.addExerciseImages([File('image1.jpg')], title: 'Image 1');
|
||||
provider.addExerciseImages([File('image2.jpg')], title: 'Image 2');
|
||||
provider.addExerciseImages([
|
||||
ExerciseSubmissionImage(imageFile: File('image1.jpg'), title: 'Image 1'),
|
||||
ExerciseSubmissionImage(imageFile: File('image2.jpg'), title: 'Image 2'),
|
||||
]);
|
||||
|
||||
expect(provider.exerciseImages.length, 2);
|
||||
|
||||
@@ -152,49 +126,23 @@ void main() {
|
||||
expect(provider.exerciseImages.length, 0);
|
||||
});
|
||||
|
||||
/// Images should be stored in the order they were added
|
||||
/// Important for display consistency
|
||||
test('should preserve image order', () {
|
||||
final file1 = File('first.jpg');
|
||||
final file2 = File('second.jpg');
|
||||
final file3 = File('third.jpg');
|
||||
|
||||
provider.addExerciseImages([file1]);
|
||||
provider.addExerciseImages([file2]);
|
||||
provider.addExerciseImages([file3]);
|
||||
|
||||
expect(provider.exerciseImages[0].path, 'first.jpg');
|
||||
expect(provider.exerciseImages[1].path, 'second.jpg');
|
||||
expect(provider.exerciseImages[2].path, 'third.jpg');
|
||||
});
|
||||
|
||||
/// Multiple images can be added in a single call with shared metadata
|
||||
/// Useful for bulk uploads from the same source
|
||||
test('should handle batch adding multiple images at once', () {
|
||||
final files = [File('batch1.jpg'), File('batch2.jpg'), File('batch3.jpg')];
|
||||
|
||||
provider.addExerciseImages(files, title: 'Batch Upload', style: '3');
|
||||
|
||||
expect(provider.exerciseImages.length, 3);
|
||||
});
|
||||
|
||||
/// Removing one image from a set should not affect others
|
||||
test('should allow removing specific image from multiple images', () {
|
||||
final file1 = File('keep1.jpg');
|
||||
final file2 = File('remove.jpg');
|
||||
final file3 = File('keep2.jpg');
|
||||
|
||||
provider.addExerciseImages([file1]);
|
||||
provider.addExerciseImages([file2]);
|
||||
provider.addExerciseImages([file3]);
|
||||
provider.addExerciseImages([ExerciseSubmissionImage(imageFile: file1)]);
|
||||
provider.addExerciseImages([ExerciseSubmissionImage(imageFile: file2)]);
|
||||
provider.addExerciseImages([ExerciseSubmissionImage(imageFile: file3)]);
|
||||
|
||||
expect(provider.exerciseImages.length, 3);
|
||||
|
||||
provider.removeExercise(file2.path);
|
||||
provider.removeImage(file2.path);
|
||||
|
||||
expect(provider.exerciseImages.length, 2);
|
||||
expect(provider.exerciseImages[0].path, file1.path);
|
||||
expect(provider.exerciseImages[1].path, file3.path);
|
||||
expect(provider.exerciseImages[0].imageFile.path, file1.path);
|
||||
expect(provider.exerciseImages[1].imageFile.path, file3.path);
|
||||
});
|
||||
|
||||
/// Test with extremely long strings (1000 chars) to ensure no
|
||||
@@ -203,12 +151,14 @@ void main() {
|
||||
final mockFile = File('long.jpg');
|
||||
final longString = 'a' * 1000;
|
||||
|
||||
provider.addExerciseImages(
|
||||
[mockFile],
|
||||
title: longString,
|
||||
author: longString,
|
||||
authorUrl: 'https://example.com/$longString',
|
||||
);
|
||||
provider.addExerciseImages([
|
||||
ExerciseSubmissionImage(
|
||||
imageFile: mockFile,
|
||||
title: longString,
|
||||
author: longString,
|
||||
authorUrl: 'https://example.com/$longString',
|
||||
),
|
||||
]);
|
||||
|
||||
expect(provider.exerciseImages.length, 1);
|
||||
});
|
||||
@@ -218,12 +168,14 @@ void main() {
|
||||
test('should handle special characters in metadata', () {
|
||||
final mockFile = File('special.jpg');
|
||||
|
||||
provider.addExerciseImages(
|
||||
[mockFile],
|
||||
title: 'Title with émojis 🎉 and spëcial çhars',
|
||||
author: 'Autör with ümlauts',
|
||||
authorUrl: 'https://example.com/path?query=value&another=value',
|
||||
);
|
||||
provider.addExerciseImages([
|
||||
ExerciseSubmissionImage(
|
||||
imageFile: mockFile,
|
||||
title: 'Title with émojis 🎉 and spëcial çhars',
|
||||
author: 'Autör with ümlauts',
|
||||
authorUrl: 'https://example.com/path?query=value&another=value',
|
||||
),
|
||||
]);
|
||||
|
||||
expect(provider.exerciseImages.length, 1);
|
||||
});
|
||||
@@ -235,7 +187,7 @@ void main() {
|
||||
test('should reset all state after clear', () {
|
||||
provider.exerciseNameEn = 'Test Exercise';
|
||||
provider.descriptionEn = 'Description';
|
||||
provider.addExerciseImages([File('test.jpg')]);
|
||||
provider.addExerciseImages([ExerciseSubmissionImage(imageFile: File('test.jpg'))]);
|
||||
|
||||
provider.clear();
|
||||
|
||||
383
test/exercises/contribute_exercise_image_test.mocks.dart
Normal file
383
test/exercises/contribute_exercise_image_test.mocks.dart
Normal file
@@ -0,0 +1,383 @@
|
||||
// Mocks generated by Mockito 5.4.6 from annotations
|
||||
// in wger/test/exercises/contribute_exercise_image_test.dart.
|
||||
// Do not manually edit this file.
|
||||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'dart:async' as _i14;
|
||||
import 'dart:ui' as _i15;
|
||||
|
||||
import 'package:http/http.dart' as _i5;
|
||||
import 'package:mockito/mockito.dart' as _i1;
|
||||
import 'package:mockito/src/dummies.dart' as _i11;
|
||||
import 'package:wger/models/exercises/category.dart' as _i13;
|
||||
import 'package:wger/models/exercises/equipment.dart' as _i8;
|
||||
import 'package:wger/models/exercises/exercise_submission.dart' as _i10;
|
||||
import 'package:wger/models/exercises/exercise_submission_images.dart' as _i7;
|
||||
import 'package:wger/models/exercises/language.dart' as _i12;
|
||||
import 'package:wger/models/exercises/muscle.dart' as _i9;
|
||||
import 'package:wger/models/exercises/variation.dart' as _i3;
|
||||
import 'package:wger/providers/add_exercise.dart' as _i6;
|
||||
import 'package:wger/providers/auth.dart' as _i4;
|
||||
import 'package:wger/providers/base_provider.dart' as _i2;
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: avoid_redundant_argument_values
|
||||
// ignore_for_file: avoid_setters_without_getters
|
||||
// ignore_for_file: comment_references
|
||||
// ignore_for_file: deprecated_member_use
|
||||
// ignore_for_file: deprecated_member_use_from_same_package
|
||||
// ignore_for_file: implementation_imports
|
||||
// ignore_for_file: invalid_use_of_visible_for_testing_member
|
||||
// ignore_for_file: must_be_immutable
|
||||
// ignore_for_file: prefer_const_constructors
|
||||
// ignore_for_file: unnecessary_parenthesis
|
||||
// ignore_for_file: camel_case_types
|
||||
// ignore_for_file: subtype_of_sealed_class
|
||||
// ignore_for_file: invalid_use_of_internal_member
|
||||
|
||||
class _FakeWgerBaseProvider_0 extends _i1.SmartFake implements _i2.WgerBaseProvider {
|
||||
_FakeWgerBaseProvider_0(Object parent, Invocation parentInvocation)
|
||||
: super(parent, parentInvocation);
|
||||
}
|
||||
|
||||
class _FakeVariation_1 extends _i1.SmartFake implements _i3.Variation {
|
||||
_FakeVariation_1(Object parent, Invocation parentInvocation) : super(parent, parentInvocation);
|
||||
}
|
||||
|
||||
class _FakeAuthProvider_2 extends _i1.SmartFake implements _i4.AuthProvider {
|
||||
_FakeAuthProvider_2(Object parent, Invocation parentInvocation) : super(parent, parentInvocation);
|
||||
}
|
||||
|
||||
class _FakeClient_3 extends _i1.SmartFake implements _i5.Client {
|
||||
_FakeClient_3(Object parent, Invocation parentInvocation) : super(parent, parentInvocation);
|
||||
}
|
||||
|
||||
class _FakeUri_4 extends _i1.SmartFake implements Uri {
|
||||
_FakeUri_4(Object parent, Invocation parentInvocation) : super(parent, parentInvocation);
|
||||
}
|
||||
|
||||
class _FakeResponse_5 extends _i1.SmartFake implements _i5.Response {
|
||||
_FakeResponse_5(Object parent, Invocation parentInvocation) : super(parent, parentInvocation);
|
||||
}
|
||||
|
||||
/// A class which mocks [AddExerciseProvider].
|
||||
///
|
||||
/// See the documentation for Mockito's code generation for more information.
|
||||
class MockAddExerciseProvider extends _i1.Mock implements _i6.AddExerciseProvider {
|
||||
MockAddExerciseProvider() {
|
||||
_i1.throwOnMissingStub(this);
|
||||
}
|
||||
|
||||
@override
|
||||
_i2.WgerBaseProvider get baseProvider =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.getter(#baseProvider),
|
||||
returnValue: _FakeWgerBaseProvider_0(this, Invocation.getter(#baseProvider)),
|
||||
)
|
||||
as _i2.WgerBaseProvider);
|
||||
|
||||
@override
|
||||
List<_i7.ExerciseSubmissionImage> get exerciseImages =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.getter(#exerciseImages),
|
||||
returnValue: <_i7.ExerciseSubmissionImage>[],
|
||||
)
|
||||
as List<_i7.ExerciseSubmissionImage>);
|
||||
|
||||
@override
|
||||
List<String> get alternateNamesEn =>
|
||||
(super.noSuchMethod(Invocation.getter(#alternateNamesEn), returnValue: <String>[])
|
||||
as List<String>);
|
||||
|
||||
@override
|
||||
List<String> get alternateNamesTrans =>
|
||||
(super.noSuchMethod(Invocation.getter(#alternateNamesTrans), returnValue: <String>[])
|
||||
as List<String>);
|
||||
|
||||
@override
|
||||
List<_i8.Equipment> get equipment =>
|
||||
(super.noSuchMethod(Invocation.getter(#equipment), returnValue: <_i8.Equipment>[])
|
||||
as List<_i8.Equipment>);
|
||||
|
||||
@override
|
||||
bool get newVariation =>
|
||||
(super.noSuchMethod(Invocation.getter(#newVariation), returnValue: false) as bool);
|
||||
|
||||
@override
|
||||
_i3.Variation get variation =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.getter(#variation),
|
||||
returnValue: _FakeVariation_1(this, Invocation.getter(#variation)),
|
||||
)
|
||||
as _i3.Variation);
|
||||
|
||||
@override
|
||||
List<_i9.Muscle> get primaryMuscles =>
|
||||
(super.noSuchMethod(Invocation.getter(#primaryMuscles), returnValue: <_i9.Muscle>[])
|
||||
as List<_i9.Muscle>);
|
||||
|
||||
@override
|
||||
List<_i9.Muscle> get secondaryMuscles =>
|
||||
(super.noSuchMethod(Invocation.getter(#secondaryMuscles), returnValue: <_i9.Muscle>[])
|
||||
as List<_i9.Muscle>);
|
||||
|
||||
@override
|
||||
_i10.ExerciseSubmissionApi get exerciseApiObject =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.getter(#exerciseApiObject),
|
||||
returnValue: _i11.dummyValue<_i10.ExerciseSubmissionApi>(
|
||||
this,
|
||||
Invocation.getter(#exerciseApiObject),
|
||||
),
|
||||
)
|
||||
as _i10.ExerciseSubmissionApi);
|
||||
|
||||
@override
|
||||
set exerciseNameEn(String? value) => super.noSuchMethod(
|
||||
Invocation.setter(#exerciseNameEn, value),
|
||||
returnValueForMissingStub: null,
|
||||
);
|
||||
|
||||
@override
|
||||
set exerciseNameTrans(String? value) => super.noSuchMethod(
|
||||
Invocation.setter(#exerciseNameTrans, value),
|
||||
returnValueForMissingStub: null,
|
||||
);
|
||||
|
||||
@override
|
||||
set descriptionEn(String? value) =>
|
||||
super.noSuchMethod(Invocation.setter(#descriptionEn, value), returnValueForMissingStub: null);
|
||||
|
||||
@override
|
||||
set descriptionTrans(String? value) => super.noSuchMethod(
|
||||
Invocation.setter(#descriptionTrans, value),
|
||||
returnValueForMissingStub: null,
|
||||
);
|
||||
|
||||
@override
|
||||
set languageEn(_i12.Language? value) =>
|
||||
super.noSuchMethod(Invocation.setter(#languageEn, value), returnValueForMissingStub: null);
|
||||
|
||||
@override
|
||||
set languageTranslation(_i12.Language? value) => super.noSuchMethod(
|
||||
Invocation.setter(#languageTranslation, value),
|
||||
returnValueForMissingStub: null,
|
||||
);
|
||||
|
||||
@override
|
||||
set alternateNamesEn(List<String>? value) => super.noSuchMethod(
|
||||
Invocation.setter(#alternateNamesEn, value),
|
||||
returnValueForMissingStub: null,
|
||||
);
|
||||
|
||||
@override
|
||||
set alternateNamesTrans(List<String>? value) => super.noSuchMethod(
|
||||
Invocation.setter(#alternateNamesTrans, value),
|
||||
returnValueForMissingStub: null,
|
||||
);
|
||||
|
||||
@override
|
||||
set category(_i13.ExerciseCategory? value) =>
|
||||
super.noSuchMethod(Invocation.setter(#category, value), returnValueForMissingStub: null);
|
||||
|
||||
@override
|
||||
set equipment(List<_i8.Equipment>? equipment) =>
|
||||
super.noSuchMethod(Invocation.setter(#equipment, equipment), returnValueForMissingStub: null);
|
||||
|
||||
@override
|
||||
set variationConnectToExercise(int? value) => super.noSuchMethod(
|
||||
Invocation.setter(#variationConnectToExercise, value),
|
||||
returnValueForMissingStub: null,
|
||||
);
|
||||
|
||||
@override
|
||||
set variationId(int? variation) => super.noSuchMethod(
|
||||
Invocation.setter(#variationId, variation),
|
||||
returnValueForMissingStub: null,
|
||||
);
|
||||
|
||||
@override
|
||||
set primaryMuscles(List<_i9.Muscle>? muscles) => super.noSuchMethod(
|
||||
Invocation.setter(#primaryMuscles, muscles),
|
||||
returnValueForMissingStub: null,
|
||||
);
|
||||
|
||||
@override
|
||||
set secondaryMuscles(List<_i9.Muscle>? muscles) => super.noSuchMethod(
|
||||
Invocation.setter(#secondaryMuscles, muscles),
|
||||
returnValueForMissingStub: null,
|
||||
);
|
||||
|
||||
@override
|
||||
bool get hasListeners =>
|
||||
(super.noSuchMethod(Invocation.getter(#hasListeners), returnValue: false) as bool);
|
||||
|
||||
@override
|
||||
void clear() =>
|
||||
super.noSuchMethod(Invocation.method(#clear, []), returnValueForMissingStub: null);
|
||||
|
||||
@override
|
||||
void addExerciseImages(List<_i7.ExerciseSubmissionImage>? images) => super.noSuchMethod(
|
||||
Invocation.method(#addExerciseImages, [images]),
|
||||
returnValueForMissingStub: null,
|
||||
);
|
||||
|
||||
@override
|
||||
void removeImage(String? path) =>
|
||||
super.noSuchMethod(Invocation.method(#removeImage, [path]), returnValueForMissingStub: null);
|
||||
|
||||
@override
|
||||
_i14.Future<int> postExerciseToServer() =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#postExerciseToServer, []),
|
||||
returnValue: _i14.Future<int>.value(0),
|
||||
)
|
||||
as _i14.Future<int>);
|
||||
|
||||
@override
|
||||
_i14.Future<int> addExerciseSubmission() =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#addExerciseSubmission, []),
|
||||
returnValue: _i14.Future<int>.value(0),
|
||||
)
|
||||
as _i14.Future<int>);
|
||||
|
||||
@override
|
||||
_i14.Future<void> addImages(int? exerciseId) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#addImages, [exerciseId]),
|
||||
returnValue: _i14.Future<void>.value(),
|
||||
returnValueForMissingStub: _i14.Future<void>.value(),
|
||||
)
|
||||
as _i14.Future<void>);
|
||||
|
||||
@override
|
||||
_i14.Future<bool> validateLanguage(String? input, String? languageCode) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#validateLanguage, [input, languageCode]),
|
||||
returnValue: _i14.Future<bool>.value(false),
|
||||
)
|
||||
as _i14.Future<bool>);
|
||||
|
||||
@override
|
||||
void addListener(_i15.VoidCallback? listener) => super.noSuchMethod(
|
||||
Invocation.method(#addListener, [listener]),
|
||||
returnValueForMissingStub: null,
|
||||
);
|
||||
|
||||
@override
|
||||
void removeListener(_i15.VoidCallback? listener) => super.noSuchMethod(
|
||||
Invocation.method(#removeListener, [listener]),
|
||||
returnValueForMissingStub: null,
|
||||
);
|
||||
|
||||
@override
|
||||
void dispose() =>
|
||||
super.noSuchMethod(Invocation.method(#dispose, []), returnValueForMissingStub: null);
|
||||
|
||||
@override
|
||||
void notifyListeners() =>
|
||||
super.noSuchMethod(Invocation.method(#notifyListeners, []), returnValueForMissingStub: null);
|
||||
}
|
||||
|
||||
/// A class which mocks [WgerBaseProvider].
|
||||
///
|
||||
/// See the documentation for Mockito's code generation for more information.
|
||||
class MockWgerBaseProvider extends _i1.Mock implements _i2.WgerBaseProvider {
|
||||
MockWgerBaseProvider() {
|
||||
_i1.throwOnMissingStub(this);
|
||||
}
|
||||
|
||||
@override
|
||||
_i4.AuthProvider get auth =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.getter(#auth),
|
||||
returnValue: _FakeAuthProvider_2(this, Invocation.getter(#auth)),
|
||||
)
|
||||
as _i4.AuthProvider);
|
||||
|
||||
@override
|
||||
_i5.Client get client =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.getter(#client),
|
||||
returnValue: _FakeClient_3(this, Invocation.getter(#client)),
|
||||
)
|
||||
as _i5.Client);
|
||||
|
||||
@override
|
||||
set auth(_i4.AuthProvider? value) =>
|
||||
super.noSuchMethod(Invocation.setter(#auth, value), returnValueForMissingStub: null);
|
||||
|
||||
@override
|
||||
set client(_i5.Client? value) =>
|
||||
super.noSuchMethod(Invocation.setter(#client, value), returnValueForMissingStub: null);
|
||||
|
||||
@override
|
||||
Map<String, String> getDefaultHeaders({bool? includeAuth = false}) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#getDefaultHeaders, [], {#includeAuth: includeAuth}),
|
||||
returnValue: <String, String>{},
|
||||
)
|
||||
as Map<String, String>);
|
||||
|
||||
@override
|
||||
Uri makeUrl(String? path, {int? id, String? objectMethod, Map<String, dynamic>? query}) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(
|
||||
#makeUrl,
|
||||
[path],
|
||||
{#id: id, #objectMethod: objectMethod, #query: query},
|
||||
),
|
||||
returnValue: _FakeUri_4(
|
||||
this,
|
||||
Invocation.method(
|
||||
#makeUrl,
|
||||
[path],
|
||||
{#id: id, #objectMethod: objectMethod, #query: query},
|
||||
),
|
||||
),
|
||||
)
|
||||
as Uri);
|
||||
|
||||
@override
|
||||
_i14.Future<dynamic> fetch(Uri? uri) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#fetch, [uri]),
|
||||
returnValue: _i14.Future<dynamic>.value(),
|
||||
)
|
||||
as _i14.Future<dynamic>);
|
||||
|
||||
@override
|
||||
_i14.Future<List<dynamic>> fetchPaginated(Uri? uri) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#fetchPaginated, [uri]),
|
||||
returnValue: _i14.Future<List<dynamic>>.value(<dynamic>[]),
|
||||
)
|
||||
as _i14.Future<List<dynamic>>);
|
||||
|
||||
@override
|
||||
_i14.Future<Map<String, dynamic>> post(Map<String, dynamic>? data, Uri? uri) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#post, [data, uri]),
|
||||
returnValue: _i14.Future<Map<String, dynamic>>.value(<String, dynamic>{}),
|
||||
)
|
||||
as _i14.Future<Map<String, dynamic>>);
|
||||
|
||||
@override
|
||||
_i14.Future<Map<String, dynamic>> patch(Map<String, dynamic>? data, Uri? uri) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#patch, [data, uri]),
|
||||
returnValue: _i14.Future<Map<String, dynamic>>.value(<String, dynamic>{}),
|
||||
)
|
||||
as _i14.Future<Map<String, dynamic>>);
|
||||
|
||||
@override
|
||||
_i14.Future<_i5.Response> deleteRequest(String? url, int? id) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#deleteRequest, [url, id]),
|
||||
returnValue: _i14.Future<_i5.Response>.value(
|
||||
_FakeResponse_5(this, Invocation.method(#deleteRequest, [url, id])),
|
||||
),
|
||||
)
|
||||
as _i14.Future<_i5.Response>);
|
||||
}
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'dart:async' as _i15;
|
||||
import 'dart:io' as _i12;
|
||||
import 'dart:ui' as _i16;
|
||||
|
||||
import 'package:flutter/material.dart' as _i18;
|
||||
@@ -16,6 +15,7 @@ import 'package:wger/models/exercises/category.dart' as _i7;
|
||||
import 'package:wger/models/exercises/equipment.dart' as _i8;
|
||||
import 'package:wger/models/exercises/exercise.dart' as _i6;
|
||||
import 'package:wger/models/exercises/exercise_submission.dart' as _i13;
|
||||
import 'package:wger/models/exercises/exercise_submission_images.dart' as _i12;
|
||||
import 'package:wger/models/exercises/language.dart' as _i10;
|
||||
import 'package:wger/models/exercises/muscle.dart' as _i9;
|
||||
import 'package:wger/models/exercises/variation.dart' as _i3;
|
||||
@@ -97,9 +97,12 @@ class MockAddExerciseProvider extends _i1.Mock implements _i11.AddExerciseProvid
|
||||
as _i2.WgerBaseProvider);
|
||||
|
||||
@override
|
||||
List<_i12.File> get exerciseImages =>
|
||||
(super.noSuchMethod(Invocation.getter(#exerciseImages), returnValue: <_i12.File>[])
|
||||
as List<_i12.File>);
|
||||
List<_i12.ExerciseSubmissionImage> get exerciseImages =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.getter(#exerciseImages),
|
||||
returnValue: <_i12.ExerciseSubmissionImage>[],
|
||||
)
|
||||
as List<_i12.ExerciseSubmissionImage>);
|
||||
|
||||
@override
|
||||
List<String> get alternateNamesEn =>
|
||||
@@ -234,40 +237,19 @@ class MockAddExerciseProvider extends _i1.Mock implements _i11.AddExerciseProvid
|
||||
super.noSuchMethod(Invocation.method(#clear, []), returnValueForMissingStub: null);
|
||||
|
||||
@override
|
||||
void addExerciseImages(
|
||||
List<_i12.File>? images, {
|
||||
String? title,
|
||||
String? author,
|
||||
String? authorUrl,
|
||||
String? sourceUrl,
|
||||
String? derivativeSourceUrl,
|
||||
String? style = '1',
|
||||
}) => super.noSuchMethod(
|
||||
Invocation.method(
|
||||
#addExerciseImages,
|
||||
[images],
|
||||
{
|
||||
#title: title,
|
||||
#author: author,
|
||||
#authorUrl: authorUrl,
|
||||
#sourceUrl: sourceUrl,
|
||||
#derivativeSourceUrl: derivativeSourceUrl,
|
||||
#style: style,
|
||||
},
|
||||
),
|
||||
void addExerciseImages(List<_i12.ExerciseSubmissionImage>? images) => super.noSuchMethod(
|
||||
Invocation.method(#addExerciseImages, [images]),
|
||||
returnValueForMissingStub: null,
|
||||
);
|
||||
|
||||
@override
|
||||
void removeExercise(String? path) => super.noSuchMethod(
|
||||
Invocation.method(#removeExercise, [path]),
|
||||
returnValueForMissingStub: null,
|
||||
);
|
||||
void removeImage(String? path) =>
|
||||
super.noSuchMethod(Invocation.method(#removeImage, [path]), returnValueForMissingStub: null);
|
||||
|
||||
@override
|
||||
_i15.Future<int> addExercise() =>
|
||||
_i15.Future<int> postExerciseToServer() =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#addExercise, []),
|
||||
Invocation.method(#postExerciseToServer, []),
|
||||
returnValue: _i15.Future<int>.value(0),
|
||||
)
|
||||
as _i15.Future<int>);
|
||||
|
||||
Reference in New Issue
Block a user