Reuse AddExerciseTextArea in the image form

This commit is contained in:
Roland Geider
2025-10-08 14:05:50 +02:00
parent 0add2a6bd1
commit c63057fe35
14 changed files with 207 additions and 186 deletions

View File

@@ -145,4 +145,4 @@ const String API_RESULTS_PAGE_SIZE = '100';
const INTERPOLATION_MARKER = 123; const INTERPOLATION_MARKER = 123;
/// Creative Commons license IDs /// Creative Commons license IDs
const CC_BY_SA_4_ID = 4; const CC_BY_SA_4_ID = 2;

View File

@@ -0,0 +1,43 @@
import 'package:flutter/material.dart';
import 'package:wger/l10n/generated/app_localizations.dart';
/// The amount of characters an exercise description needs to have
const MIN_CHARS_DESCRIPTION = 40;
/// The amount of characters an exercise name needs to have
const MIN_CHARS_NAME = 5;
const MAX_CHARS_NAME = 40;
String? validateName(String? name, BuildContext context) {
if (name!.isEmpty) {
return AppLocalizations.of(context).enterValue;
}
if (name.length < MIN_CHARS_NAME || name.length > MAX_CHARS_NAME) {
return AppLocalizations.of(
context,
).enterCharacters(MIN_CHARS_NAME.toString(), MAX_CHARS_NAME.toString());
}
return null;
}
String? validateAuthorName(String? name, BuildContext context) {
if (name!.isEmpty) {
return AppLocalizations.of(context).enterValue;
}
return null;
}
String? validateExerciseDescription(String? name, BuildContext context) {
if (name!.isEmpty) {
return AppLocalizations.of(context).enterValue;
}
if (name.length < MIN_CHARS_DESCRIPTION) {
return AppLocalizations.of(context).enterMinCharacters(MIN_CHARS_DESCRIPTION.toString());
}
return null;
}

View File

@@ -741,19 +741,19 @@
"@imageDetailsLicenseTitleHint": { "@imageDetailsLicenseTitleHint": {
"description": "Hint text for image title field" "description": "Hint text for image title field"
}, },
"imageDetailsSourceLink": "Link to the source website, if available", "imageDetailsSourceLink": "Link to the source website",
"@imageDetailsSourceLink": { "@imageDetailsSourceLink": {
"description": "Label for source link field" "description": "Label for source link field"
}, },
"imageDetailsAuthor": "Author(s)", "author": "Author(s)",
"@imageDetailsAuthor": { "@Author": {
"description": "Label for author field" "description": "Label for author field"
}, },
"imageDetailsAuthorHint": "Enter author name", "authorHint": "Enter author name",
"@imageDetailsAuthorHint": { "@authorHint": {
"description": "Hint text for author field" "description": "Hint text for author field"
}, },
"imageDetailsAuthorLink": "Link to author website or profile, if available", "imageDetailsAuthorLink": "Link to author website or profile",
"@imageDetailsAuthorLink": { "@imageDetailsAuthorLink": {
"description": "Label for author link field" "description": "Label for author link field"
}, },

View File

@@ -11,7 +11,7 @@ ExerciseCategory _$ExerciseCategoryFromJson(Map<String, dynamic> json) {
return ExerciseCategory(id: (json['id'] as num).toInt(), name: json['name'] as String); return ExerciseCategory(id: (json['id'] as num).toInt(), name: json['name'] as String);
} }
ap<String, dynamic> _$ExerciseCategoryToJson(ExerciseCategory instance) => <String, dynamic>{ Map<String, dynamic> _$ExerciseCategoryToJson(ExerciseCategory instance) => <String, dynamic>{
'id': instance.id, 'id': instance.id,
'name': instance.name, 'name': instance.name,
}; };

View File

