From 75c18d283d3df66fb12cea522cbf5714a61c2d95 Mon Sep 17 00:00:00 2001 From: Roland Geider Date: Thu, 31 Mar 2022 00:20:13 +0200 Subject: [PATCH] Split up the widgets used, this was getting too cluttered --- lib/helpers/exercises/forms.dart | 33 ++ lib/screens/add_exercise_screen.dart | 414 +----------------- lib/widgets/add_exercise/steps/basics.dart | 106 +++++ .../add_exercise/steps/description.dart | 32 ++ lib/widgets/add_exercise/steps/images.dart | 54 +++ .../add_exercise/steps/translations.dart | 99 +++++ .../add_exercise/steps/variations.dart | 105 +++++ 7 files changed, 439 insertions(+), 404 deletions(-) create mode 100644 lib/helpers/exercises/forms.dart create mode 100644 lib/widgets/add_exercise/steps/basics.dart create mode 100644 lib/widgets/add_exercise/steps/description.dart create mode 100644 lib/widgets/add_exercise/steps/images.dart create mode 100644 lib/widgets/add_exercise/steps/translations.dart create mode 100644 lib/widgets/add_exercise/steps/variations.dart diff --git a/lib/helpers/exercises/forms.dart b/lib/helpers/exercises/forms.dart new file mode 100644 index 00000000..25d92bde --- /dev/null +++ b/lib/helpers/exercises/forms.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/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, MAX_CHARS_NAME); + } + + return null; +} + +String? validateDescription(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); + } + + return null; +} diff --git a/lib/screens/add_exercise_screen.dart b/lib/screens/add_exercise_screen.dart index 3a23657e..92edda29 100644 --- a/lib/screens/add_exercise_screen.dart +++ b/lib/screens/add_exercise_screen.dart @@ -3,19 +3,13 @@ import 'dart:developer'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:provider/provider.dart'; -import 'package:wger/models/exercises/category.dart'; -import 'package:wger/models/exercises/equipment.dart'; -import 'package:wger/models/exercises/language.dart'; -import 'package:wger/models/exercises/muscle.dart'; import 'package:wger/providers/add_exercise_provider.dart'; -import 'package:wger/providers/exercises.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/mixins/image_picker_mixin.dart'; -import 'package:wger/widgets/add_exercise/preview_images.dart'; +import 'package:wger/widgets/add_exercise/steps/basics.dart'; +import 'package:wger/widgets/add_exercise/steps/description.dart'; +import 'package:wger/widgets/add_exercise/steps/images.dart'; +import 'package:wger/widgets/add_exercise/steps/translations.dart'; +import 'package:wger/widgets/add_exercise/steps/variations.dart'; import 'package:wger/widgets/core/app_bar.dart'; -import 'package:wger/widgets/exercises/exercises.dart'; -import 'package:wger/widgets/exercises/forms.dart'; class AddExerciseScreen extends StatefulWidget { const AddExerciseScreen({Key? key}) : super(key: key); @@ -31,37 +25,6 @@ abstract class ValidateStep { abstract VoidCallback _submit; } -/// 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, MAX_CHARS_NAME); - } - - return null; -} - -String? validateDescription(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); - } - - return null; -} - class _AddExerciseScreenState extends State { int _currentStep = 0; int lastStepIndex = AddExerciseScreen.STEPS_IN_FORM - 1; @@ -115,23 +78,23 @@ class _AddExerciseScreenState extends State { steps: [ Step( title: Text(AppLocalizations.of(context).baseData), - content: _BasicStepContent(formkey: _keys[0]), + content: BasicStepContent(formkey: _keys[0]), ), Step( title: Text(AppLocalizations.of(context).variations), - content: _DuplicatesAndVariationsStepContent(formkey: _keys[1]), + content: DuplicatesAndVariationsStepContent(formkey: _keys[1]), ), Step( title: Text(AppLocalizations.of(context).description), - content: _DescriptionStepContent(formkey: _keys[2]), + content: DescriptionStepContent(formkey: _keys[2]), ), Step( title: Text(AppLocalizations.of(context).translation), - content: _DescriptionTranslationStepContent(formkey: _keys[3]), + content: DescriptionTranslationStepContent(formkey: _keys[3]), ), Step( title: Text(AppLocalizations.of(context).images), - content: _ImagesStepContent(formkey: _keys[4]), + content: ImagesStepContent(formkey: _keys[4]), ), ], currentStep: _currentStep, @@ -167,360 +130,3 @@ class _AddExerciseScreenState extends State { ); } } - -class _BasicStepContent extends StatelessWidget { - final GlobalKey formkey; - const _BasicStepContent({required this.formkey}); - - @override - Widget build(BuildContext context) { - final addExerciseProvider = context.read(); - final exerciseProvider = context.read(); - final categories = exerciseProvider.categories; - final muscles = exerciseProvider.muscles; - final equipment = exerciseProvider.equipment; - final languages = exerciseProvider.languages; - - // Initialize some values - addExerciseProvider.category = categories.first; - addExerciseProvider.language = languages.first; - - return Form( - key: formkey, - child: Column( - children: [ - AddExerciseTextArea( - onChange: (value) => {}, - title: '${AppLocalizations.of(context).name}*', - helperText: AppLocalizations.of(context).baseNameEnglish, - isRequired: true, - validator: (name) => validateName(name, context), - onSaved: (String? name) => addExerciseProvider.exerciseNameEn = name!, - ), - AddExerciseTextArea( - onChange: (value) => {}, - title: AppLocalizations.of(context).alternativeNames, - isMultiline: true, - helperText: AppLocalizations.of(context).oneNamePerLine, - onSaved: (String? alternateName) => - addExerciseProvider.alternateNamesEn = alternateName!.split('\n'), - ), - ExerciseCategoryInputWidget( - categories: categories, - title: AppLocalizations.of(context).category, - callback: (ExerciseCategory newValue) { - addExerciseProvider.category = newValue; - }, - displayName: (ExerciseCategory c) => c.name, - ), - AddExerciseMultiselectButton( - title: AppLocalizations.of(context).equipment, - items: equipment, - initialItems: addExerciseProvider.equipment, - onChange: (dynamic entries) { - addExerciseProvider.equipment = entries.cast(); - }, - onSaved: (dynamic entries) { - addExerciseProvider.equipment = entries.cast(); - }, - ), - AddExerciseMultiselectButton( - title: AppLocalizations.of(context).muscles, - items: muscles, - initialItems: addExerciseProvider.primaryMuscles, - onChange: (dynamic muscles) { - addExerciseProvider.primaryMuscles = muscles.cast(); - }, - onSaved: (dynamic muscles) { - addExerciseProvider.primaryMuscles = muscles.cast(); - }, - ), - AddExerciseMultiselectButton( - title: AppLocalizations.of(context).musclesSecondary, - items: muscles, - initialItems: addExerciseProvider.secondaryMuscles, - onChange: (dynamic muscles) { - addExerciseProvider.secondaryMuscles = muscles.cast(); - }, - onSaved: (dynamic muscles) { - addExerciseProvider.secondaryMuscles = muscles.cast(); - }, - ), - Consumer( - builder: (context, value, child) => MuscleRowWidget( - muscles: value.primaryMuscles, - musclesSecondary: value.secondaryMuscles, - ), - ), - const MuscleColorHelper(main: true), - const SizedBox(height: 5), - const MuscleColorHelper(main: false), - ], - ), - ); - } -} - -class _DuplicatesAndVariationsStepContent extends StatelessWidget { - final GlobalKey formkey; - - const _DuplicatesAndVariationsStepContent({required this.formkey}); - - @override - Widget build(BuildContext context) { - final addExerciseProvider = context.read(); - final exerciseProvider = context.read(); - - return Form( - key: formkey, - child: Column( - children: [ - Text( - AppLocalizations.of(context).whatVariationsExist, - style: Theme.of(context).textTheme.caption, - ), - SizedBox( - height: 400, - child: SingleChildScrollView( - child: Column( - children: [ - // Exercise bases with variations - ...exerciseProvider.exerciseBasesByVariation.keys - .map( - (key) => Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - //mainAxisSize: MainAxisSize.max, - children: [ - ...exerciseProvider.exerciseBasesByVariation[key]! - .map( - (base) => Text( - base - .getExercises( - Localizations.localeOf(context).languageCode) - .name, - overflow: TextOverflow.ellipsis, - ), - ) - .toList(), - const SizedBox(height: 20), - ], - ), - ), - Consumer( - builder: (ctx, provider, __) => Switch( - value: provider.variationId == key, - onChanged: (state) => provider.variationId = key), - ), - ], - ), - ) - .toList(), - // Exercise bases without variations - ...exerciseProvider.bases - .where((b) => b.variationId == null) - .map( - (base) => Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.max, - children: [ - Text( - base - .getExercises(Localizations.localeOf(context).languageCode) - .name, - overflow: TextOverflow.ellipsis, - ), - const SizedBox(height: 20), - ], - ), - ), - Consumer( - builder: (ctx, provider, __) => Switch( - value: provider.newVariationForExercise == base.id, - onChanged: (state) => provider.newVariationForExercise = base.id, - ), - ), - ], - ), - ) - .toList(), - ], - ), - ), - ), - ], - ), - ); - } -} - -class _ImagesStepContent extends StatefulWidget { - final GlobalKey formkey; - const _ImagesStepContent({required this.formkey}); - - @override - State<_ImagesStepContent> createState() => _ImagesStepContentState(); -} - -class _ImagesStepContentState extends State<_ImagesStepContent> with ExerciseImagePickerMixin { - @override - Widget build(BuildContext context) { - return Form( - key: widget.formkey, - child: Column( - children: [ - Text( - AppLocalizations.of(context).add_exercise_image_license, - style: Theme.of(context).textTheme.caption, - ), - Consumer( - builder: (ctx, provider, __) => provider.exerciseImages.isNotEmpty - ? PreviewExerciseImages( - selectedImages: provider.exerciseImages, - ) - : Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - IconButton( - onPressed: () => pickImages(context, pickFromCamera: true), - icon: const Icon(Icons.camera_alt), - ), - IconButton( - onPressed: () => pickImages(context), - icon: const Icon(Icons.collections), - ), - ], - ), - ), - Text( - 'Only JPEG, PNG and WEBP files below 20 MB are supported', - style: Theme.of(context).textTheme.caption, - ) - ], - ), - ); - } -} - -class _DescriptionStepContent extends StatelessWidget { - final GlobalKey formkey; - const _DescriptionStepContent({required this.formkey}); - - @override - Widget build(BuildContext context) { - final addExerciseProvider = context.read(); - - return Form( - key: formkey, - child: Column( - children: [ - AddExerciseTextArea( - onChange: (value) => {}, - title: '${AppLocalizations.of(context).description}*', - isRequired: true, - isMultiline: true, - validator: (name) => validateDescription(name, context), - onSaved: (String? description) => addExerciseProvider.descriptionEn = description!, - ), - ], - ), - ); - } -} - -class _DescriptionTranslationStepContent extends StatefulWidget { - final GlobalKey formkey; - const _DescriptionTranslationStepContent({required this.formkey}); - - @override - State<_DescriptionTranslationStepContent> createState() => - _DescriptionTranslationStepContentState(); -} - -class _DescriptionTranslationStepContentState extends State<_DescriptionTranslationStepContent> { - bool translate = false; - - @override - Widget build(BuildContext context) { - final addExerciseProvider = context.read(); - final exerciseProvider = context.read(); - final languages = exerciseProvider.languages; - - return Form( - key: widget.formkey, - child: Column( - children: [ - SwitchListTile( - title: Text(AppLocalizations.of(context).translation), - subtitle: Text(AppLocalizations.of(context).translateExercise), - value: translate, - onChanged: (_) { - setState(() { - translate = !translate; - }); - }, - ), - if (translate) - Column( - children: [ - ExerciseCategoryInputWidget( - categories: languages, - title: AppLocalizations.of(context).language, - displayName: (Language l) => l.fullName, - callback: (Language newValue) { - addExerciseProvider.language = newValue; - }, - ), - AddExerciseTextArea( - onChange: (value) => {}, - title: '${AppLocalizations.of(context).name}*', - isRequired: true, - validator: (name) => validateName(name, context), - onSaved: (String? name) => addExerciseProvider.exerciseNameTrans = name!, - ), - AddExerciseTextArea( - onChange: (value) => {}, - title: AppLocalizations.of(context).alternativeNames, - isMultiline: true, - helperText: AppLocalizations.of(context).oneNamePerLine, - validator: (alternateNames) { - // check that each line (name) is at least MIN_CHARACTERS_NAME long - if (alternateNames?.isNotEmpty == true) { - final names = alternateNames!.split('\n'); - for (final name in names) { - if (name.length < MIN_CHARS_NAME || name.length > MAX_CHARS_NAME) { - return AppLocalizations.of(context).enterCharacters( - MIN_CHARS_NAME, - MAX_CHARS_NAME, - ); - } - } - } - return null; - }, - onSaved: (String? alternateName) => - addExerciseProvider.alternateNamesTrans = alternateName!.split('\n'), - ), - AddExerciseTextArea( - onChange: (value) => {}, - title: '${AppLocalizations.of(context).description}*', - isRequired: true, - isMultiline: true, - validator: (name) => validateDescription(name, context), - onSaved: (String? description) => - addExerciseProvider.descriptionTrans = description!, - ), - ], - ), - ], - ), - ); - } -} diff --git a/lib/widgets/add_exercise/steps/basics.dart b/lib/widgets/add_exercise/steps/basics.dart new file mode 100644 index 00000000..3c918c0f --- /dev/null +++ b/lib/widgets/add_exercise/steps/basics.dart @@ -0,0 +1,106 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:provider/provider.dart'; +import 'package:wger/helpers/exercises/forms.dart'; +import 'package:wger/models/exercises/category.dart'; +import 'package:wger/models/exercises/equipment.dart'; +import 'package:wger/models/exercises/muscle.dart'; +import 'package:wger/providers/add_exercise_provider.dart'; +import 'package:wger/providers/exercises.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/exercises/exercises.dart'; +import 'package:wger/widgets/exercises/forms.dart'; + +class BasicStepContent extends StatelessWidget { + final GlobalKey formkey; + const BasicStepContent({required this.formkey}); + + @override + Widget build(BuildContext context) { + final addExerciseProvider = context.read(); + final exerciseProvider = context.read(); + final categories = exerciseProvider.categories; + final muscles = exerciseProvider.muscles; + final equipment = exerciseProvider.equipment; + final languages = exerciseProvider.languages; + + // Initialize some values + addExerciseProvider.category = categories.first; + addExerciseProvider.language = languages.first; + + return Form( + key: formkey, + child: Column( + children: [ + AddExerciseTextArea( + onChange: (value) => {}, + title: '${AppLocalizations.of(context).name}*', + helperText: AppLocalizations.of(context).baseNameEnglish, + isRequired: true, + validator: (name) => validateName(name, context), + onSaved: (String? name) => addExerciseProvider.exerciseNameEn = name!, + ), + AddExerciseTextArea( + onChange: (value) => {}, + title: AppLocalizations.of(context).alternativeNames, + isMultiline: true, + helperText: AppLocalizations.of(context).oneNamePerLine, + onSaved: (String? alternateName) => + addExerciseProvider.alternateNamesEn = alternateName!.split('\n'), + ), + ExerciseCategoryInputWidget( + categories: categories, + title: AppLocalizations.of(context).category, + callback: (ExerciseCategory newValue) { + addExerciseProvider.category = newValue; + }, + displayName: (ExerciseCategory c) => c.name, + ), + AddExerciseMultiselectButton( + title: AppLocalizations.of(context).equipment, + items: equipment, + initialItems: addExerciseProvider.equipment, + onChange: (dynamic entries) { + addExerciseProvider.equipment = entries.cast(); + }, + onSaved: (dynamic entries) { + addExerciseProvider.equipment = entries.cast(); + }, + ), + AddExerciseMultiselectButton( + title: AppLocalizations.of(context).muscles, + items: muscles, + initialItems: addExerciseProvider.primaryMuscles, + onChange: (dynamic muscles) { + addExerciseProvider.primaryMuscles = muscles.cast(); + }, + onSaved: (dynamic muscles) { + addExerciseProvider.primaryMuscles = muscles.cast(); + }, + ), + AddExerciseMultiselectButton( + title: AppLocalizations.of(context).musclesSecondary, + items: muscles, + initialItems: addExerciseProvider.secondaryMuscles, + onChange: (dynamic muscles) { + addExerciseProvider.secondaryMuscles = muscles.cast(); + }, + onSaved: (dynamic muscles) { + addExerciseProvider.secondaryMuscles = muscles.cast(); + }, + ), + Consumer( + builder: (context, value, child) => MuscleRowWidget( + muscles: value.primaryMuscles, + musclesSecondary: value.secondaryMuscles, + ), + ), + const MuscleColorHelper(main: true), + const SizedBox(height: 5), + const MuscleColorHelper(main: false), + ], + ), + ); + } +} diff --git a/lib/widgets/add_exercise/steps/description.dart b/lib/widgets/add_exercise/steps/description.dart new file mode 100644 index 00000000..39bf3a9a --- /dev/null +++ b/lib/widgets/add_exercise/steps/description.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:provider/provider.dart'; +import 'package:wger/helpers/exercises/forms.dart'; +import 'package:wger/providers/add_exercise_provider.dart'; +import 'package:wger/widgets/add_exercise/add_exercise_text_area.dart'; + +class DescriptionStepContent extends StatelessWidget { + final GlobalKey formkey; + const DescriptionStepContent({required this.formkey}); + + @override + Widget build(BuildContext context) { + final addExerciseProvider = context.read(); + + return Form( + key: formkey, + child: Column( + children: [ + AddExerciseTextArea( + onChange: (value) => {}, + title: '${AppLocalizations.of(context).description}*', + isRequired: true, + isMultiline: true, + validator: (name) => validateDescription(name, context), + onSaved: (String? description) => addExerciseProvider.descriptionEn = description!, + ), + ], + ), + ); + } +} diff --git a/lib/widgets/add_exercise/steps/images.dart b/lib/widgets/add_exercise/steps/images.dart new file mode 100644 index 00000000..a2c0b3de --- /dev/null +++ b/lib/widgets/add_exercise/steps/images.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:provider/provider.dart'; +import 'package:wger/providers/add_exercise_provider.dart'; +import 'package:wger/widgets/add_exercise/mixins/image_picker_mixin.dart'; +import 'package:wger/widgets/add_exercise/preview_images.dart'; + +class ImagesStepContent extends StatefulWidget { + final GlobalKey formkey; + const ImagesStepContent({required this.formkey}); + + @override + State createState() => _ImagesStepContentState(); +} + +class _ImagesStepContentState extends State with ExerciseImagePickerMixin { + @override + Widget build(BuildContext context) { + return Form( + key: widget.formkey, + child: Column( + children: [ + Text( + AppLocalizations.of(context).add_exercise_image_license, + style: Theme.of(context).textTheme.caption, + ), + Consumer( + builder: (ctx, provider, __) => provider.exerciseImages.isNotEmpty + ? PreviewExerciseImages( + selectedImages: provider.exerciseImages, + ) + : Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + IconButton( + onPressed: () => pickImages(context, pickFromCamera: true), + icon: const Icon(Icons.camera_alt), + ), + IconButton( + onPressed: () => pickImages(context), + icon: const Icon(Icons.collections), + ), + ], + ), + ), + Text( + 'Only JPEG, PNG and WEBP files below 20 MB are supported', + style: Theme.of(context).textTheme.caption, + ) + ], + ), + ); + } +} diff --git a/lib/widgets/add_exercise/steps/translations.dart b/lib/widgets/add_exercise/steps/translations.dart new file mode 100644 index 00000000..67751664 --- /dev/null +++ b/lib/widgets/add_exercise/steps/translations.dart @@ -0,0 +1,99 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:provider/provider.dart'; +import 'package:wger/helpers/exercises/forms.dart'; +import 'package:wger/models/exercises/language.dart'; +import 'package:wger/providers/add_exercise_provider.dart'; +import 'package:wger/providers/exercises.dart'; +import 'package:wger/widgets/add_exercise/add_exercise_text_area.dart'; +import 'package:wger/widgets/exercises/forms.dart'; + +class DescriptionTranslationStepContent extends StatefulWidget { + final GlobalKey formkey; + const DescriptionTranslationStepContent({required this.formkey}); + + @override + State createState() => + _DescriptionTranslationStepContentState(); +} + +class _DescriptionTranslationStepContentState extends State { + bool translate = false; + + @override + Widget build(BuildContext context) { + final addExerciseProvider = context.read(); + final exerciseProvider = context.read(); + final languages = exerciseProvider.languages; + + return Form( + key: widget.formkey, + child: Column( + children: [ + SwitchListTile( + title: Text(AppLocalizations.of(context).translation), + subtitle: Text(AppLocalizations.of(context).translateExercise), + value: translate, + onChanged: (_) { + setState(() { + translate = !translate; + }); + }, + ), + if (translate) + Column( + children: [ + ExerciseCategoryInputWidget( + categories: languages, + title: AppLocalizations.of(context).language, + displayName: (Language l) => l.fullName, + callback: (Language newValue) { + addExerciseProvider.language = newValue; + }, + ), + AddExerciseTextArea( + onChange: (value) => {}, + title: '${AppLocalizations.of(context).name}*', + isRequired: true, + validator: (name) => validateName(name, context), + onSaved: (String? name) => addExerciseProvider.exerciseNameTrans = name!, + ), + AddExerciseTextArea( + onChange: (value) => {}, + title: AppLocalizations.of(context).alternativeNames, + isMultiline: true, + helperText: AppLocalizations.of(context).oneNamePerLine, + validator: (alternateNames) { + // check that each line (name) is at least MIN_CHARACTERS_NAME long + if (alternateNames?.isNotEmpty == true) { + final names = alternateNames!.split('\n'); + for (final name in names) { + if (name.length < MIN_CHARS_NAME || name.length > MAX_CHARS_NAME) { + return AppLocalizations.of(context).enterCharacters( + MIN_CHARS_NAME, + MAX_CHARS_NAME, + ); + } + } + } + return null; + }, + onSaved: (String? alternateName) => + addExerciseProvider.alternateNamesTrans = alternateName!.split('\n'), + ), + AddExerciseTextArea( + onChange: (value) => {}, + title: '${AppLocalizations.of(context).description}*', + isRequired: true, + isMultiline: true, + validator: (name) => validateDescription(name, context), + onSaved: (String? description) => + addExerciseProvider.descriptionTrans = description!, + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/widgets/add_exercise/steps/variations.dart b/lib/widgets/add_exercise/steps/variations.dart new file mode 100644 index 00000000..b04f0143 --- /dev/null +++ b/lib/widgets/add_exercise/steps/variations.dart @@ -0,0 +1,105 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:provider/provider.dart'; +import 'package:wger/providers/add_exercise_provider.dart'; +import 'package:wger/providers/exercises.dart'; + +class DuplicatesAndVariationsStepContent extends StatelessWidget { + final GlobalKey formkey; + + const DuplicatesAndVariationsStepContent({required this.formkey}); + + @override + Widget build(BuildContext context) { + final addExerciseProvider = context.read(); + final exerciseProvider = context.read(); + + return Form( + key: formkey, + child: Column( + children: [ + Text( + AppLocalizations.of(context).whatVariationsExist, + style: Theme.of(context).textTheme.caption, + ), + SizedBox( + height: 400, + child: SingleChildScrollView( + child: Column( + children: [ + // Exercise bases with variations + ...exerciseProvider.exerciseBasesByVariation.keys + .map( + (key) => Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + //mainAxisSize: MainAxisSize.max, + children: [ + ...exerciseProvider.exerciseBasesByVariation[key]! + .map( + (base) => Text( + base + .getExercises( + Localizations.localeOf(context).languageCode) + .name, + overflow: TextOverflow.ellipsis, + ), + ) + .toList(), + const SizedBox(height: 20), + ], + ), + ), + Consumer( + builder: (ctx, provider, __) => Switch( + value: provider.variationId == key, + onChanged: (state) => provider.variationId = key), + ), + ], + ), + ) + .toList(), + // Exercise bases without variations + ...exerciseProvider.bases + .where((b) => b.variationId == null) + .map( + (base) => Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + children: [ + Text( + base + .getExercises(Localizations.localeOf(context).languageCode) + .name, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 20), + ], + ), + ), + Consumer( + builder: (ctx, provider, __) => Switch( + value: provider.newVariationForExercise == base.id, + onChanged: (state) => provider.newVariationForExercise = base.id, + ), + ), + ], + ), + ) + .toList(), + ], + ), + ), + ), + ], + ), + ); + } +}