diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 17df7652d..10b3fe7eb 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -515,5 +515,6 @@ "description": "Snackbar message to show on copying data to a new log entry" }, "appUpdateTitle" : "Update needed", - "appUpdateContent" : "This version of the app is not compatible with the server, please update your application." + "appUpdateContent" : "This version of the app is not compatible with the server, please update your application.", + "add_excercise_image_license": "Images must be compatible with the CC BY SA_license. If in doubt, upload only photos you've taken yourself." } diff --git a/lib/main.dart b/lib/main.dart index c349b3302..92a97f685 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -47,6 +47,7 @@ import 'package:wger/screens/workout_plan_screen.dart'; import 'package:wger/screens/workout_plans_screen.dart'; import 'package:wger/theme/theme.dart'; +import 'providers/add_excercise_provider.dart'; import 'providers/auth.dart'; void main() { @@ -102,6 +103,9 @@ class MyApp extends StatelessWidget { GalleryProvider(Provider.of(context, listen: false), []), update: (context, auth, previous) => previous ?? GalleryProvider(auth, []), ), + ChangeNotifierProvider( + create: (_) => AddExcerciseProvider(), + ) ], child: Consumer( builder: (ctx, auth, _) => MaterialApp( diff --git a/lib/providers/add_excercise_provider.dart b/lib/providers/add_excercise_provider.dart new file mode 100644 index 000000000..0a2dc3ca4 --- /dev/null +++ b/lib/providers/add_excercise_provider.dart @@ -0,0 +1,19 @@ +import 'package:flutter/foundation.dart'; + +import 'dart:io'; + +class AddExcerciseProvider with ChangeNotifier { + List get excerciseImages => [..._excerciseImages]; + final List _excerciseImages = []; + + void addExcerciseImages(List excercizes) { + _excerciseImages.addAll(excercizes); + notifyListeners(); + } + + void removeExcercise(String path) { + final file = _excerciseImages.where((element) => element.path == path).first; + _excerciseImages.remove(file); + notifyListeners(); + } +} diff --git a/lib/screens/add_exercise_screen.dart b/lib/screens/add_exercise_screen.dart index 3448c4891..db34d33ac 100644 --- a/lib/screens/add_exercise_screen.dart +++ b/lib/screens/add_exercise_screen.dart @@ -1,8 +1,16 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:provider/provider.dart'; +import 'package:wger/providers/add_excercise_provider.dart'; import 'package:wger/widgets/add_exercise/add_exercise_dropdown_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/mixins/image_picker_mixin.dart'; +import 'package:wger/widgets/add_exercise/preview_images.dart'; import 'package:wger/widgets/core/app_bar.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; class AddExerciseScreen extends StatefulWidget { const AddExerciseScreen({Key? key}) : super(key: key); @@ -148,28 +156,42 @@ class _DuplicatesAndVariationsStepContent extends StatelessWidget { } } -class _ImagesStepContent extends StatelessWidget { - final GlobalKey _imagesStepFormKey = GlobalKey(); +class _ImagesStepContent extends StatefulWidget { + @override + State<_ImagesStepContent> createState() => _ImagesStepContentState(); +} +class _ImagesStepContentState extends State<_ImagesStepContent> with ExcerciseImagePickerMixin { + final GlobalKey _imagesStepFormKey = GlobalKey(); @override Widget build(BuildContext context) { - return Form( - key: _imagesStepFormKey, - child: Column( - children: [ - AddExerciseTextArea( - onChange: (value) => print(value), - title: 'Name', - isRequired: true, - ), - AddExerciseTextArea( - onChange: (value) => print(value), - title: 'Alternative names', - isMultiline: true, - helperText: 'One name per line', + return Column( + children: [ + Text( + AppLocalizations.of(context).add_excercise_image_license, + style: Theme.of(context).textTheme.caption, + ), + Consumer( + builder: (ctx, provider, __) => provider.excerciseImages.isNotEmpty + ? PreviewExcercizeImages( + selectedimages: provider.excerciseImages, + ) + : ElevatedButton( + onPressed: () => pickImages(context), + child: const Text('BROWSE FOR FILES'), + style: ButtonStyle( + backgroundColor: MaterialStateProperty.resolveWith((states) => Colors.black)), + ), + ), + RichText( + text: TextSpan( + style: Theme.of(context).textTheme.caption, + children: const [ + TextSpan(text: 'Only JPEG, PNG and WEBP files below 20 MB are supported'), + ], ), - ], - ), + ) + ], ); } } diff --git a/lib/widgets/add_exercise/mixins/image_picker_mixin.dart b/lib/widgets/add_exercise/mixins/image_picker_mixin.dart new file mode 100644 index 000000000..fcc07565a --- /dev/null +++ b/lib/widgets/add_exercise/mixins/image_picker_mixin.dart @@ -0,0 +1,51 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:provider/provider.dart'; +import 'package:wger/providers/add_excercise_provider.dart'; + +const validFileExtensions = ['jpg', 'jpeg', 'png', 'webp']; +const maxFileSize = 20; + +mixin ExcerciseImagePickerMixin { + bool _validateFileType(int fileLength) { + final kb = fileLength / 1024; + final mb = kb / 1024; + return mb > maxFileSize; + } + + bool _validateFileSize(File file) { + final extension = file.path.split('.').last; + return validFileExtensions.any((element) => extension == element.toLowerCase()); + } + + void pickImages(BuildContext context) async { + final imagePicker = ImagePicker(); + final images = await imagePicker.pickMultiImage(); + final selectedImages = []; + if (images != null) { + selectedImages.addAll(images.map((e) => File(e.path)).toList()); + + for (final image in selectedImages) { + bool isFileValid = true; + String errorMessage = ''; + + if (!_validateFileSize(image)) { + isFileValid = false; + errorMessage = "Select only 'jpg', 'jpeg', 'png', 'webp' files"; + } + if (_validateFileType(image.lengthSync())) { + isFileValid = true; + errorMessage = 'File Size should not be greater than 20 mb'; + } + + if (!isFileValid) { + showDialog(context: context, builder: (context) => Text(errorMessage)); + return; + } + } + context.read().addExcerciseImages(selectedImages); + } + } +} diff --git a/lib/widgets/add_exercise/preview_images.dart b/lib/widgets/add_exercise/preview_images.dart new file mode 100644 index 000000000..839f2243a --- /dev/null +++ b/lib/widgets/add_exercise/preview_images.dart @@ -0,0 +1,74 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:wger/widgets/add_exercise/mixins/image_picker_mixin.dart'; +import '../../providers/add_excercise_provider.dart'; + +class PreviewExcercizeImages extends StatelessWidget with ExcerciseImagePickerMixin { + PreviewExcercizeImages({ + Key? key, + required this.selectedimages, + }) : super(key: key); + + final List selectedimages; + @override + Widget build(BuildContext context) { + return SizedBox( + height: 300, + child: ListView(scrollDirection: Axis.horizontal, children: [ + ...selectedimages + .map( + (file) => SizedBox( + height: 200, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Stack( + children: [ + Image.file(file), + Positioned( + bottom: 0, + right: 0, + child: Padding( + padding: const EdgeInsets.all(3.0), + child: Container( + decoration: BoxDecoration( + color: Colors.grey.withOpacity(0.5), + borderRadius: const BorderRadius.all(Radius.circular(20))), + child: IconButton( + iconSize: 20, + onPressed: () => + context.read().removeExcercise(file.path), + color: Colors.white, + icon: const Icon(Icons.delete), + ), + ), + ), + ), + ], + ), + ), + ), + ) + .toList(), + const SizedBox( + width: 10, + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: Container( + color: Colors.grey, + height: 200, + width: 100, + child: Center( + child: IconButton( + icon: const Icon(Icons.add), + onPressed: () => pickImages(context), + ), + ), + ), + ) + ]), + ); + } +}