@@ -23,6 +23,7 @@ class AddExerciseProvider with ChangeNotifier {
List<ExerciseSubmissionImage> get exerciseImages => [..._exerciseImages]; List<ExerciseSubmissionImage> get exerciseImages => [..._exerciseImages];
String author = '';
String? exerciseNameEn; String? exerciseNameEn;
String? exerciseNameTrans; String? exerciseNameTrans;
String? descriptionEn; String? descriptionEn;

View File

@@ -1,22 +1,22 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class AddExerciseTextArea extends StatelessWidget { class AddExerciseTextArea extends StatelessWidget {
const AddExerciseTextArea({ AddExerciseTextArea({
super.key, super.key,
required this.onChange,
required this.title, required this.title,
ValueChanged<String>? onChange,
this.helperText = '', this.helperText = '',
this.isRequired = true,
this.isMultiline = false, this.isMultiline = false,
this.initialValue = '',
this.validator, this.validator,
this.onSaved, this.onSaved,
}); }) : onChange = onChange ?? ((String value) {});
final ValueChanged<String> onChange; final ValueChanged<String> onChange;
final bool isRequired;
final bool isMultiline; final bool isMultiline;
final String title; final String title;
final String helperText; final String helperText;
final String? initialValue;
final FormFieldValidator<String?>? validator; final FormFieldValidator<String?>? validator;
final FormFieldSetter<String?>? onSaved; final FormFieldSetter<String?>? onSaved;
@@ -28,6 +28,7 @@ class AddExerciseTextArea extends StatelessWidget {
return Padding( return Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: TextFormField( child: TextFormField(
initialValue: initialValue,
keyboardType: isMultiline ? TextInputType.multiline : TextInputType.text, keyboardType: isMultiline ? TextInputType.multiline : TextInputType.text,
maxLines: isMultiline ? null : DEFAULT_LINES, maxLines: isMultiline ? null : DEFAULT_LINES,
minLines: isMultiline ? MULTILINE_MIN_LINES : DEFAULT_LINES, minLines: isMultiline ? MULTILINE_MIN_LINES : DEFAULT_LINES,
@@ -35,12 +36,11 @@ class AddExerciseTextArea extends StatelessWidget {
onSaved: onSaved, onSaved: onSaved,
decoration: InputDecoration( decoration: InputDecoration(
contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
border: const OutlineInputBorder( border: const OutlineInputBorder(borderRadius: BorderRadius.all(Radius.circular(10))),
borderRadius: BorderRadius.all(Radius.circular(10)),
),
labelText: title, labelText: title,
alignLabelWithHint: true, alignLabelWithHint: true,
helperText: helperText, helperText: helperText,
helperMaxLines: 3,
), ),
onChanged: onChange, onChanged: onChange,
), ),

View File

@@ -1,8 +1,12 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:wger/core/validators.dart';
import 'package:wger/helpers/exercises/validators.dart';
import 'package:wger/l10n/generated/app_localizations.dart'; import 'package:wger/l10n/generated/app_localizations.dart';
import 'package:wger/models/exercises/exercise_submission_images.dart'; import 'package:wger/models/exercises/exercise_submission_images.dart';
import 'package:wger/widgets/add_exercise/license_info_widget.dart'; import 'package:wger/widgets/add_exercise/license_info_widget.dart';
import 'add_exercise_text_area.dart';
/// Form for collecting CC BY-SA 4.0 license metadata for exercise images /// Form for collecting CC BY-SA 4.0 license metadata for exercise images
/// ///
/// This form is displayed after image selection in Step 5 of exercise creation. /// This form is displayed after image selection in Step 5 of exercise creation.
@@ -50,6 +54,11 @@ class _ImageDetailsFormState extends State<ImageDetailsForm> {
/// Currently selected image type /// Currently selected image type
ImageType _selectedImageType = ImageType.photo; ImageType _selectedImageType = ImageType.photo;
@override
void initState() {
super.initState();
}
@override @override
void dispose() { void dispose() {
_titleController.dispose(); _titleController.dispose();
@@ -60,139 +69,78 @@ class _ImageDetailsFormState extends State<ImageDetailsForm> {
super.dispose(); super.dispose();
} }
/// Validates URL format
///
/// Returns error message if URL is invalid, null if valid or empty
String? _validateUrl(String? value) {
if (value == null || value.trim().isEmpty) {
return null; // Empty is OK (optional field)
}
final trimmedValue = value.trim();
// Check if starts with http:// or https://
if (!trimmedValue.startsWith('http://') && !trimmedValue.startsWith('https://')) {
return AppLocalizations.of(context).invalidUrl;
}
// Try to parse as URI
try {
final uri = Uri.parse(trimmedValue);
if (!uri.hasScheme || !uri.hasAuthority) {
return AppLocalizations.of(context).invalidUrl;
}
} catch (e) {
return AppLocalizations.of(context).invalidUrl;
}
return null;
}
/// 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':
return '1';
case '3D':
return '2';
case 'LINE':
return '3';
case 'LOW-POLY':
return '4';
case 'OTHER':
return '5';
default:
return '1'; // Default to PHOTO if unknown
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final i18n = AppLocalizations.of(context);
return SingleChildScrollView( return SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Form( child: Form(
key: _formKey, key: _formKey,
child: Column( child: Column(
spacing: 8,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
AppLocalizations.of(context).imageDetailsTitle, AppLocalizations.of(context).imageDetailsTitle,
style: Theme.of( style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
context,
).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
), ),
const SizedBox(height: 16), const SizedBox(height: 8),
_buildImagePreview(), _buildImagePreview(),
const SizedBox(height: 24), const SizedBox(height: 8),
// License title field - helps identify the image
_buildTextField(
controller: _titleController,
label: AppLocalizations.of(context).imageDetailsLicenseTitle,
hint: AppLocalizations.of(context).imageDetailsLicenseTitleHint,
),
const SizedBox(height: 16),
// Source URL - where the image was found (license_object_url in API)
_buildTextField(
controller: _sourceLinkController,
label: AppLocalizations.of(context).imageDetailsSourceLink,
hint: 'https://example.com',
keyboardType: TextInputType.url,
validator: _validateUrl,
),
const SizedBox(height: 16),
// Author name - required for proper CC BY-SA attribution // Author name - required for proper CC BY-SA attribution
_buildTextField( AddExerciseTextArea(
controller: _authorController, title: '${AppLocalizations.of(context).author}*',
label: AppLocalizations.of(context).imageDetailsAuthor, initialValue: widget.submissionImage.author,
hint: AppLocalizations.of(context).imageDetailsAuthorHint, onSaved: (value) => widget.submissionImage.author = value!,
validator: (name) => validateAuthorName(name, context),
),
// License title field - helps identify the image
AddExerciseTextArea(
title: AppLocalizations.of(context).imageDetailsLicenseTitle,
helperText: AppLocalizations.of(context).imageDetailsLicenseTitleHint,
initialValue: widget.submissionImage.title,
onSaved: (value) => widget.submissionImage.title = value,
),
// Source URL - where the image was found (license_object_url in API)
AddExerciseTextArea(
title: AppLocalizations.of(context).imageDetailsSourceLink,
initialValue: widget.submissionImage.sourceUrl,
onSaved: (value) => widget.submissionImage.sourceUrl = value,
validator: (value) => validateUrl(value, i18n, required: false),
), ),
const SizedBox(height: 16),
// Author's website/profile URL // Author's website/profile URL
_buildTextField( AddExerciseTextArea(
controller: _authorLinkController, title: AppLocalizations.of(context).imageDetailsAuthorLink,
label: AppLocalizations.of(context).imageDetailsAuthorLink, initialValue: widget.submissionImage.authorUrl,
hint: 'https://example.com/author', onSaved: (value) => widget.submissionImage.authorUrl = value,
keyboardType: TextInputType.url, validator: (value) => validateUrl(value, i18n, required: false),
validator: _validateUrl,
), ),
const SizedBox(height: 16),
// Original source if this is a derivative work (modified from another image) // Original source if this is a derivative work (modified from another image)
_buildTextField( AddExerciseTextArea(
controller: _originalSourceController, title: AppLocalizations.of(context).imageDetailsDerivativeSource,
label: AppLocalizations.of(context).imageDetailsDerivativeSource,
hint: 'https://example.com/original',
keyboardType: TextInputType.url,
helperText: AppLocalizations.of(context).imageDetailsDerivativeHelp, helperText: AppLocalizations.of(context).imageDetailsDerivativeHelp,
validator: _validateUrl, initialValue: widget.submissionImage.derivativeSourceUrl,
onSaved: (value) => widget.submissionImage.derivativeSourceUrl = value,
validator: (value) => validateUrl(value, i18n, required: false),
), ),
const SizedBox(height: 24),
_buildImageTypeSelector(), _buildImageTypeSelector(),
const SizedBox(height: 24), const SizedBox(height: 16),
// License info as separate widget for better optimization // License info as separate widget for better optimization
const LicenseInfoWidget(), const LicenseInfoWidget(),
const SizedBox(height: 24), const SizedBox(height: 8),
_buildButtons(), _buildButtons(),
], ],
), ),
), ),
),
); );
} }
@@ -292,7 +240,9 @@ class _ImageDetailsFormState extends State<ImageDetailsForm> {
type.label, type.label,
style: TextStyle( style: TextStyle(
fontSize: 11, fontSize: 11,
color: isSelected ? Colors.blue : Colors.grey.shade700, color: isSelected
? theme.buttonTheme.colorScheme!.onPrimary
: theme.buttonTheme.colorScheme!.primary,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
), ),
textAlign: TextAlign.center, textAlign: TextAlign.center,
@@ -322,10 +272,6 @@ class _ImageDetailsFormState extends State<ImageDetailsForm> {
return; 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 // Add optional fields only if user provided values
final title = _titleController.text.trim(); final title = _titleController.text.trim();
if (title.isNotEmpty) { if (title.isNotEmpty) {

View File

@@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:wger/helpers/consts.dart'; import 'package:wger/helpers/consts.dart';
import 'package:wger/helpers/exercises/forms.dart'; import 'package:wger/helpers/exercises/validators.dart';
import 'package:wger/helpers/i18n.dart'; import 'package:wger/helpers/i18n.dart';
import 'package:wger/l10n/generated/app_localizations.dart'; import 'package:wger/l10n/generated/app_localizations.dart';
import 'package:wger/models/exercises/category.dart'; import 'package:wger/models/exercises/category.dart';
@@ -9,6 +9,7 @@ import 'package:wger/models/exercises/equipment.dart';
import 'package:wger/models/exercises/muscle.dart'; import 'package:wger/models/exercises/muscle.dart';
import 'package:wger/providers/add_exercise.dart'; import 'package:wger/providers/add_exercise.dart';
import 'package:wger/providers/exercises.dart'; import 'package:wger/providers/exercises.dart';
import 'package:wger/providers/user.dart';
import 'package:wger/widgets/add_exercise/add_exercise_multiselect_button.dart'; import 'package:wger/widgets/add_exercise/add_exercise_multiselect_button.dart';
import 'package:wger/widgets/add_exercise/add_exercise_text_area.dart'; import 'package:wger/widgets/add_exercise/add_exercise_text_area.dart';
import 'package:wger/widgets/exercises/exercises.dart'; import 'package:wger/widgets/exercises/exercises.dart';
@@ -21,6 +22,7 @@ class Step1Basics extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final userProvider = context.read<UserProvider>();
final addExerciseProvider = context.read<AddExerciseProvider>(); final addExerciseProvider = context.read<AddExerciseProvider>();
final exerciseProvider = context.read<ExercisesProvider>(); final exerciseProvider = context.read<ExercisesProvider>();
final categories = exerciseProvider.categories; final categories = exerciseProvider.categories;
@@ -38,21 +40,25 @@ class Step1Basics extends StatelessWidget {
builder: (context, provider, child) => Column( builder: (context, provider, child) => Column(
children: [ children: [
AddExerciseTextArea( AddExerciseTextArea(
onChange: (value) => {},
title: '${AppLocalizations.of(context).name}*', title: '${AppLocalizations.of(context).name}*',
helperText: AppLocalizations.of(context).baseNameEnglish, helperText: AppLocalizations.of(context).baseNameEnglish,
isRequired: true,
validator: (name) => validateName(name, context), validator: (name) => validateName(name, context),
onSaved: (String? name) => addExerciseProvider.exerciseNameEn = name!, onSaved: (String? name) => addExerciseProvider.exerciseNameEn = name,
), ),
AddExerciseTextArea( AddExerciseTextArea(
onChange: (value) => {},
title: AppLocalizations.of(context).alternativeNames, title: AppLocalizations.of(context).alternativeNames,
isMultiline: true, isMultiline: true,
helperText: AppLocalizations.of(context).oneNamePerLine, helperText: AppLocalizations.of(context).oneNamePerLine,
onSaved: (String? alternateName) => onSaved: (String? alternateName) =>
addExerciseProvider.alternateNamesEn = alternateName!.split('\n'), addExerciseProvider.alternateNamesEn = alternateName!.split('\n'),
), ),
AddExerciseTextArea(
title: '${AppLocalizations.of(context).author}*',
isMultiline: false,
validator: (name) => validateAuthorName(name, context),
initialValue: userProvider.profile!.username,
onSaved: (String? author) => addExerciseProvider.author = author!,
),
ExerciseCategoryInputWidget<ExerciseCategory>( ExerciseCategoryInputWidget<ExerciseCategory>(
key: const Key('category-dropdown'), key: const Key('category-dropdown'),
entries: categories, entries: categories,

View File

@@ -1,6 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:wger/helpers/exercises/forms.dart'; import 'package:wger/helpers/exercises/validators.dart';
import 'package:wger/l10n/generated/app_localizations.dart'; import 'package:wger/l10n/generated/app_localizations.dart';
import 'package:wger/providers/add_exercise.dart'; import 'package:wger/providers/add_exercise.dart';
import 'package:wger/widgets/add_exercise/add_exercise_text_area.dart'; import 'package:wger/widgets/add_exercise/add_exercise_text_area.dart';
@@ -23,10 +23,9 @@ class Step3Description extends StatelessWidget {
onChange: (value) => {}, onChange: (value) => {},
title: '${i18n.description}*', title: '${i18n.description}*',
helperText: i18n.enterTextInLanguage, helperText: i18n.enterTextInLanguage,
isRequired: true,
isMultiline: true, isMultiline: true,
validator: (name) => validateExerciseDescription(name, context), validator: (name) => validateExerciseDescription(name, context),
onSaved: (String? description) => addExerciseProvider.descriptionEn = description!, onSaved: (String? description) => addExerciseProvider.descriptionEn = description,
), ),
], ],
), ),

View File

@@ -1,6 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:wger/helpers/exercises/forms.dart'; import 'package:wger/helpers/exercises/validators.dart';
import 'package:wger/l10n/generated/app_localizations.dart'; import 'package:wger/l10n/generated/app_localizations.dart';
import 'package:wger/models/exercises/language.dart'; import 'package:wger/models/exercises/language.dart';
import 'package:wger/providers/add_exercise.dart'; import 'package:wger/providers/add_exercise.dart';
@@ -59,14 +59,11 @@ class _Step4TranslationState extends State<Step4Translation> {
}, },
), ),
AddExerciseTextArea( AddExerciseTextArea(
onChange: (value) => {},
title: '${i18n.name}*', title: '${i18n.name}*',
isRequired: true,
validator: (name) => validateName(name, context), validator: (name) => validateName(name, context),
onSaved: (String? name) => addExerciseProvider.exerciseNameTrans = name!, onSaved: (String? name) => addExerciseProvider.exerciseNameTrans = name!,
), ),
AddExerciseTextArea( AddExerciseTextArea(
onChange: (value) => {},
title: i18n.alternativeNames, title: i18n.alternativeNames,
isMultiline: true, isMultiline: true,
helperText: i18n.oneNamePerLine, helperText: i18n.oneNamePerLine,
@@ -93,7 +90,6 @@ class _Step4TranslationState extends State<Step4Translation> {
onChange: (value) => {}, onChange: (value) => {},
title: '${i18n.description}*', title: '${i18n.description}*',
helperText: i18n.enterTextInLanguage, helperText: i18n.enterTextInLanguage,
isRequired: true,
isMultiline: true, isMultiline: true,
validator: (name) => validateExerciseDescription(name, context), validator: (name) => validateExerciseDescription(name, context),
onSaved: (String? description) => onSaved: (String? description) =>

View File

@@ -6,6 +6,7 @@ import 'package:provider/provider.dart';
import 'package:wger/l10n/generated/app_localizations.dart'; import 'package:wger/l10n/generated/app_localizations.dart';
import 'package:wger/models/exercises/exercise_submission_images.dart'; import 'package:wger/models/exercises/exercise_submission_images.dart';
import 'package:wger/providers/add_exercise.dart'; import 'package:wger/providers/add_exercise.dart';
import 'package:wger/providers/user.dart';
import 'package:wger/widgets/add_exercise/image_details_form.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/mixins/image_picker_mixin.dart';
import 'package:wger/widgets/add_exercise/preview_images.dart'; import 'package:wger/widgets/add_exercise/preview_images.dart';
@@ -33,7 +34,7 @@ class Step5Images extends StatefulWidget {
class _Step5ImagesState extends State<Step5Images> with ExerciseImagePickerMixin { class _Step5ImagesState extends State<Step5Images> with ExerciseImagePickerMixin {
/// Currently selected image waiting for metadata input /// Currently selected image waiting for metadata input
/// When non-null, ImageDetailsForm is displayed instead of image picker /// When non-null, ImageDetailsForm is displayed instead of image picker
ExerciseSubmissionImage? _currentImageToAddNew; ExerciseSubmissionImage? _currentImageToAdd;
/// Show dialog to choose between Camera and Gallery /// Show dialog to choose between Camera and Gallery
Future<void> _showImageSourceDialog(BuildContext context) async { Future<void> _showImageSourceDialog(BuildContext context) async {
@@ -75,6 +76,7 @@ class _Step5ImagesState extends State<Step5Images> with ExerciseImagePickerMixin
/// ///
/// [pickFromCamera] - If true, opens camera; otherwise opens gallery /// [pickFromCamera] - If true, opens camera; otherwise opens gallery
void _pickAndShowImageDetails(BuildContext context, {bool pickFromCamera = false}) async { void _pickAndShowImageDetails(BuildContext context, {bool pickFromCamera = false}) async {
final userProvider = context.read<UserProvider>();
final imagePicker = ImagePicker(); final imagePicker = ImagePicker();
XFile? selectedImage; XFile? selectedImage;
@@ -114,7 +116,10 @@ class _Step5ImagesState extends State<Step5Images> with ExerciseImagePickerMixin
// Show metadata collection form for valid image // Show metadata collection form for valid image
setState(() { setState(() {
_currentImageToAddNew = ExerciseSubmissionImage(imageFile: imageFile); _currentImageToAdd = ExerciseSubmissionImage(
imageFile: imageFile,
author: userProvider.profile?.username ?? '',
);
}); });
} }
} }
@@ -135,14 +140,14 @@ class _Step5ImagesState extends State<Step5Images> with ExerciseImagePickerMixin
// Reset form state - image is now visible in preview list // Reset form state - image is now visible in preview list
setState(() { setState(() {
_currentImageToAddNew = null; _currentImageToAdd = null;
}); });
} }
/// Cancel metadata input and return to image picker /// Cancel metadata input and return to image picker
void _cancelImageAdd() { void _cancelImageAdd() {
setState(() { setState(() {
_currentImageToAddNew = null; _currentImageToAdd = null;
}); });
} }
@@ -153,7 +158,7 @@ class _Step5ImagesState extends State<Step5Images> with ExerciseImagePickerMixin
child: Column( child: Column(
children: [ children: [
// License notice - shown when not entering metadata // License notice - shown when not entering metadata
if (_currentImageToAddNew == null) if (_currentImageToAdd == null)
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: Text( child: Text(
@@ -164,15 +169,15 @@ class _Step5ImagesState extends State<Step5Images> with ExerciseImagePickerMixin
), ),
// Metadata collection form - shown when image is selected // Metadata collection form - shown when image is selected
if (_currentImageToAddNew != null) if (_currentImageToAdd != null)
ImageDetailsForm( ImageDetailsForm(
submissionImage: _currentImageToAddNew!, submissionImage: _currentImageToAdd!,
onAdd: _addImageWithDetails, onAdd: _addImageWithDetails,
onCancel: _cancelImageAdd, onCancel: _cancelImageAdd,
), ),
// Image picker or preview - shown when not entering metadata // Image picker or preview - shown when not entering metadata
if (_currentImageToAddNew == null) if (_currentImageToAdd == null)
Consumer<AddExerciseProvider>( Consumer<AddExerciseProvider>(
builder: (ctx, provider, __) { builder: (ctx, provider, __) {
if (provider.exerciseImages.isNotEmpty) { if (provider.exerciseImages.isNotEmpty) {

View File

@@ -11,13 +11,14 @@ class Step6Overview extends StatelessWidget {
final i18n = AppLocalizations.of(context); final i18n = AppLocalizations.of(context);
return Consumer<AddExerciseProvider>( return Consumer<AddExerciseProvider>(
builder: (ctx, provider, __) => Column( builder: (ctx, provider, _) => Column(
spacing: 8, spacing: 8,
children: [ children: [
Text(i18n.baseData, style: Theme.of(context).textTheme.headlineSmall), Text(i18n.baseData, style: Theme.of(context).textTheme.headlineSmall),
Table( Table(
columnWidths: const {0: FlexColumnWidth(2), 1: FlexColumnWidth(3)}, columnWidths: const {0: FlexColumnWidth(2), 1: FlexColumnWidth(3)},
children: [ children: [
TableRow(children: [Text(i18n.author), Text(provider.author)]),
TableRow(children: [Text(i18n.name), Text(provider.exerciseNameEn ?? '...')]), TableRow(children: [Text(i18n.name), Text(provider.exerciseNameEn ?? '...')]),
TableRow( TableRow(
children: [ children: [

View File

@@ -8,13 +8,13 @@ import 'dart:ui' as _i15;
import 'package:http/http.dart' as _i5; import 'package:http/http.dart' as _i5;
import 'package:mockito/mockito.dart' as _i1; import 'package:mockito/mockito.dart' as _i1;
import 'package:mockito/src/dummies.dart' as _i11; import 'package:mockito/src/dummies.dart' as _i8;
import 'package:wger/models/exercises/category.dart' as _i13; import 'package:wger/models/exercises/category.dart' as _i13;
import 'package:wger/models/exercises/equipment.dart' as _i8; import 'package:wger/models/exercises/equipment.dart' as _i9;
import 'package:wger/models/exercises/exercise_submission.dart' as _i10; import 'package:wger/models/exercises/exercise_submission.dart' as _i11;
import 'package:wger/models/exercises/exercise_submission_images.dart' as _i7; 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/language.dart' as _i12;
import 'package:wger/models/exercises/muscle.dart' as _i9; import 'package:wger/models/exercises/muscle.dart' as _i10;
import 'package:wger/models/exercises/variation.dart' as _i3; import 'package:wger/models/exercises/variation.dart' as _i3;
import 'package:wger/providers/add_exercise.dart' as _i6; import 'package:wger/providers/add_exercise.dart' as _i6;
import 'package:wger/providers/auth.dart' as _i4; import 'package:wger/providers/auth.dart' as _i4;
@@ -84,6 +84,14 @@ class MockAddExerciseProvider extends _i1.Mock implements _i6.AddExerciseProvide
) )
as List<_i7.ExerciseSubmissionImage>); as List<_i7.ExerciseSubmissionImage>);
@override
String get author =>
(super.noSuchMethod(
Invocation.getter(#author),
returnValue: _i8.dummyValue<String>(this, Invocation.getter(#author)),
)
as String);
@override @override
List<String> get alternateNamesEn => List<String> get alternateNamesEn =>
(super.noSuchMethod(Invocation.getter(#alternateNamesEn), returnValue: <String>[]) (super.noSuchMethod(Invocation.getter(#alternateNamesEn), returnValue: <String>[])
@@ -95,9 +103,9 @@ class MockAddExerciseProvider extends _i1.Mock implements _i6.AddExerciseProvide
as List<String>); as List<String>);
@override @override
List<_i8.Equipment> get equipment => List<_i9.Equipment> get equipment =>
(super.noSuchMethod(Invocation.getter(#equipment), returnValue: <_i8.Equipment>[]) (super.noSuchMethod(Invocation.getter(#equipment), returnValue: <_i9.Equipment>[])
as List<_i8.Equipment>); as List<_i9.Equipment>);
@override @override
bool get newVariation => bool get newVariation =>
@@ -112,25 +120,29 @@ class MockAddExerciseProvider extends _i1.Mock implements _i6.AddExerciseProvide
as _i3.Variation); as _i3.Variation);
@override @override
List<_i9.Muscle> get primaryMuscles => List<_i10.Muscle> get primaryMuscles =>
(super.noSuchMethod(Invocation.getter(#primaryMuscles), returnValue: <_i9.Muscle>[]) (super.noSuchMethod(Invocation.getter(#primaryMuscles), returnValue: <_i10.Muscle>[])
as List<_i9.Muscle>); as List<_i10.Muscle>);
@override @override
List<_i9.Muscle> get secondaryMuscles => List<_i10.Muscle> get secondaryMuscles =>
(super.noSuchMethod(Invocation.getter(#secondaryMuscles), returnValue: <_i9.Muscle>[]) (super.noSuchMethod(Invocation.getter(#secondaryMuscles), returnValue: <_i10.Muscle>[])
as List<_i9.Muscle>); as List<_i10.Muscle>);
@override @override
_i10.ExerciseSubmissionApi get exerciseApiObject => _i11.ExerciseSubmissionApi get exerciseApiObject =>
(super.noSuchMethod( (super.noSuchMethod(
Invocation.getter(#exerciseApiObject), Invocation.getter(#exerciseApiObject),
returnValue: _i11.dummyValue<_i10.ExerciseSubmissionApi>( returnValue: _i8.dummyValue<_i11.ExerciseSubmissionApi>(
this, this,
Invocation.getter(#exerciseApiObject), Invocation.getter(#exerciseApiObject),
), ),
) )
as _i10.ExerciseSubmissionApi); as _i11.ExerciseSubmissionApi);
@override
set author(String? value) =>
super.noSuchMethod(Invocation.setter(#author, value), returnValueForMissingStub: null);
@override @override
set exerciseNameEn(String? value) => super.noSuchMethod( set exerciseNameEn(String? value) => super.noSuchMethod(
@@ -181,7 +193,7 @@ class MockAddExerciseProvider extends _i1.Mock implements _i6.AddExerciseProvide
super.noSuchMethod(Invocation.setter(#category, value), returnValueForMissingStub: null); super.noSuchMethod(Invocation.setter(#category, value), returnValueForMissingStub: null);
@override @override
set equipment(List<_i8.Equipment>? equipment) => set equipment(List<_i9.Equipment>? equipment) =>
super.noSuchMethod(Invocation.setter(#equipment, equipment), returnValueForMissingStub: null); super.noSuchMethod(Invocation.setter(#equipment, equipment), returnValueForMissingStub: null);
@override @override
@@ -197,13 +209,13 @@ class MockAddExerciseProvider extends _i1.Mock implements _i6.AddExerciseProvide
); );
@override @override
set primaryMuscles(List<_i9.Muscle>? muscles) => super.noSuchMethod( set primaryMuscles(List<_i10.Muscle>? muscles) => super.noSuchMethod(
Invocation.setter(#primaryMuscles, muscles), Invocation.setter(#primaryMuscles, muscles),
returnValueForMissingStub: null, returnValueForMissingStub: null,
); );
@override @override
set secondaryMuscles(List<_i9.Muscle>? muscles) => super.noSuchMethod( set secondaryMuscles(List<_i10.Muscle>? muscles) => super.noSuchMethod(
Invocation.setter(#secondaryMuscles, muscles), Invocation.setter(#secondaryMuscles, muscles),
returnValueForMissingStub: null, returnValueForMissingStub: null,
); );

View File

@@ -8,13 +8,13 @@ import 'dart:ui' as _i16;
import 'package:flutter/material.dart' as _i18; import 'package:flutter/material.dart' as _i18;
import 'package:mockito/mockito.dart' as _i1; import 'package:mockito/mockito.dart' as _i1;
import 'package:mockito/src/dummies.dart' as _i14; import 'package:mockito/src/dummies.dart' as _i13;
import 'package:shared_preferences/shared_preferences.dart' as _i4; import 'package:shared_preferences/shared_preferences.dart' as _i4;
import 'package:wger/database/exercises/exercise_database.dart' as _i5; import 'package:wger/database/exercises/exercise_database.dart' as _i5;
import 'package:wger/models/exercises/category.dart' as _i7; import 'package:wger/models/exercises/category.dart' as _i7;
import 'package:wger/models/exercises/equipment.dart' as _i8; import 'package:wger/models/exercises/equipment.dart' as _i8;
import 'package:wger/models/exercises/exercise.dart' as _i6; 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.dart' as _i14;
import 'package:wger/models/exercises/exercise_submission_images.dart' as _i12; 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/language.dart' as _i10;
import 'package:wger/models/exercises/muscle.dart' as _i9; import 'package:wger/models/exercises/muscle.dart' as _i9;
@@ -104,6 +104,14 @@ class MockAddExerciseProvider extends _i1.Mock implements _i11.AddExerciseProvid
) )
as List<_i12.ExerciseSubmissionImage>); as List<_i12.ExerciseSubmissionImage>);
@override
String get author =>
(super.noSuchMethod(
Invocation.getter(#author),
returnValue: _i13.dummyValue<String>(this, Invocation.getter(#author)),
)
as String);
@override @override
List<String> get alternateNamesEn => List<String> get alternateNamesEn =>
(super.noSuchMethod(Invocation.getter(#alternateNamesEn), returnValue: <String>[]) (super.noSuchMethod(Invocation.getter(#alternateNamesEn), returnValue: <String>[])
@@ -142,15 +150,19 @@ class MockAddExerciseProvider extends _i1.Mock implements _i11.AddExerciseProvid
as List<_i9.Muscle>); as List<_i9.Muscle>);
@override @override
_i13.ExerciseSubmissionApi get exerciseApiObject => _i14.ExerciseSubmissionApi get exerciseApiObject =>
(super.noSuchMethod( (super.noSuchMethod(
Invocation.getter(#exerciseApiObject), Invocation.getter(#exerciseApiObject),
returnValue: _i14.dummyValue<_i13.ExerciseSubmissionApi>( returnValue: _i13.dummyValue<_i14.ExerciseSubmissionApi>(
this, this,
Invocation.getter(#exerciseApiObject), Invocation.getter(#exerciseApiObject),
), ),
) )
as _i13.ExerciseSubmissionApi); as _i14.ExerciseSubmissionApi);
@override
set author(String? value) =>
super.noSuchMethod(Invocation.setter(#author, value), returnValueForMissingStub: null);
@override @override
set exerciseNameEn(String? value) => super.noSuchMethod( set exerciseNameEn(String? value) => super.noSuchMethod(