diff --git a/assets/icons/diet.png b/assets/icons/diet.png new file mode 100644 index 00000000..81ebb78e Binary files /dev/null and b/assets/icons/diet.png differ diff --git a/assets/icons/eatvio_logo.svg b/assets/icons/eatvio_logo.svg new file mode 100644 index 00000000..e6ed7018 --- /dev/null +++ b/assets/icons/eatvio_logo.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/fitness.png b/assets/icons/fitness.png new file mode 100644 index 00000000..ef127b81 Binary files /dev/null and b/assets/icons/fitness.png differ diff --git a/assets/icons/paste2.svg b/assets/icons/paste2.svg new file mode 100644 index 00000000..e4e5ac12 --- /dev/null +++ b/assets/icons/paste2.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/assets/icons/slim.png b/assets/icons/slim.png new file mode 100644 index 00000000..e428ede8 Binary files /dev/null and b/assets/icons/slim.png differ diff --git a/assets/icons/yoga-pose.png b/assets/icons/yoga-pose.png new file mode 100644 index 00000000..57b9e680 Binary files /dev/null and b/assets/icons/yoga-pose.png differ diff --git a/lib/core/app_router.dart b/lib/core/app_router.dart index eaae83ab..1cb3b901 100644 --- a/lib/core/app_router.dart +++ b/lib/core/app_router.dart @@ -12,6 +12,8 @@ import 'package:eatvio_app/features/complaining/presentation/complaining_screen. import 'package:eatvio_app/features/consultant/become_consultant/presentation/pages/become_consultant_screen.dart'; import 'package:eatvio_app/features/consultant/become_consultant/presentation/pages/stepper_become_consultant_screen.dart'; import 'package:eatvio_app/features/create_diet/presentation/create_diet_screen.dart'; +import 'package:eatvio_app/features/create_diet/presentation/diet_preview_screen.dart'; +import 'package:eatvio_app/features/create_plan/presentation/create_plan_screen.dart'; import 'package:eatvio_app/features/diary/presentation/pages/diary_screen.dart'; import 'package:eatvio_app/features/diets/presentation/pages/about_diet_screen.dart'; import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; @@ -267,6 +269,21 @@ class AppRouter { name: CreateDietScreen.name, builder: (context, state) => CreateDietScreen(), ), + GoRoute( + path: DietPreviewScreen.path, + name: DietPreviewScreen.name, + builder: (context, state) { + final data = state.extra as Diet; + return DietPreviewScreen( + diet: data, + ); + }, + ), + GoRoute( + path: CreatePlanScreen.path, + name: CreatePlanScreen.name, + builder: (context, state) => CreatePlanScreen(), + ), ], ); } diff --git a/lib/core/const.dart b/lib/core/const.dart index 396c2dce..f7dba8dd 100644 --- a/lib/core/const.dart +++ b/lib/core/const.dart @@ -1 +1,5 @@ const String baseUrl = 'https://dev2.eatvio.com'; + +const List durationValues = [7, 14, 21]; + +const List typeValues = ['veg', 'light', 'carb', 'meat']; diff --git a/lib/core/locator_service.dart b/lib/core/locator_service.dart index 96b60fd3..8d6994f5 100644 --- a/lib/core/locator_service.dart +++ b/lib/core/locator_service.dart @@ -1,5 +1,9 @@ import 'package:dio/dio.dart'; import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/features/create_diet/presentation/cubit/create_diet_cubit.dart'; +import 'package:eatvio_app/features/create_diet/repositories/create_diet_repository.dart'; +import 'package:eatvio_app/features/create_diet/services/create_diet_service.dart'; +import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_cubit.dart'; import 'package:eatvio_app/features/diets/repositories/diet_repository.dart'; import 'package:eatvio_app/features/diets/services/diet_service.dart'; import 'package:eatvio_app/core/initialization_cubit/initialization_cubit.dart'; @@ -76,6 +80,9 @@ Future init() async { sl.registerFactory(() => AboutDietCubit(dietsRepository: sl())); sl.registerFactory(() => BlogPostCreateCubit(blogPostRepository: sl())); sl.registerFactory(() => SubCubit(profileRepository: sl())); + sl.registerFactory(() => CreateDietCubit( profileRepository: sl(),createDietRepository: sl(), glossaryRepository: sl())); + sl.registerFactory(() => CreatePlanCubit()); + // Repository sl.registerLazySingleton(() => ChatRepository(chatService: sl())); @@ -91,6 +98,7 @@ Future init() async { sl.registerLazySingleton(() => GlossaryRepository(glossaryService: sl())); sl.registerLazySingleton(() => DietRepository(dietsService: sl())); sl.registerLazySingleton(() => BlogPostRepository(blogPostService: sl())); + sl.registerLazySingleton(() => CreateDietRepository(createDietService: sl())); // Services sl.registerLazySingleton(() => ChatService(sl())); @@ -106,6 +114,7 @@ Future init() async { sl.registerLazySingleton(() => GlossaryService(sl())); sl.registerLazySingleton(() => DietService(sl())); sl.registerLazySingleton(() => BlogPostService(sl())); + sl.registerLazySingleton(() => CreateDietService(sl())); // External sl.registerLazySingleton( diff --git a/lib/core/theme/custom_text_style.dart b/lib/core/theme/custom_text_style.dart index 91ddffa2..7f00ad9b 100644 --- a/lib/core/theme/custom_text_style.dart +++ b/lib/core/theme/custom_text_style.dart @@ -225,4 +225,10 @@ abstract class TextStyles { fontWeight: FontWeight.w500, height: 1.255, ); + + static const textFS10FW500 = TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + height: 1.255, + ); } diff --git a/lib/core/theme/theme_helper.dart b/lib/core/theme/theme_helper.dart index 63675d02..77f7595e 100644 --- a/lib/core/theme/theme_helper.dart +++ b/lib/core/theme/theme_helper.dart @@ -178,6 +178,10 @@ class PrimaryColors { Color get transparentBlue => Color(0xFF2F80ED).withOpacity(0.1); Color get lightGray0 => Color(0xFFF6F6F6); + + Color get lightGray00 => Color(0xFFE9E9EA); + + Color get dottedBorder => Color(0xFFE6E6E6); } PrimaryColors get appTheme => ThemeHelper().themeColor(); diff --git a/lib/core/utils/toasts.dart b/lib/core/utils/toasts.dart index 2f86daa9..2730d6ce 100644 --- a/lib/core/utils/toasts.dart +++ b/lib/core/utils/toasts.dart @@ -5,11 +5,24 @@ const int _toastDuration = 2; void showInfoToast(String message) { Fluttertoast.showToast( - msg: message, - toastLength: Toast.LENGTH_SHORT, - gravity: ToastGravity.TOP, - timeInSecForIosWeb: _toastDuration, - backgroundColor: appTheme.blueA400, - textColor: appTheme.whiteA700, - fontSize: 16.0,); + msg: message, + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.TOP, + timeInSecForIosWeb: _toastDuration, + backgroundColor: appTheme.blueA400, + textColor: appTheme.whiteA700, + fontSize: 16.0, + ); +} + +void showErrorToast(String message) { + Fluttertoast.showToast( + msg: message, + toastLength: Toast.LENGTH_LONG, + gravity: ToastGravity.TOP, + timeInSecForIosWeb: _toastDuration, + backgroundColor: appTheme.whiteA700, + textColor: appTheme.gray, + fontSize: 16.0, + ); } diff --git a/lib/features/diets/presentation/widgets/cpfc_container.dart b/lib/core/widgets/cpfc_container.dart similarity index 82% rename from lib/features/diets/presentation/widgets/cpfc_container.dart rename to lib/core/widgets/cpfc_container.dart index d4663910..d7e53361 100644 --- a/lib/features/diets/presentation/widgets/cpfc_container.dart +++ b/lib/core/widgets/cpfc_container.dart @@ -4,10 +4,10 @@ import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:flutter/material.dart'; class CpfcContainer extends StatelessWidget { - const CpfcContainer({super.key, required this.calories, required this.protein, required this.fat, required this.carbohydrates}); + const CpfcContainer({super.key, required this.calories, required this.proteins, required this.fats, required this.carbohydrates}); final double calories; - final double protein; - final double fat; + final double proteins; + final double fats; final double carbohydrates; final int roundDigit = 1; @override @@ -27,13 +27,13 @@ class CpfcContainer extends StatelessWidget { ), Column( children: [ - Text('${protein.toStringAsFixed(roundDigit)}', style: TextStyles.textMSemibold.copyWith(color: appTheme.black)), + Text('${proteins.toStringAsFixed(roundDigit)}', style: TextStyles.textMSemibold.copyWith(color: appTheme.black)), Text(s.proteins, style: TextStyles.textXSSemibold.copyWith(color: appTheme.black)), ], ), Column( children: [ - Text('${fat.toStringAsFixed(roundDigit)}', style: TextStyles.textMSemibold.copyWith(color: appTheme.black)), + Text('${fats.toStringAsFixed(roundDigit)}', style: TextStyles.textMSemibold.copyWith(color: appTheme.black)), Text(s.fats, style: TextStyles.textXSSemibold.copyWith(color: appTheme.black)), ], ), diff --git a/lib/core/widgets/custom_drop_down_button.dart b/lib/core/widgets/custom_drop_down_button.dart new file mode 100644 index 00000000..843d6730 --- /dev/null +++ b/lib/core/widgets/custom_drop_down_button.dart @@ -0,0 +1,41 @@ +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; + +class CustomDropDownButton extends StatelessWidget { + const CustomDropDownButton({ + super.key, + required this.items, + required this.onChanged, + this.value, + this.hint, + }); + final T? value; + final List> items; + final void Function(T?)? onChanged; + final Widget? hint; + @override + Widget build(BuildContext context) { + return DropdownButtonFormField( + hint: hint, + icon: SvgPicture.asset( + 'assets/icons/arrow_down2.svg', + width: 12, + height: 8, + ), + borderRadius: BorderRadius.circular(20), + dropdownColor: appTheme.lightGray0, + decoration: InputDecoration( + border: OutlineInputBorder( + borderRadius: const BorderRadius.all(Radius.circular(20)), + borderSide: BorderSide.none, + ), + fillColor: appTheme.lightGray0, + filled: true, + ), + value: value, + items: items, + onChanged: onChanged, + ); + } +} diff --git a/lib/core/widgets/custom_text_form_field.dart b/lib/core/widgets/custom_text_form_field.dart index 2ce5a525..77e7086a 100644 --- a/lib/core/widgets/custom_text_form_field.dart +++ b/lib/core/widgets/custom_text_form_field.dart @@ -13,7 +13,7 @@ class CustomTextFormField extends StatelessWidget { this.obscureText = false, this.textInputAction, this.textInputType, - this.maxLines, + this.maxLines = 1, this.hintText, this.hintStyle, this.prefix, @@ -35,9 +35,12 @@ class CustomTextFormField extends StatelessWidget { this.onEditingComplete, this.minLines = 1, this.maxLength, + this.expands = false, + this.textAlignVertical, + this.counter = const SizedBox.shrink(), super.key, }); - + final Widget? counter; final Alignment? alignment; final double? width; final TextEditingController? controller; @@ -49,7 +52,7 @@ class CustomTextFormField extends StatelessWidget { final TextInputAction? textInputAction; final TextInputType? textInputType; final int? maxLines; - final int minLines; + final int? minLines; final String? hintText; final TextStyle? hintStyle; final Widget? prefix; @@ -69,12 +72,15 @@ class CustomTextFormField extends StatelessWidget { final void Function(String?)? onSaved; final void Function(String)? onChanged; final int? maxLength; - + final bool expands; + final TextAlignVertical? textAlignVertical; @override Widget build(BuildContext context) { return Align( alignment: alignment ?? Alignment.center, - child: TextFormField( + child: TextFormField( + textAlignVertical: textAlignVertical, + expands: expands, minLines: minLines, maxLength: maxLength, onChanged: onChanged, @@ -88,10 +94,11 @@ class CustomTextFormField extends StatelessWidget { ), obscureText: obscureText!, textInputAction: textInputAction, - maxLines: maxLines ?? 1, + maxLines: maxLines, initialValue: initialValue, cursorColor: appTheme.blueA400, decoration: InputDecoration( + counter: counter, suffixText: suffixText, hintText: hintText ?? "", hintStyle: TextStyle( diff --git a/lib/features/diets/presentation/widgets/main_background_bouble.dart b/lib/core/widgets/main_background_bouble.dart similarity index 100% rename from lib/features/diets/presentation/widgets/main_background_bouble.dart rename to lib/core/widgets/main_background_bouble.dart diff --git a/lib/features/create_diet/models/create_diet_model.dart b/lib/features/create_diet/models/create_diet_model.dart index 6517e784..5a53754b 100644 --- a/lib/features/create_diet/models/create_diet_model.dart +++ b/lib/features/create_diet/models/create_diet_model.dart @@ -1,13 +1,15 @@ import 'dart:io'; +import 'package:dio/dio.dart'; import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart'; import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; +import 'package:flutter_image_compress/flutter_image_compress.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; part 'create_diet_model.freezed.dart'; @freezed -class CreateDietModel with _$CreateDietModel{ +class CreateDietModel with _$CreateDietModel { const CreateDietModel._(); const factory CreateDietModel({ @@ -15,21 +17,21 @@ class CreateDietModel with _$CreateDietModel{ String? details, String? state, int? duration, - List? type, - File? image, + required List type, + XFile? image, List>>? dailySchedule, }) = _CreateDietModel; Future> toCreationJson() async => { - 'name': name, - 'duration': duration, - 'state': state, - 'type': type, - 'details': details, - 'image': image != null ? await ImageCompressUtility.compressFile(image!) : null, - }; + 'name': name, + 'duration': duration, + 'state': state, + 'type': type, + 'details': details, + 'image': image != null ? await MultipartFile.fromFile((await ImageCompressUtility.compressFile(File(image!.path))).path, filename: image!.name) : null, + }; Map toDailyScheduleCreationJson() => { - 'daily_schedule': dailySchedule, - }; -} \ No newline at end of file + 'daily_schedule': dailySchedule?.map((element) => element.map((element) => element.map((element) => element.toJson()).toList()).toList()).toList(), + }; +} diff --git a/lib/features/create_diet/models/create_diet_model.freezed.dart b/lib/features/create_diet/models/create_diet_model.freezed.dart index bc3eccbb..05001e64 100644 --- a/lib/features/create_diet/models/create_diet_model.freezed.dart +++ b/lib/features/create_diet/models/create_diet_model.freezed.dart @@ -20,8 +20,8 @@ mixin _$CreateDietModel { String? get details => throw _privateConstructorUsedError; String? get state => throw _privateConstructorUsedError; int? get duration => throw _privateConstructorUsedError; - List? get type => throw _privateConstructorUsedError; - File? get image => throw _privateConstructorUsedError; + List get type => throw _privateConstructorUsedError; + XFile? get image => throw _privateConstructorUsedError; List>>? get dailySchedule => throw _privateConstructorUsedError; @@ -43,8 +43,8 @@ abstract class $CreateDietModelCopyWith<$Res> { String? details, String? state, int? duration, - List? type, - File? image, + List type, + XFile? image, List>>? dailySchedule}); } @@ -67,7 +67,7 @@ class _$CreateDietModelCopyWithImpl<$Res, $Val extends CreateDietModel> Object? details = freezed, Object? state = freezed, Object? duration = freezed, - Object? type = freezed, + Object? type = null, Object? image = freezed, Object? dailySchedule = freezed, }) { @@ -88,14 +88,14 @@ class _$CreateDietModelCopyWithImpl<$Res, $Val extends CreateDietModel> ? _value.duration : duration // ignore: cast_nullable_to_non_nullable as int?, - type: freezed == type + type: null == type ? _value.type : type // ignore: cast_nullable_to_non_nullable - as List?, + as List, image: freezed == image ? _value.image : image // ignore: cast_nullable_to_non_nullable - as File?, + as XFile?, dailySchedule: freezed == dailySchedule ? _value.dailySchedule : dailySchedule // ignore: cast_nullable_to_non_nullable @@ -117,8 +117,8 @@ abstract class _$$CreateDietModelImplCopyWith<$Res> String? details, String? state, int? duration, - List? type, - File? image, + List type, + XFile? image, List>>? dailySchedule}); } @@ -139,7 +139,7 @@ class __$$CreateDietModelImplCopyWithImpl<$Res> Object? details = freezed, Object? state = freezed, Object? duration = freezed, - Object? type = freezed, + Object? type = null, Object? image = freezed, Object? dailySchedule = freezed, }) { @@ -160,14 +160,14 @@ class __$$CreateDietModelImplCopyWithImpl<$Res> ? _value.duration : duration // ignore: cast_nullable_to_non_nullable as int?, - type: freezed == type + type: null == type ? _value._type : type // ignore: cast_nullable_to_non_nullable - as List?, + as List, image: freezed == image ? _value.image : image // ignore: cast_nullable_to_non_nullable - as File?, + as XFile?, dailySchedule: freezed == dailySchedule ? _value._dailySchedule : dailySchedule // ignore: cast_nullable_to_non_nullable @@ -184,7 +184,7 @@ class _$CreateDietModelImpl extends _CreateDietModel { this.details, this.state, this.duration, - final List? type, + required final List type, this.image, final List>>? dailySchedule}) : _type = type, @@ -199,18 +199,16 @@ class _$CreateDietModelImpl extends _CreateDietModel { final String? state; @override final int? duration; - final List? _type; + final List _type; @override - List? get type { - final value = _type; - if (value == null) return null; + List get type { if (_type is EqualUnmodifiableListView) return _type; // ignore: implicit_dynamic_type - return EqualUnmodifiableListView(value); + return EqualUnmodifiableListView(_type); } @override - final File? image; + final XFile? image; final List>>? _dailySchedule; @override List>>? get dailySchedule { @@ -269,8 +267,8 @@ abstract class _CreateDietModel extends CreateDietModel { final String? details, final String? state, final int? duration, - final List? type, - final File? image, + required final List type, + final XFile? image, final List>>? dailySchedule}) = _$CreateDietModelImpl; const _CreateDietModel._() : super._(); @@ -284,9 +282,9 @@ abstract class _CreateDietModel extends CreateDietModel { @override int? get duration; @override - List? get type; + List get type; @override - File? get image; + XFile? get image; @override List>>? get dailySchedule; diff --git a/lib/features/create_diet/presentation/create_diet_screen.dart b/lib/features/create_diet/presentation/create_diet_screen.dart index 81db4990..ed4fd233 100644 --- a/lib/features/create_diet/presentation/create_diet_screen.dart +++ b/lib/features/create_diet/presentation/create_diet_screen.dart @@ -1,4 +1,22 @@ -import 'package:flutter/widgets.dart'; +import 'package:eatvio_app/core/locator_service.dart'; +import 'package:eatvio_app/core/theme/custom_text_style.dart'; +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/core/utils/localization_utils.dart'; +import 'package:eatvio_app/core/widgets/custom_outlined_button.dart'; +import 'package:eatvio_app/core/widgets/custom_text_form_field.dart'; +import 'package:eatvio_app/core/widgets/main_background_bouble.dart'; +import 'package:eatvio_app/features/create_diet/presentation/cubit/create_diet_cubit.dart'; +import 'package:eatvio_app/features/create_diet/presentation/cubit/create_diet_state.dart'; +import 'package:eatvio_app/features/create_diet/presentation/widgets/day_widget.dart'; +import 'package:eatvio_app/features/create_diet/presentation/diet_preview_screen.dart'; +import 'package:eatvio_app/features/create_diet/presentation/widgets/diet_duration_drop_down_widget.dart'; +import 'package:eatvio_app/features/create_diet/presentation/widgets/diet_image_picker_widget.dart'; +import 'package:eatvio_app/features/create_diet/presentation/widgets/status_drop_down_widget.dart'; +import 'package:eatvio_app/features/create_diet/presentation/widgets/diet_type_choice_widget.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; class CreateDietScreen extends StatelessWidget { static const name = 'createDietScreen'; @@ -7,6 +25,154 @@ class CreateDietScreen extends StatelessWidget { @override Widget build(BuildContext context) { - return const Placeholder(); + return BlocProvider( + create: (context) => sl(), + child: const CreateDietScreenContent(), + ); } -} \ No newline at end of file +} + +class CreateDietScreenContent extends StatelessWidget { + const CreateDietScreenContent({super.key}); + + @override + Widget build(BuildContext context) { + final s = S.of(context); + return Scaffold( + backgroundColor: appTheme.gray100, + appBar: AppBar( + centerTitle: true, + title: Text( + s.create_diet, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + softWrap: true, + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), + ), + ), + body: SingleChildScrollView( + child: Column( + children: [ + SizedBox(height: 8), + MainBackgroundBouble( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + children: [ + SizedBox(height: 16), + CustomTextFormField( + hintText: s.enter_diet_name, + onChanged: (value) { + context.read().setName(value); + }, + ), + SizedBox(height: 12), + SizedBox( + height: 132, + child: CustomTextFormField( + textAlignVertical: TextAlignVertical.top, + maxLines: null, + minLines: null, + expands: true, + hintText: s.enter_diet_description, + onChanged: (value) { + context.read().setDetails(value); + }, + ), + ), + SizedBox(height: 12), + DietDurationDropDownWidget(), + SizedBox(height: 12), + StatusDropDownWidget(), + SizedBox(height: 16), + DietTypeChoiceWidget(), + SizedBox(height: 16), + DietImagePickerWidget(), + SizedBox(height: 16), + ], + ), + ), + SizedBox(height: 16), + BlocBuilder( + builder: (context, state) { + return MainBackgroundBouble( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: Center( + child: Text( + s.diet_plan + ' (${state.dietData.duration ?? 0} ' + getDayForm(state.dietData.duration ?? 0, s) + ')', + style: TextStyles.headingXLSemibold.copyWith( + color: appTheme.black, + ), + ), + ), + ), + ); + }, + ), + SizedBox(height: 8), + BlocBuilder( + builder: (context, state) { + return ListView.separated( + padding: const EdgeInsets.all(0), + itemBuilder: (context, index) => DayWidget(index: index), + itemCount: state.dietData.dailySchedule?.length ?? 0, + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + separatorBuilder: (context, index) => SizedBox(height: 8), + ); + }, + ), + SizedBox(height: 20), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + children: [ + CustomOutlinedButton( + onPressed: () { + context.read().setDietState(2); + }, + text: s.to_draft, + height: 52, + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.gray), + buttonStyle: ButtonStyle( + side: WidgetStatePropertyAll(BorderSide.none), + backgroundColor: WidgetStatePropertyAll(appTheme.dottedBorder), + ), + ), + SizedBox(height: 12), + CustomOutlinedButton( + onPressed: () async { + final diet = await context.read().getDietForPreview(); + GoRouter.of(context).pushNamed(DietPreviewScreen.name, extra: diet); + }, + text: s.preview, + height: 52, + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.gray), + buttonStyle: ButtonStyle( + side: WidgetStatePropertyAll(BorderSide.none), + backgroundColor: WidgetStatePropertyAll(appTheme.dottedBorder), + ), + ), + SizedBox(height: 12), + CustomOutlinedButton( + onPressed: () { + context.read().publishDiet(); + }, + text: s.publish, + height: 52, + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), + buttonStyle: ButtonStyle( + side: WidgetStatePropertyAll(BorderSide.none), + backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), + ), + ), + ], + ), + ), + SizedBox(height: 20), + ], + ), + ), + ); + } +} diff --git a/lib/features/create_diet/presentation/cubit/create_diet_cubit.dart b/lib/features/create_diet/presentation/cubit/create_diet_cubit.dart index bc093d41..949241af 100644 --- a/lib/features/create_diet/presentation/cubit/create_diet_cubit.dart +++ b/lib/features/create_diet/presentation/cubit/create_diet_cubit.dart @@ -1,11 +1,20 @@ -import 'dart:io'; - +import 'dart:convert'; +import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/features/create_diet/presentation/cubit/create_diet_state.dart'; +import 'package:eatvio_app/features/create_diet/repositories/create_diet_repository.dart'; import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; +import 'package:eatvio_app/features/glossary/models/glossary_model.dart'; +import 'package:eatvio_app/features/glossary/repositories/glossary_repository.dart'; +import 'package:eatvio_app/features/profile/repositories/profile_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_image_compress/flutter_image_compress.dart'; class CreateDietCubit extends Cubit { - CreateDietCubit() : super(CreateDietState.initial()); + final CreateDietRepository createDietRepository; + final ProfileRepository profileRepository; + final GlossaryRepository glossaryRepository; + static const _dietStateValues = ['active', 'personal', 'draft']; + CreateDietCubit({required this.profileRepository, required this.createDietRepository, required this.glossaryRepository}) : super(CreateDietState.initial()); void setDetails(String text) { emit(state.copyWith(dietData: state.dietData.copyWith(details: text))); @@ -15,11 +24,60 @@ class CreateDietCubit extends Cubit { emit(state.copyWith(dietData: state.dietData.copyWith(name: name))); } - void setDuration(int duration) { - emit(state.copyWith(dietData: state.dietData.copyWith(duration: duration))); + void setDuration(int index) { + final selectedDaysValue = durationValues[index]; + final currentDaysValue = state.dietData.dailySchedule?.length; + final dailySchedule = state.dietData.dailySchedule; + + if (currentDaysValue != null && dailySchedule != null) { + final deepCopyOfDailySchedule = dailySchedule + .map( + (day) => day.map((meal) => [...meal]).toList(), + ) + .toList(); + + if (currentDaysValue > selectedDaysValue) { + final difference = currentDaysValue - selectedDaysValue; + for (int i = 0; i < difference; i++) { + deepCopyOfDailySchedule.removeLast(); + } + } else if (selectedDaysValue > currentDaysValue) { + final difference = selectedDaysValue - currentDaysValue; + for (int i = 0; i < difference; i++) { + deepCopyOfDailySchedule.add([[], [], [], [], []]); + } + } + emit( + state.copyWith( + dietData: state.dietData.copyWith( + dailySchedule: deepCopyOfDailySchedule, + ), + ), + ); + } else { + final List>> dailySchedule = List.generate(selectedDaysValue, (index) => [[], [], [], [], []]); + + emit( + state.copyWith( + dietData: state.dietData.copyWith(dailySchedule: dailySchedule), + ), + ); + } + emit( + state.copyWith( + durationIndex: index, + dietData: state.dietData.copyWith( + duration: durationValues[index], + ), + ), + ); } - void setImage(File image) { + Future> getGlossaryListByQuery(String query) async { + return await glossaryRepository.getGlossaryListByQuery(query); + } + + void setImage(XFile? image) { emit(state.copyWith(dietData: state.dietData.copyWith(image: image))); } @@ -27,11 +85,182 @@ class CreateDietCubit extends Cubit { emit(state.copyWith(dietData: state.dietData.copyWith(dailySchedule: dailySchedule))); } - void setDietState(String dietState) { - emit(state.copyWith(dietData: state.dietData.copyWith(state: dietState))); + void setDietState(int index) { + emit(state.copyWith(dietStateIndex: index, dietData: state.dietData.copyWith(state: _dietStateValues[index]))); } - void setDietType(List type) { + void setDietType(List type) { emit(state.copyWith(dietData: state.dietData.copyWith(type: type))); } + + void addPortion({required int dayIndex, required int mealIndex, required DailySchedule portion}) { + final dailySchedule = state.dietData.dailySchedule; + if (dailySchedule != null) { + final deepCopyOfDailySchedule = dailySchedule + .map( + (day) => day.map((meal) => [...meal]).toList(), + ) + .toList(); + deepCopyOfDailySchedule[dayIndex][mealIndex].add(portion); + + final newDietData = state.dietData.copyWith(dailySchedule: deepCopyOfDailySchedule); + emit(state.copyWith(dietData: newDietData)); + } + } + + void removePortion({ + required int day, + required int meal, + required int portionIndex, + }) { + final dailySchedule = state.dietData.dailySchedule; + if (dailySchedule != null) { + final deepCopyOfDailySchedule = dailySchedule + .map( + (day) => day.map((meal) => [...meal]).toList(), + ) + .toList(); + deepCopyOfDailySchedule[day][meal].removeAt(portionIndex); + + final newDietData = state.dietData.copyWith(dailySchedule: deepCopyOfDailySchedule); + emit(state.copyWith(dietData: newDietData)); + } + } + + void updatePortion({required int dayIndex, required int mealIndex, required int portionIndex, required DailySchedule portion}) { + final dailySchedule = state.dietData.dailySchedule; + if (dailySchedule != null) { + final deepCopyOfDailySchedule = dailySchedule + .map( + (day) => day.map((meal) => [...meal]).toList(), + ) + .toList(); + deepCopyOfDailySchedule[dayIndex][mealIndex][portionIndex] = portion; + + final newDietData = state.dietData.copyWith(dailySchedule: deepCopyOfDailySchedule); + emit(state.copyWith(dietData: newDietData)); + } + } + + void copyDay(int dayIndex) { + final dailySchedule = state.dietData.dailySchedule; + if (dailySchedule != null) { + final deepCopyOfDailySchedule = dailySchedule + .map( + (day) => day.map((meal) => [...meal]).toList(), + ) + .toList(); + final copiedDay = deepCopyOfDailySchedule[dayIndex]; + emit(state.copyWith(copiedDay: copiedDay)); + } + } + + void pasteCopiedDay(int dayIndex) { + final dailySchedule = state.dietData.dailySchedule; + final copiedDay = state.copiedDay; + if (dailySchedule != null && copiedDay != null) { + final deepCopyOfDailySchedule = dailySchedule + .map( + (day) => day.map((meal) => [...meal]).toList(), + ) + .toList(); + deepCopyOfDailySchedule[dayIndex] = copiedDay; + final newDietData = state.dietData.copyWith(dailySchedule: deepCopyOfDailySchedule); + emit(state.copyWith(dietData: newDietData)); + } + } + + Future getDietForPreview() async { + final authorProfile = await profileRepository.getMyProfile(); + final imageBytes = (await state.dietData.image?.readAsBytes()) ?? []; + final imageInBase64 = base64.encode(imageBytes); + final carbs = state.dietData.dailySchedule?.map((element) { + return element.fold(0, (prev, element) { + return prev + + element.fold(0, (prev, element) { + return prev + (element.dailyNutrition?.carbohydrates ?? 0); + }); + }); + }).toList(); + + final calories = state.dietData.dailySchedule?.map((element) { + return element.fold(0, (prev, element) { + return prev + + element.fold(0, (prev, element) { + return prev + (element.dailyNutrition?.energy ?? 0); + }); + }); + }).toList(); + + final fats = state.dietData.dailySchedule?.map((element) { + return element.fold(0, (prev, element) { + return prev + + element.fold(0, (prev, element) { + return prev + (element.dailyNutrition?.fats ?? 0); + }); + }); + }).toList(); + + final proteins = state.dietData.dailySchedule?.map((element) { + return element.fold(0, (prev, element) { + return prev + + element.fold(0, (prev, element) { + return prev + (element.dailyNutrition?.proteins ?? 0); + }); + }); + }).toList(); + final dailyNutrition = []; + if (carbs != null && calories != null && fats != null && proteins != null) { + for (int i = 0; i < carbs.length; i++) { + dailyNutrition.add( + DailyNutrition( + carbohydrates: carbs[i], + energy: calories[i], + fats: fats[i], + proteins: proteins[i], + ), + ); + } + } + final diet = Diet( + id: 0, + name: state.dietData.name ?? '', + image: imageInBase64, + duration: state.dietData.duration ?? 0, + state: state.dietData.state ?? '', + type: DietType( + meat: state.dietData.type[0], + veg: state.dietData.type[1], + carb: state.dietData.type[2], + light: state.dietData.type[3], + ), + details: state.dietData.details ?? '', + dailySchedule: state.dietData.dailySchedule ?? [], + author: Author(authorSlug: authorProfile.slug, authorName: authorProfile.name, authorProfileIcon: authorProfile.profilePhotoUrl, authorIsMe: false), + totalLikes: 0, + totalViews: 0, + totalComments: 0, + wasLikedByMe: false, + isFavorite: false, + dailyNutrition: dailyNutrition, + ); + return diet; + } + + Future publishDiet() async { + // бек судя по всему с багом + // try { + // final model = await state.dietData.toCreationJson(); + // model.remove('image'); + // final dailySchedule = await state.dietData.toDailyScheduleCreationJson(); + // final dio = sl(); + // final qwe = await dio.post('/api/diets/create', data: model); + // // final diet = await createDietRepository.createDiet(model: model); + // // final qwe2 = createDietRepository.createDietDailySchedule(id: diet.id, model: dailySchedule); + // } catch (e, stackTrace) { + // print(e); + // print('---------------------------------'); + // print(stackTrace); + // } + } } diff --git a/lib/features/create_diet/presentation/cubit/create_diet_state.dart b/lib/features/create_diet/presentation/cubit/create_diet_state.dart index 61ac70e9..df571110 100644 --- a/lib/features/create_diet/presentation/cubit/create_diet_state.dart +++ b/lib/features/create_diet/presentation/cubit/create_diet_state.dart @@ -1,15 +1,19 @@ import 'package:eatvio_app/features/create_diet/models/create_diet_model.dart'; +import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; part 'create_diet_state.freezed.dart'; @freezed class CreateDietState with _$CreateDietState { - const factory CreateDietState({ + const factory CreateDietState({ required bool isLoading, required String? errorMessage, - required CreateDietModel dietData, + required CreateDietModel dietData, + List>? copiedDay, + int? durationIndex, + int? dietStateIndex, }) = _CreateDietState; - factory CreateDietState.initial() => CreateDietState(isLoading: false, errorMessage: null, dietData: CreateDietModel()); + factory CreateDietState.initial() => CreateDietState(isLoading: false, errorMessage: null, dietData: CreateDietModel(type: [false, false, false, false])); } diff --git a/lib/features/create_diet/presentation/cubit/create_diet_state.freezed.dart b/lib/features/create_diet/presentation/cubit/create_diet_state.freezed.dart index 0a540986..c58a70e9 100644 --- a/lib/features/create_diet/presentation/cubit/create_diet_state.freezed.dart +++ b/lib/features/create_diet/presentation/cubit/create_diet_state.freezed.dart @@ -19,6 +19,10 @@ mixin _$CreateDietState { bool get isLoading => throw _privateConstructorUsedError; String? get errorMessage => throw _privateConstructorUsedError; CreateDietModel get dietData => throw _privateConstructorUsedError; + List>? get copiedDay => + throw _privateConstructorUsedError; + int? get durationIndex => throw _privateConstructorUsedError; + int? get dietStateIndex => throw _privateConstructorUsedError; /// Create a copy of CreateDietState /// with the given fields replaced by the non-null parameter values. @@ -33,7 +37,13 @@ abstract class $CreateDietStateCopyWith<$Res> { CreateDietState value, $Res Function(CreateDietState) then) = _$CreateDietStateCopyWithImpl<$Res, CreateDietState>; @useResult - $Res call({bool isLoading, String? errorMessage, CreateDietModel dietData}); + $Res call( + {bool isLoading, + String? errorMessage, + CreateDietModel dietData, + List>? copiedDay, + int? durationIndex, + int? dietStateIndex}); $CreateDietModelCopyWith<$Res> get dietData; } @@ -56,6 +66,9 @@ class _$CreateDietStateCopyWithImpl<$Res, $Val extends CreateDietState> Object? isLoading = null, Object? errorMessage = freezed, Object? dietData = null, + Object? copiedDay = freezed, + Object? durationIndex = freezed, + Object? dietStateIndex = freezed, }) { return _then(_value.copyWith( isLoading: null == isLoading @@ -70,6 +83,18 @@ class _$CreateDietStateCopyWithImpl<$Res, $Val extends CreateDietState> ? _value.dietData : dietData // ignore: cast_nullable_to_non_nullable as CreateDietModel, + copiedDay: freezed == copiedDay + ? _value.copiedDay + : copiedDay // ignore: cast_nullable_to_non_nullable + as List>?, + durationIndex: freezed == durationIndex + ? _value.durationIndex + : durationIndex // ignore: cast_nullable_to_non_nullable + as int?, + dietStateIndex: freezed == dietStateIndex + ? _value.dietStateIndex + : dietStateIndex // ignore: cast_nullable_to_non_nullable + as int?, ) as $Val); } @@ -92,7 +117,13 @@ abstract class _$$CreateDietStateImplCopyWith<$Res> __$$CreateDietStateImplCopyWithImpl<$Res>; @override @useResult - $Res call({bool isLoading, String? errorMessage, CreateDietModel dietData}); + $Res call( + {bool isLoading, + String? errorMessage, + CreateDietModel dietData, + List>? copiedDay, + int? durationIndex, + int? dietStateIndex}); @override $CreateDietModelCopyWith<$Res> get dietData; @@ -114,6 +145,9 @@ class __$$CreateDietStateImplCopyWithImpl<$Res> Object? isLoading = null, Object? errorMessage = freezed, Object? dietData = null, + Object? copiedDay = freezed, + Object? durationIndex = freezed, + Object? dietStateIndex = freezed, }) { return _then(_$CreateDietStateImpl( isLoading: null == isLoading @@ -128,6 +162,18 @@ class __$$CreateDietStateImplCopyWithImpl<$Res> ? _value.dietData : dietData // ignore: cast_nullable_to_non_nullable as CreateDietModel, + copiedDay: freezed == copiedDay + ? _value._copiedDay + : copiedDay // ignore: cast_nullable_to_non_nullable + as List>?, + durationIndex: freezed == durationIndex + ? _value.durationIndex + : durationIndex // ignore: cast_nullable_to_non_nullable + as int?, + dietStateIndex: freezed == dietStateIndex + ? _value.dietStateIndex + : dietStateIndex // ignore: cast_nullable_to_non_nullable + as int?, )); } } @@ -138,7 +184,11 @@ class _$CreateDietStateImpl implements _CreateDietState { const _$CreateDietStateImpl( {required this.isLoading, required this.errorMessage, - required this.dietData}); + required this.dietData, + final List>? copiedDay, + this.durationIndex, + this.dietStateIndex}) + : _copiedDay = copiedDay; @override final bool isLoading; @@ -146,10 +196,24 @@ class _$CreateDietStateImpl implements _CreateDietState { final String? errorMessage; @override final CreateDietModel dietData; + final List>? _copiedDay; + @override + List>? get copiedDay { + final value = _copiedDay; + if (value == null) return null; + if (_copiedDay is EqualUnmodifiableListView) return _copiedDay; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(value); + } + + @override + final int? durationIndex; + @override + final int? dietStateIndex; @override String toString() { - return 'CreateDietState(isLoading: $isLoading, errorMessage: $errorMessage, dietData: $dietData)'; + return 'CreateDietState(isLoading: $isLoading, errorMessage: $errorMessage, dietData: $dietData, copiedDay: $copiedDay, durationIndex: $durationIndex, dietStateIndex: $dietStateIndex)'; } @override @@ -162,12 +226,24 @@ class _$CreateDietStateImpl implements _CreateDietState { (identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage) && (identical(other.dietData, dietData) || - other.dietData == dietData)); + other.dietData == dietData) && + const DeepCollectionEquality() + .equals(other._copiedDay, _copiedDay) && + (identical(other.durationIndex, durationIndex) || + other.durationIndex == durationIndex) && + (identical(other.dietStateIndex, dietStateIndex) || + other.dietStateIndex == dietStateIndex)); } @override - int get hashCode => - Object.hash(runtimeType, isLoading, errorMessage, dietData); + int get hashCode => Object.hash( + runtimeType, + isLoading, + errorMessage, + dietData, + const DeepCollectionEquality().hash(_copiedDay), + durationIndex, + dietStateIndex); /// Create a copy of CreateDietState /// with the given fields replaced by the non-null parameter values. @@ -183,7 +259,10 @@ abstract class _CreateDietState implements CreateDietState { const factory _CreateDietState( {required final bool isLoading, required final String? errorMessage, - required final CreateDietModel dietData}) = _$CreateDietStateImpl; + required final CreateDietModel dietData, + final List>? copiedDay, + final int? durationIndex, + final int? dietStateIndex}) = _$CreateDietStateImpl; @override bool get isLoading; @@ -191,6 +270,12 @@ abstract class _CreateDietState implements CreateDietState { String? get errorMessage; @override CreateDietModel get dietData; + @override + List>? get copiedDay; + @override + int? get durationIndex; + @override + int? get dietStateIndex; /// Create a copy of CreateDietState /// with the given fields replaced by the non-null parameter values. diff --git a/lib/features/create_diet/presentation/diet_preview_screen.dart b/lib/features/create_diet/presentation/diet_preview_screen.dart new file mode 100644 index 00000000..2da26a46 --- /dev/null +++ b/lib/features/create_diet/presentation/diet_preview_screen.dart @@ -0,0 +1,474 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; +import 'package:eatvio_app/core/utils/localization_utils.dart'; +import 'package:eatvio_app/core/utils/string_extentions.dart'; +import 'package:eatvio_app/core/widgets/bookmark_button.dart'; +import 'package:eatvio_app/core/widgets/custom_divider.dart'; +import 'package:eatvio_app/core/widgets/diet_info_row.dart'; +import 'package:eatvio_app/features/diets/presentation/widgets/author_card.dart'; +import 'package:eatvio_app/core/widgets/cpfc_container.dart'; +import 'package:eatvio_app/features/diets/presentation/widgets/expandable_text.dart'; +import 'package:eatvio_app/features/diets/presentation/widgets/info_bouble.dart'; +import 'package:eatvio_app/core/widgets/main_background_bouble.dart'; +import 'package:eatvio_app/features/diets/presentation/widgets/more_options.dart'; +import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:eatvio_app/core/theme/custom_text_style.dart'; +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_portal/flutter_portal.dart'; +import 'package:flutter_svg/svg.dart'; + +class DietPreviewScreen extends StatefulWidget { + final Diet diet; + late final List selectedIndexes; + static const name = 'createDietScreenPreview'; + static const path = '/create-diet-screen/preview'; + DietPreviewScreen({ + super.key, + required this.diet, + }) { + selectedIndexes = List.generate(diet.dailySchedule.length, (_) => 0); + } + + @override + State createState() => _DietPreviewScreenState(); +} + +class _DietPreviewScreenState extends State { + final ScrollController scrollController = ScrollController(); + final textEditingController = TextEditingController(); + bool isMoreOptionsVisible = false; + late final Uint8List imageData; + @override + void initState() { + super.initState(); + imageData = base64.decode(widget.diet.image); + scrollController.addListener(() { + if (isMoreOptionsVisible) { + setState(() { + isMoreOptionsVisible = false; + }); + } + }); + } + + @override + Widget build(BuildContext context) { + final s = S.of(context); + return Scaffold( + backgroundColor: appTheme.gray100, + resizeToAvoidBottomInset: true, + bottomSheet: Container( + padding: EdgeInsets.symmetric(vertical: 12) + (Platform.isIOS ? const EdgeInsets.only(bottom: 16) : EdgeInsets.zero), + decoration: BoxDecoration( + color: Colors.white, + border: Border( + top: BorderSide( + color: Colors.black.withOpacity(0.1), + ), + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + Expanded( + child: SizedBox( + child: FloatingActionButton.extended( + heroTag: 'nextButtonRegistration', + highlightElevation: 0, + elevation: 0, + focusElevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + backgroundColor: appTheme.greenA700, + onPressed: () {}, + label: Text( + s.startDiet, + style: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), + ), + ), + ), + ), + SizedBox(width: 8), + BookmarkButton( + isFavorite: widget.diet.isFavorite, + onTap: () {}, + width: 52, + height: 52, + size: 24, + shape: BoxShape.circle, + color: appTheme.gray50, + ), + ], + ), + SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: SizedBox( + child: TextFormField( + onChanged: (_) { + setState(() {}); + }, + controller: textEditingController, + maxLines: 5, + minLines: 1, + keyboardType: TextInputType.multiline, + style: TextStyles.textMMedium.copyWith(color: appTheme.black), + decoration: InputDecoration( + isDense: true, + contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), + hintText: s.comment.capitalize(), + hintStyle: TextStyles.textMMedium.copyWith(color: appTheme.lightGray600), + filled: true, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + borderSide: BorderSide( + color: Color(0xFFD1D1D6), + width: 1, + ), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + borderSide: BorderSide( + color: Color(0xFFD1D1D6), + width: 1, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + borderSide: BorderSide( + color: Color(0xFFD1D1D6), + width: 1, + ), + ), + ), + ), + ), + ), + SizedBox(width: 8), + GestureDetector( + onTap: () {}, + child: Container( + decoration: BoxDecoration( + color: textEditingController.text.length > 2 ? appTheme.blueA400 : appTheme.gray50, + shape: BoxShape.circle, + ), + height: 32, + width: 32, + child: Icon( + Icons.send_rounded, + size: 18, + color: textEditingController.text.length > 2 ? appTheme.whiteA700 : appTheme.black, + ), + ), + ), + ], + ), + ], + ), + ), + ), + appBar: AppBar( + centerTitle: true, + title: Text( + widget.diet.name, + maxLines: 2, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + softWrap: true, + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), + ), + actions: [ + PortalTarget( + visible: isMoreOptionsVisible, + portalFollower: MoreOptions(disableActions: true, id: widget.diet.id), + anchor: Aligned( + shiftToWithinBound: AxisFlag(x: true, y: true), + follower: Alignment.topRight, + target: Alignment.bottomRight, + ), + child: IconButton( + onPressed: () { + setState(() { + isMoreOptionsVisible = !isMoreOptionsVisible; + }); + }, + icon: SvgPicture.asset( + 'assets/icons/more_button.svg', + width: 20, + height: 20, + ), + ), + ), + ], + ), + body: SingleChildScrollView( + controller: scrollController, + padding: EdgeInsets.only(bottom: 160), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 8), + MainBackgroundBouble( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Stack( + children: [ + Image.memory( + imageData, + height: 300, + width: MediaQuery.sizeOf(context).width, + fit: BoxFit.fill, + ), + // CachedNetworkImage( + // imageUrl: baseUrl + '/storage/' + widget.diet.image, + // ), + Positioned( + bottom: 16, + left: 0, + right: 0, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + InfoBouble( + children: [ + SvgPicture.asset('assets/icons/eye.svg', width: 18, height: 12), + SizedBox(width: 8), + Text( + widget.diet.totalViews.toString(), + style: TextStyles.textXSSemibold.copyWith(color: appTheme.black), + ), + ], + backgroundColor: appTheme.whiteA700, + ), + SizedBox(width: 12), + GestureDetector( + onTap: () {}, + child: InfoBouble( + children: [ + SvgPicture.asset( + 'assets/icons/heart.svg', + width: 14, + height: 12, + colorFilter: widget.diet.wasLikedByMe ? ColorFilter.mode(appTheme.blueA400, BlendMode.srcIn) : null, + ), + SizedBox(width: 8), + Text( + widget.diet.totalLikes.toString(), + style: TextStyles.textXSSemibold.copyWith(color: appTheme.black), + ), + ], + backgroundColor: appTheme.whiteA700, + ), + ), + SizedBox(width: 12), + InfoBouble( + children: [ + SvgPicture.asset('assets/icons/comment2.svg', width: 13, height: 12), + SizedBox(width: 8), + Text( + widget.diet.totalComments.toString(), + style: TextStyles.textXSSemibold.copyWith(color: appTheme.black), + ), + ], + backgroundColor: appTheme.whiteA700, + ), + ], + ), + ), + ], + ), + SizedBox(height: 16), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Text( + s.about_diet, + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), + ), + ), + SizedBox(height: 12), + DietInfoRow(diet: widget.diet), + ExpandableText( + text: widget.diet.details, + ), + SizedBox(height: 20), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: AuthorCard( + author: widget.diet.author, + ), + ), + SizedBox(height: 16), + ], + ), + ), + SizedBox(height: 16), + MainBackgroundBouble( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: Center( + child: Text( + s.diet_plan + '(${widget.diet.duration.toString()} ' + getDayForm(widget.diet.duration, s) + ')', + style: TextStyles.headingXLSemibold.copyWith( + color: appTheme.black, + ), + ), + ), + ), + ), + SizedBox(height: 8), + ListView.builder( + physics: NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemBuilder: (context, index) { + return _dayOfPlan(widget.diet.dailySchedule[index], index, s); + }, + itemCount: widget.diet.dailySchedule.length, + ), + MainBackgroundBouble( + padding: EdgeInsets.symmetric(vertical: 16), + child: Column( + children: [ + Container( + padding: EdgeInsets.symmetric(horizontal: 16), + alignment: Alignment.centerLeft, + child: Text( + '0' + ' ' + getCommentForm(widget.diet.totalComments, s), + style: TextStyles.textMMedium.copyWith( + color: appTheme.gray500, + ), + ), + ), + SizedBox(height: 16), + ], + ), + ), + ], + ), + ), + ); + } + + _dayOfPlan(List> day, int dayNumber, S s) { + _choiceChipWidget(int index) { + return ChoiceChip( + selectedColor: appTheme.blueA400, + pressElevation: 0, + showCheckmark: false, + side: BorderSide.none, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + label: Text((index + 1).toString()), + labelStyle: TextStyles.textXSSemibold.copyWith( + color: index == widget.selectedIndexes[dayNumber] ? Colors.white : appTheme.gray600, + ), + selected: widget.selectedIndexes[dayNumber] == index, + backgroundColor: appTheme.gray100, + onSelected: (bool selected) { + setState(() { + widget.selectedIndexes[dayNumber] = index; + }); + }, + ); + } + + return Padding( + padding: const EdgeInsets.only(bottom: 8), + child: MainBackgroundBouble( + padding: EdgeInsets.only( + top: 16, + ), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + s.day + ' ' + (dayNumber + 1).toString(), + style: TextStyles.headingXLSemibold.copyWith( + color: appTheme.black, + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: CustomDivider(), + ), + Text( + s.meal, + style: TextStyles.textMMedium.copyWith( + color: appTheme.black, + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _choiceChipWidget(0), + _choiceChipWidget(1), + _choiceChipWidget(2), + _choiceChipWidget(3), + _choiceChipWidget(4), + ], + ), + SizedBox(height: 8), + ListView.builder( + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + itemBuilder: (context, index) => _daily(day[widget.selectedIndexes[dayNumber]][index], s), + itemCount: day[widget.selectedIndexes[dayNumber]].length, + ), + SizedBox(height: 8), + ], + ), + ), + CpfcContainer( + calories: widget.diet.dailyNutrition[dayNumber].energy, + proteins: widget.diet.dailyNutrition[dayNumber].proteins, + fats: widget.diet.dailyNutrition[dayNumber].fats, + carbohydrates: widget.diet.dailyNutrition[dayNumber].carbohydrates, + ), + ], + ), + ), + ); + } + + _daily(DailySchedule daily, S s) { + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: Row( + children: [ + SizedBox( + width: MediaQuery.sizeOf(context).width * 0.6, + child: Text( + daily.name, + style: TextStyles.textMMedium.copyWith( + color: appTheme.black, + ), + ), + ), + Spacer(), + Text( + daily.quantity.toString() + ' ' + getLocalizedMeasurementUnits(s, daily.portion), + style: TextStyles.textMSemibold.copyWith( + color: appTheme.black, + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/create_diet/presentation/widgets/add_meal_widget.dart b/lib/features/create_diet/presentation/widgets/add_meal_widget.dart new file mode 100644 index 00000000..f705adb4 --- /dev/null +++ b/lib/features/create_diet/presentation/widgets/add_meal_widget.dart @@ -0,0 +1,294 @@ +import 'package:eatvio_app/core/locator_service.dart'; +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/core/widgets/custom_text_form_field.dart'; +import 'package:eatvio_app/features/create_diet/presentation/cubit/create_diet_cubit.dart'; +import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart'; +import 'package:eatvio_app/features/diary/presentation/widgets/glossary_moderation_request_modal_widget.dart'; +import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; +import 'package:eatvio_app/features/glossary/models/glossary_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_typeahead/flutter_typeahead.dart'; + +class AddMealWidget extends StatefulWidget { + const AddMealWidget({ + super.key, + required this.createDietCubit, + this.portionIndex, + this.productNameControllerText, + this.quantityControllerText, + required this.dayIndex, + required this.mealIndex, + }); + final CreateDietCubit createDietCubit; + final String? productNameControllerText; + final String? quantityControllerText; + final int? portionIndex; + final int dayIndex; + final int mealIndex; + @override + State createState() => _AddMealWidgetState(); +} + +class _AddMealWidgetState extends State { + GlossaryModel? selectedGlossary; + final productNameController = TextEditingController(); + final quantityController = TextEditingController(); + + @override + void initState() { + super.initState(); + productNameController.text = widget.productNameControllerText ?? ''; + quantityController.text = widget.quantityControllerText ?? ''; + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16) + + EdgeInsets.only( + bottom: MediaQuery.of(context).viewInsets.bottom, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Добавление блюда', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w600, + color: Color(0xFF323F4B), + ), + ), + const SizedBox(height: 16), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + width: MediaQuery.of(context).size.width * 0.8 - 32, + child: TypeAheadField( + controller: productNameController, + itemBuilder: (context, glossary) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + glossary.name, + style: TextStyle( + height: 1, + fontSize: 16, + ), + ), + ); + }, + itemSeparatorBuilder: (context, index) => _divider(context), + onSelected: (newSelectedGlossary) async { + setState(() { + selectedGlossary = newSelectedGlossary; + productNameController.text = selectedGlossary!.name; + }); + }, + suggestionsCallback: (query) async { + if (query.length < 1) return []; + final suggestionsList = await widget.createDietCubit.getGlossaryListByQuery(query); + return suggestionsList; + }, + builder: (context, controller, focusNode) { + return CustomTextFormField( + controller: controller, + autofocus: true, + focusNode: focusNode, + hintText: 'Введите название продукта/блюда', + ); + }, + emptyBuilder: (context) { + return const SizedBox.shrink(); + }, + errorBuilder: (context, object) => Padding( + padding: const EdgeInsets.all(8.0), + child: Text('Нет подходящих вариантов'), + ), + ), + ), + const SizedBox(width: 4), + SizedBox( + width: MediaQuery.of(context).size.width * 0.2 - 4, + child: CustomTextFormField( + controller: quantityController, + keyboardType: TextInputType.number, + hintText: selectedGlossary == null + ? '100' + : selectedGlossary!.grInPortion == 0 + ? '100' + : '1', + suffixText: selectedGlossary == null + ? 'гр' + : selectedGlossary?.grInPortion == 0 + ? 'гр' + : 'шт', + ), + ), + ], + ), + const SizedBox(height: 16), + Row( + children: [ + if (widget.portionIndex != null) + InkWell( + onTap: () { + widget.createDietCubit.removePortion( + day: widget.dayIndex, + meal: widget.mealIndex, + portionIndex: widget.portionIndex!, + ); + Navigator.of(context).pop(); + }, + borderRadius: BorderRadius.circular(20), + child: Ink( + width: 52, + height: 52, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + color: Color(0xFFF6F6F6), + ), + child: Icon( + Icons.delete_outline_rounded, + color: Colors.red, + ), + ), + ), + if (widget.portionIndex != null) const SizedBox(width: 8), + InkWell( + onTap: addPortion, + borderRadius: BorderRadius.circular(20), + child: Ink( + width: MediaQuery.of(context).size.width - (widget.portionIndex == null ? 32 : 92), + height: 52, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + color: appTheme.blueA400, + ), + child: Center( + child: Text( + widget.portionIndex == null ? 'Добавить' : 'Сохранить', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Colors.white, + ), + ), + ), + ), + ), + ], + ), + ], + ), + ); + } + + Future addPortion() async { + final portionIndex = widget.portionIndex; + final measurementUnits = selectedGlossary?.grInPortion == null + ? Portion.PCS + : selectedGlossary?.grInPortion == 0 + ? Portion.GRAMS + : Portion.PCS; + final glossary = selectedGlossary; + if (glossary != null) { + final [calories, carbohydrates, proteins, fats] = _getCPFC(glossary, measurementUnits); + final portion = DailySchedule( + dailyNutrition: DailyNutrition(energy: calories, proteins: proteins, fats: fats, carbohydrates: carbohydrates), + quantity: int.parse(quantityController.text), + name: glossary.name, + uniq: '', + productNutritionalValueId: glossary.id.toString(), + portion: measurementUnits, + ); + if (portionIndex == null) { + widget.createDietCubit.addPortion( + dayIndex: widget.dayIndex, + mealIndex: widget.mealIndex, + portion: portion, + ); + } else { + widget.createDietCubit.updatePortion( + dayIndex: widget.dayIndex, + mealIndex: widget.mealIndex, + portionIndex: portionIndex, + portion: portion, + ); + } + } else { + Navigator.of(context).pop(); + final newGlossary = await showModalBottomSheet( + isScrollControlled: true, + useRootNavigator: true, + context: context, + builder: (context) => Padding( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).viewInsets.bottom, + ), + child: GlossaryModerationRequestModalWidget( + productName: productNameController.text, + diaryCubit: sl(), + ), + ), + ); + if (newGlossary != null) { + final [calories, carbohydrates, proteins, fats] = _getCPFC(newGlossary, Portion.GRAMS); + final portion = DailySchedule( + dailyNutrition: DailyNutrition(energy: calories, proteins: proteins, fats: fats, carbohydrates: carbohydrates), + quantity: int.parse(quantityController.text), + name: newGlossary.name, + uniq: '', + productNutritionalValueId: newGlossary.id.toString(), + portion: measurementUnits, + ); + if (portionIndex == null) { + widget.createDietCubit.addPortion( + dayIndex: widget.dayIndex, + mealIndex: widget.mealIndex, + portion: portion, + ); + } else { + widget.createDietCubit.updatePortion( + dayIndex: widget.dayIndex, + mealIndex: widget.mealIndex, + portion: portion, + portionIndex: portionIndex, + ); + } + } + } + } + + List _getCPFC(GlossaryModel glossary, Portion measurementUnits) { + final double calories; + final double carbohydrates; + final double proteins; + final double fats; + if (measurementUnits == Portion.GRAMS) { + calories = (glossary.energyOn100Gr * int.parse(quantityController.text) / 100); + carbohydrates = (glossary.carbohydratesOn100Gr * int.parse(quantityController.text) / 100); + proteins = (glossary.proteinsOn100Gr * int.parse(quantityController.text) / 100); + fats = (glossary.fatsOn100Gr * int.parse(quantityController.text) / 100); + } else { + calories = (glossary.energyOnPortion * int.parse(quantityController.text)); + carbohydrates = (glossary.carbohydratesOnPortion * int.parse(quantityController.text)); + proteins = (glossary.proteinsOnPortion * int.parse(quantityController.text)); + fats = (glossary.fatsOnPortion * int.parse(quantityController.text)); + } + return [calories, carbohydrates, proteins, fats]; + } + + _divider(BuildContext context) { + return Container( + height: 0.4, + width: MediaQuery.of(context).size.width - 32, + decoration: BoxDecoration( + color: Colors.black26, + borderRadius: BorderRadius.circular(16), + ), + ); + } +} diff --git a/lib/features/create_diet/presentation/widgets/day_widget.dart b/lib/features/create_diet/presentation/widgets/day_widget.dart new file mode 100644 index 00000000..732d6a7a --- /dev/null +++ b/lib/features/create_diet/presentation/widgets/day_widget.dart @@ -0,0 +1,338 @@ +import 'package:eatvio_app/core/theme/custom_text_style.dart'; +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/core/utils/localization_utils.dart'; +import 'package:eatvio_app/core/utils/toasts.dart'; +import 'package:eatvio_app/core/widgets/cpfc_container.dart'; +import 'package:eatvio_app/core/widgets/custom_divider.dart'; +import 'package:eatvio_app/core/widgets/custom_outlined_button.dart'; +import 'package:eatvio_app/core/widgets/main_background_bouble.dart'; +import 'package:eatvio_app/features/create_diet/presentation/cubit/create_diet_cubit.dart'; +import 'package:eatvio_app/features/create_diet/presentation/cubit/create_diet_state.dart'; +import 'package:eatvio_app/features/create_diet/presentation/widgets/add_meal_widget.dart'; +import 'package:eatvio_app/features/feed/presentation/widgets/choice_chip_widget.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/svg.dart'; + +class DayWidget extends StatefulWidget { + DayWidget({ + super.key, + required this.index, + }); + final int index; + @override + State createState() => _DayWidgetState(); +} + +class _DayWidgetState extends State { + int _selectedMealIndex = 0; + final chipsLabelList = ['1', '2', '3', '4', '5']; + @override + Widget build(BuildContext context) { + final s = S.of(context); + return BlocBuilder( + builder: (context, state) { + return MainBackgroundBouble( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(left: 16, right: 16, top: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + s.day + ' ${widget.index + 1}', + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), + ), + Spacer(), + GestureDetector( + onTap: () { + context.read().copyDay(widget.index); + showInfoToast('${s.day} ${widget.index + 1} ${s.copied}'); + }, + child: SvgPicture.asset( + 'assets/icons/copy2.svg', + width: 20, + height: 24, + colorFilter: ColorFilter.mode(appTheme.black, BlendMode.srcIn), + ), + ), + ], + ), + SizedBox( + height: 12, + ), + CustomDivider(), + SizedBox( + height: 12, + ), + Text( + s.meal, + style: TextStyles.textMMedium.copyWith(color: appTheme.black), + ), + SizedBox(height: 16), + Row( + children: [ + ChoiceChipWidget( + chipsLabelList: chipsLabelList, + padding: EdgeInsets.symmetric(horizontal: 16, vertical: 6), + fontSize: TextStyles.textMMedium.fontSize, + fontWeight: TextStyles.textMMedium.fontWeight, + index: 0, + selectedChipIndex: _selectedMealIndex, + onSelected: (selectedValue) { + setState(() { + _selectedMealIndex = 0; + }); + }, + ), + Spacer(), + ChoiceChipWidget( + chipsLabelList: chipsLabelList, + padding: EdgeInsets.symmetric(horizontal: 16, vertical: 6), + fontSize: TextStyles.textMMedium.fontSize, + fontWeight: TextStyles.textMMedium.fontWeight, + index: 1, + selectedChipIndex: _selectedMealIndex, + onSelected: (selectedValue) { + setState(() { + _selectedMealIndex = 1; + }); + }, + ), + Spacer(), + ChoiceChipWidget( + chipsLabelList: chipsLabelList, + padding: EdgeInsets.symmetric(horizontal: 16, vertical: 6), + fontSize: TextStyles.textMMedium.fontSize, + fontWeight: TextStyles.textMMedium.fontWeight, + index: 2, + selectedChipIndex: _selectedMealIndex, + onSelected: (selectedValue) { + setState(() { + _selectedMealIndex = 2; + }); + }, + ), + Spacer(), + ChoiceChipWidget( + chipsLabelList: chipsLabelList, + padding: EdgeInsets.symmetric(horizontal: 16, vertical: 6), + fontSize: TextStyles.textMMedium.fontSize, + fontWeight: TextStyles.textMMedium.fontWeight, + index: 3, + selectedChipIndex: _selectedMealIndex, + onSelected: (selectedValue) { + setState(() { + _selectedMealIndex = 3; + }); + }, + ), + Spacer(), + ChoiceChipWidget( + chipsLabelList: chipsLabelList, + padding: EdgeInsets.symmetric(horizontal: 16, vertical: 6), + fontSize: TextStyles.textMMedium.fontSize, + fontWeight: TextStyles.textMMedium.fontWeight, + index: 4, + selectedChipIndex: _selectedMealIndex, + onSelected: (selectedValue) { + setState(() { + _selectedMealIndex = 4; + }); + }, + ), + ], + ), + ListView.separated( + separatorBuilder: (context, index) => SizedBox( + height: 8, + ), + padding: EdgeInsets.all(0), + itemBuilder: (context, index) { + final product = state.dietData.dailySchedule?[widget.index][_selectedMealIndex][index]; + if (product != null) { + return _product( + index, + product.name, + product.quantity, + getLocalizedMeasurementUnits( + s, + product.portion, + ), + widget.index, + _selectedMealIndex, + ); + } + return null; + }, + itemCount: state.dietData.dailySchedule?[widget.index][_selectedMealIndex].length ?? 0, + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + ), + SizedBox(height: 16), + Row( + children: [ + GestureDetector( + onTap: () { + context.read().pasteCopiedDay(widget.index); + }, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 12, vertical: 14), + decoration: BoxDecoration(color: appTheme.gray50, borderRadius: BorderRadius.all(Radius.circular(12))), + child: SvgPicture.asset( + 'assets/icons/paste2.svg', + width: 28, + height: 24, + ), + ), + ), + SizedBox(width: 8), + Expanded( + child: CustomOutlinedButton( + onPressed: () { + _modalBottomSheet( + dayIndex: widget.index, + mealIndex: _selectedMealIndex, + createDietCubit: context.read(), + ); + }, + text: s.add, + height: 52, + buttonTextStyle: TextStyles.textMSemibold.copyWith( + color: appTheme.whiteA700, + ), + buttonStyle: ButtonStyle( + side: WidgetStatePropertyAll(BorderSide.none), + backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), + ), + ), + ), + ], + ), + SizedBox(height: 20), + ], + ), + ), + BlocBuilder( + buildWhen: (previous, current) => previous.dietData.dailySchedule != current.dietData.dailySchedule, + builder: (context, state) { + if (state.dietData.dailySchedule != null) { + final day = state.dietData.dailySchedule?[widget.index]; + final double calories = day!.fold( + 0, + (previousValue, element) => + previousValue + + element.fold( + 0, + (previousValue, element) => previousValue + (element.dailyNutrition?.energy ?? 0), + ), + ); + + final double proteins = day.fold( + 0, + (previousValue, element) => + previousValue + + element.fold( + 0, + (previousValue, element) => previousValue + (element.dailyNutrition?.proteins ?? 0), + ), + ); + + final double fats = day.fold( + 0, + (previousValue, element) => + previousValue + + element.fold( + 0, + (previousValue, element) => previousValue + (element.dailyNutrition?.fats ?? 0), + ), + ); + + final double carbohydrates = day.fold( + 0, + (previousValue, element) => + previousValue + + element.fold( + 0, + (previousValue, element) => previousValue + (element.dailyNutrition?.carbohydrates ?? 0), + ), + ); + return CpfcContainer( + calories: calories, + proteins: proteins, + fats: fats, + carbohydrates: carbohydrates, + ); + } + return CpfcContainer( + calories: 0, + proteins: 0, + fats: 0, + carbohydrates: 0, + ); + }, + ), + ], + ), + ); + }, + ); + } + + _product(int index, String product, int quantity, String portion, int dayIndex, int mealIndex) { + return Material( + child: ListTile( + onTap: () { + _modalBottomSheet( + mealIndex: mealIndex, + dayIndex: dayIndex, + createDietCubit: context.read(), + portionIndex: index, + productNameControllerText: product, + quantityControllerText: quantity.toString(), + ); + }, + trailing: Icon(Icons.more_vert_rounded), + title: Text(product), + subtitle: Text('$quantity $portion'), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + tileColor: appTheme.gray50, + ), + ); + } + + _modalBottomSheet({ + required CreateDietCubit createDietCubit, + required int dayIndex, + required int mealIndex, + int? portionIndex, + String? productNameControllerText, + String? quantityControllerText, + }) { + return showModalBottomSheet( + useRootNavigator: true, + context: context, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(12)), + ), + isScrollControlled: true, + builder: (context) { + return AddMealWidget( + dayIndex: dayIndex, + createDietCubit: createDietCubit, + portionIndex: portionIndex, + productNameControllerText: productNameControllerText, + quantityControllerText: quantityControllerText, + mealIndex: mealIndex, + ); + }, + ); + } +} diff --git a/lib/features/create_diet/presentation/widgets/diet_duration_drop_down_widget.dart b/lib/features/create_diet/presentation/widgets/diet_duration_drop_down_widget.dart new file mode 100644 index 00000000..fda76a4f --- /dev/null +++ b/lib/features/create_diet/presentation/widgets/diet_duration_drop_down_widget.dart @@ -0,0 +1,65 @@ +import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/theme/custom_text_style.dart'; +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/core/utils/localization_utils.dart'; +import 'package:eatvio_app/features/create_diet/presentation/cubit/create_diet_cubit.dart'; +import 'package:eatvio_app/features/create_diet/presentation/cubit/create_diet_state.dart'; +import 'package:eatvio_app/core/widgets/custom_drop_down_button.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class DietDurationDropDownWidget extends StatefulWidget { + const DietDurationDropDownWidget({super.key}); + + @override + State createState() => _DietDurationDropDownWidgetState(); +} + +class _DietDurationDropDownWidgetState extends State { + final List durationDropDownTitles = []; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + final s = S.of(context); + if (durationDropDownTitles.isEmpty) { + durationValues.forEach((element) { + durationDropDownTitles.add('$element ${getDayForm(element, s)}'); + }); + } + } + + @override + Widget build(BuildContext context) { + final s = S.of(context); + return BlocBuilder( + builder: (context, state) { + return CustomDropDownButton( + hint: Text( + s.choose_diet_duration, + style: TextStyles.textMMedium.copyWith(color: appTheme.black), + ), + onChanged: (value) { + if (value != null) { + context.read().setDuration(durationDropDownTitles.indexOf(value)); + } + }, + items: durationDropDownTitles + .mapIndexed( + (index, value) => DropdownMenuItem( + child: Text( + durationDropDownTitles[index], + style: TextStyles.textMMedium.copyWith(color: appTheme.black), + ), + value: durationDropDownTitles[index], + ), + ) + .toList(), + value: state.durationIndex == null ? null : durationDropDownTitles[state.durationIndex ?? 0], + ); + }, + ); + } +} diff --git a/lib/features/create_diet/presentation/widgets/diet_image_picker_widget.dart b/lib/features/create_diet/presentation/widgets/diet_image_picker_widget.dart new file mode 100644 index 00000000..42ba88a2 --- /dev/null +++ b/lib/features/create_diet/presentation/widgets/diet_image_picker_widget.dart @@ -0,0 +1,145 @@ +import 'dart:io'; + +import 'package:dotted_border/dotted_border.dart'; +import 'package:eatvio_app/core/theme/custom_text_style.dart'; +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/features/create_diet/presentation/cubit/create_diet_cubit.dart'; +import 'package:eatvio_app/features/create_diet/presentation/cubit/create_diet_state.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:image_picker/image_picker.dart'; + +class DietImagePickerWidget extends StatefulWidget { + const DietImagePickerWidget({super.key}); + + @override + State createState() => _DietImagePickerWidgetState(); +} + +class _DietImagePickerWidgetState extends State { + void _showImagePickerOptions(S s, CreateDietCubit cubit) { + showCupertinoModalPopup( + context: context, + builder: (context) => CupertinoActionSheet( + actions: [ + CupertinoActionSheetAction( + onPressed: () async { + final picker = ImagePicker(); + final image = await picker.pickImage(source: ImageSource.camera); + cubit.setImage(image); + Navigator.of(context).pop(); + }, + child: Row( + children: [ + Icon(Icons.photo_camera), + SizedBox(width: 8), + Text(s.photo), + ], + ), + ), + CupertinoActionSheetAction( + onPressed: () async { + final picker = ImagePicker(); + final image = await picker.pickImage(source: ImageSource.gallery); + cubit.setImage(image); + Navigator.of(context).pop(); + }, + child: Row( + children: [ + Icon(Icons.photo), + SizedBox(width: 8), + Text(s.gallery), + ], + ), + ), + ], + cancelButton: CupertinoActionSheetAction( + onPressed: () => Navigator.of(context).pop(), + isDestructiveAction: true, + child: const Text('Отмена'), + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + final s = S.of(context); + return BlocBuilder( + builder: (context, state) { + return GestureDetector( + onTap: () { + _showImagePickerOptions(s, context.read()); + }, + child: Container( + height: 240, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + image: state.dietData.image != null ? DecorationImage(fit: BoxFit.cover, image: FileImage(File(state.dietData.image?.path ?? ''))) : null, + color: appTheme.gray50, + ), + width: MediaQuery.sizeOf(context).width, + padding: state.dietData.image != null ? null : EdgeInsets.all(8), + child: state.dietData.image != null + ? Padding( + padding: const EdgeInsets.only(top: 6, right: 6), + child: GestureDetector( + onTap: () { + context.read().setImage(null); + }, + child: Align( + alignment: Alignment.topRight, + child: Container( + width: 34, + height: 34, + decoration: BoxDecoration( + color: appTheme.dividerColor, + shape: BoxShape.circle, + ), + child: Icon( + Icons.close_rounded, + color: Colors.grey, + size: 16, + ), + ), + ), + ), + ) + : Stack( + children: [ + DottedBorder( + stackFit: StackFit.expand, + dashPattern: [14, 14], + color: appTheme.dottedBorder, + borderType: BorderType.RRect, + radius: Radius.circular(12), + child: SizedBox.expand(), + ), + Center( + child: Column( + children: [ + SizedBox( + height: 48, + ), + SvgPicture.asset('assets/icons/eatvio_logo.svg'), + SizedBox( + height: 24, + ), + Text( + s.add_cover, + style: TextStyles.textMMedium.copyWith(color: appTheme.gray), + ), + ], + ), + ), + ], + ), + ), + ); + }, + ); + } +} diff --git a/lib/features/create_diet/presentation/widgets/diet_type_choice_widget.dart b/lib/features/create_diet/presentation/widgets/diet_type_choice_widget.dart new file mode 100644 index 00000000..fd324885 --- /dev/null +++ b/lib/features/create_diet/presentation/widgets/diet_type_choice_widget.dart @@ -0,0 +1,100 @@ +import 'package:eatvio_app/core/theme/custom_text_style.dart'; +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/features/create_diet/presentation/cubit/create_diet_cubit.dart'; +import 'package:eatvio_app/features/create_diet/presentation/cubit/create_diet_state.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/svg.dart'; + +class DietTypeChoiceWidget extends StatefulWidget { + DietTypeChoiceWidget({super.key}); + + @override + State createState() => _DietTypeChoiceWidgetState(); +} + +class _DietTypeChoiceWidgetState extends State { + final List<(String, String)> choiceContent = []; + @override + void didChangeDependencies() { + super.didChangeDependencies(); + final s = S.of(context); + if (choiceContent.isEmpty) { + choiceContent.add( + ( + 'assets/icons/meat2.svg', + s.meat, + ), + ); + choiceContent.add( + ( + 'assets/icons/broccoli.svg', + s.vegan, + ), + ); + choiceContent.add( + ( + 'assets/icons/bread.svg', + s.carb_type, + ), + ); + choiceContent.add( + ( + 'assets/icons/cocktail.svg', + s.light_type, + ), + ); + } + } + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return Row( + children: [ + ...List.generate(4, (index) { + return Expanded( + child: GestureDetector( + onTap: () { + final dietTypes = [...state.dietData.type]; + dietTypes[index] = !dietTypes[index]; + context.read().setDietType(dietTypes); + }, + child: Container( + margin: EdgeInsets.only(right: index == 3 ? 0 : 4), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5), + color: state.dietData.type[index] ? appTheme.cardBackground : appTheme.whiteA700, + border: Border.all( + width: 1, + color: state.dietData.type[index] ? appTheme.blueA400 : appTheme.lightGray00, + ), + ), + height: 80, + child: Column( + children: [ + SizedBox(height: 16), + SvgPicture.asset( + choiceContent[index].$1, + height: 30, + ), + SizedBox(height: 8), + Text( + choiceContent[index].$2, + overflow: TextOverflow.clip, + style: TextStyles.textXSSemibold.copyWith(color: appTheme.black), + ), + ], + ), + ), + ), + ); + }), + ], + ); + }, + ); + } +} diff --git a/lib/features/create_diet/presentation/widgets/status_drop_down_widget.dart b/lib/features/create_diet/presentation/widgets/status_drop_down_widget.dart new file mode 100644 index 00000000..d2db75ee --- /dev/null +++ b/lib/features/create_diet/presentation/widgets/status_drop_down_widget.dart @@ -0,0 +1,62 @@ +import 'package:eatvio_app/core/theme/custom_text_style.dart'; +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/features/create_diet/presentation/cubit/create_diet_cubit.dart'; +import 'package:eatvio_app/features/create_diet/presentation/cubit/create_diet_state.dart'; +import 'package:eatvio_app/core/widgets/custom_drop_down_button.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:collection/collection.dart'; + +class StatusDropDownWidget extends StatefulWidget { + const StatusDropDownWidget({super.key}); + + @override + State createState() => _StatusDropDownWidgetState(); +} + +class _StatusDropDownWidgetState extends State { + final List dietStateTitles = []; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + final s = S.of(context); + if (dietStateTitles.isEmpty) { + dietStateTitles.add(s.active_diet_state); + dietStateTitles.add(s.personal_diet_state); + } + } + + @override + Widget build(BuildContext context) { + final s = S.of(context); + return BlocBuilder( + builder: (context, state) { + return CustomDropDownButton( + hint: Text( + s.diet_state, + style: TextStyles.textMMedium.copyWith(color: appTheme.black), + ), + onChanged: (value) { + if (value != null) { + context.read().setDietState(dietStateTitles.indexOf(value)); + } + }, + items: dietStateTitles + .mapIndexed( + (index, value) => DropdownMenuItem( + child: Text( + dietStateTitles[index], + style: TextStyles.textMMedium.copyWith(color: appTheme.black), + ), + value: dietStateTitles[index], + ), + ) + .toList(), + value: state.dietStateIndex == null ? null : dietStateTitles[state.dietStateIndex ?? 0], + ); + }, + ); + } +} diff --git a/lib/features/create_diet/repositories/create_diet_repository.dart b/lib/features/create_diet/repositories/create_diet_repository.dart new file mode 100644 index 00000000..4400d1c8 --- /dev/null +++ b/lib/features/create_diet/repositories/create_diet_repository.dart @@ -0,0 +1,25 @@ +import 'package:eatvio_app/features/create_diet/services/create_diet_service.dart'; +import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; + +class CreateDietRepository { + final CreateDietService createDietService; + CreateDietRepository({required this.createDietService}); + + Future createDiet({ + required Map model, + }) { + return createDietService.createDiet( + model: model, + ); + } + + Future createDietDailySchedule({ + required int id, + required Map model, + }) { + return createDietService.createDietDailySchedule( + id: id, + model: model, + ); + } +} diff --git a/lib/features/create_diet/services/create_diet_service.dart b/lib/features/create_diet/services/create_diet_service.dart index 93ec038d..03ab2a78 100644 --- a/lib/features/create_diet/services/create_diet_service.dart +++ b/lib/features/create_diet/services/create_diet_service.dart @@ -1,12 +1,22 @@ import 'package:dio/dio.dart'; +import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; import 'package:retrofit/retrofit.dart'; part 'create_diet_service.g.dart'; @RestApi() abstract class CreateDietService { - factory CreateDietService(Dio dio, {String baseUrl}) = _CreateDietService; + @POST('/api/diets/create') + @MultiPart() + Future createDiet({ + @Body() required Map model, + }); + + @POST('/api/diets/create/{id}/daily_schedule') + Future createDietDailySchedule({ + @Path() required int id, + @Body() required Map model, + }); } - \ No newline at end of file diff --git a/lib/features/create_diet/services/create_diet_service.g.dart b/lib/features/create_diet/services/create_diet_service.g.dart index a706620d..41ed51e7 100644 --- a/lib/features/create_diet/services/create_diet_service.g.dart +++ b/lib/features/create_diet/services/create_diet_service.g.dart @@ -21,6 +21,70 @@ class _CreateDietService implements CreateDietService { final ParseErrorLogger? errorLogger; + @override + Future createDiet({required Map model}) async { + final _extra = {}; + final queryParameters = {}; + final _headers = {}; + final _data = {}; + _data.addAll(model); + final _options = _setStreamType(Options( + method: 'POST', + headers: _headers, + extra: _extra, + contentType: 'multipart/form-data', + ) + .compose( + _dio.options, + '/api/diets/create', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + ))); + final _result = await _dio.fetch>(_options); + late Diet _value; + try { + _value = Diet.fromJson(_result.data!); + } on Object catch (e, s) { + errorLogger?.logError(e, s, _options); + rethrow; + } + return _value; + } + + @override + Future createDietDailySchedule({ + required int id, + required Map model, + }) async { + final _extra = {}; + final queryParameters = {}; + final _headers = {}; + final _data = {}; + _data.addAll(model); + final _options = _setStreamType(Options( + method: 'POST', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/api/diets/create/${id}/daily_schedule', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + ))); + await _dio.fetch(_options); + } + RequestOptions _setStreamType(RequestOptions requestOptions) { if (T != dynamic && !(requestOptions.responseType == ResponseType.bytes || diff --git a/lib/features/create_plan/models/create_plan_model.dart b/lib/features/create_plan/models/create_plan_model.dart new file mode 100644 index 00000000..c4d3ed21 --- /dev/null +++ b/lib/features/create_plan/models/create_plan_model.dart @@ -0,0 +1,36 @@ +import 'dart:io'; + +import 'package:dio/dio.dart'; +import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart'; +import 'package:eatvio_app/features/plans/models/plan_pagination_model.dart'; +import 'package:flutter_image_compress/flutter_image_compress.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'create_plan_model.freezed.dart'; + +@freezed +class CreatePlanModel with _$CreatePlanModel { + const CreatePlanModel._(); + + const factory CreatePlanModel({ + String? name, + String? details, + String? state, + int? duration, + required List type, + XFile? image, + required List options, + }) = _CreatePlanModel; + + Future> toCreationJson() async => { + 'name': name, + 'duration': duration, + 'state': state, + 'type': type, + 'details': details, + 'image': image != null ? await MultipartFile.fromFile((await ImageCompressUtility.compressFile(File(image!.path))).path, filename: image!.name) : null, + }; + + Map toOptionCreationJson() => {'packet1_options': options[0]}; + Map toOptionsCreationJson() => {'packet1_options': options[0], 'packet2_options': options[1], 'packet3_options': options[2]}; +} diff --git a/lib/features/create_plan/models/create_plan_model.freezed.dart b/lib/features/create_plan/models/create_plan_model.freezed.dart new file mode 100644 index 00000000..226b3072 --- /dev/null +++ b/lib/features/create_plan/models/create_plan_model.freezed.dart @@ -0,0 +1,292 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'create_plan_model.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$CreatePlanModel { + String? get name => throw _privateConstructorUsedError; + String? get details => throw _privateConstructorUsedError; + String? get state => throw _privateConstructorUsedError; + int? get duration => throw _privateConstructorUsedError; + List get type => throw _privateConstructorUsedError; + XFile? get image => throw _privateConstructorUsedError; + List get options => throw _privateConstructorUsedError; + + /// Create a copy of CreatePlanModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $CreatePlanModelCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $CreatePlanModelCopyWith<$Res> { + factory $CreatePlanModelCopyWith( + CreatePlanModel value, $Res Function(CreatePlanModel) then) = + _$CreatePlanModelCopyWithImpl<$Res, CreatePlanModel>; + @useResult + $Res call( + {String? name, + String? details, + String? state, + int? duration, + List type, + XFile? image, + List options}); +} + +/// @nodoc +class _$CreatePlanModelCopyWithImpl<$Res, $Val extends CreatePlanModel> + implements $CreatePlanModelCopyWith<$Res> { + _$CreatePlanModelCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of CreatePlanModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? name = freezed, + Object? details = freezed, + Object? state = freezed, + Object? duration = freezed, + Object? type = null, + Object? image = freezed, + Object? options = null, + }) { + return _then(_value.copyWith( + name: freezed == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String?, + details: freezed == details + ? _value.details + : details // ignore: cast_nullable_to_non_nullable + as String?, + state: freezed == state + ? _value.state + : state // ignore: cast_nullable_to_non_nullable + as String?, + duration: freezed == duration + ? _value.duration + : duration // ignore: cast_nullable_to_non_nullable + as int?, + type: null == type + ? _value.type + : type // ignore: cast_nullable_to_non_nullable + as List, + image: freezed == image + ? _value.image + : image // ignore: cast_nullable_to_non_nullable + as XFile?, + options: null == options + ? _value.options + : options // ignore: cast_nullable_to_non_nullable + as List, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$CreatePlanModelImplCopyWith<$Res> + implements $CreatePlanModelCopyWith<$Res> { + factory _$$CreatePlanModelImplCopyWith(_$CreatePlanModelImpl value, + $Res Function(_$CreatePlanModelImpl) then) = + __$$CreatePlanModelImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {String? name, + String? details, + String? state, + int? duration, + List type, + XFile? image, + List options}); +} + +/// @nodoc +class __$$CreatePlanModelImplCopyWithImpl<$Res> + extends _$CreatePlanModelCopyWithImpl<$Res, _$CreatePlanModelImpl> + implements _$$CreatePlanModelImplCopyWith<$Res> { + __$$CreatePlanModelImplCopyWithImpl( + _$CreatePlanModelImpl _value, $Res Function(_$CreatePlanModelImpl) _then) + : super(_value, _then); + + /// Create a copy of CreatePlanModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? name = freezed, + Object? details = freezed, + Object? state = freezed, + Object? duration = freezed, + Object? type = null, + Object? image = freezed, + Object? options = null, + }) { + return _then(_$CreatePlanModelImpl( + name: freezed == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String?, + details: freezed == details + ? _value.details + : details // ignore: cast_nullable_to_non_nullable + as String?, + state: freezed == state + ? _value.state + : state // ignore: cast_nullable_to_non_nullable + as String?, + duration: freezed == duration + ? _value.duration + : duration // ignore: cast_nullable_to_non_nullable + as int?, + type: null == type + ? _value._type + : type // ignore: cast_nullable_to_non_nullable + as List, + image: freezed == image + ? _value.image + : image // ignore: cast_nullable_to_non_nullable + as XFile?, + options: null == options + ? _value._options + : options // ignore: cast_nullable_to_non_nullable + as List, + )); + } +} + +/// @nodoc + +class _$CreatePlanModelImpl extends _CreatePlanModel { + const _$CreatePlanModelImpl( + {this.name, + this.details, + this.state, + this.duration, + required final List type, + this.image, + required final List options}) + : _type = type, + _options = options, + super._(); + + @override + final String? name; + @override + final String? details; + @override + final String? state; + @override + final int? duration; + final List _type; + @override + List get type { + if (_type is EqualUnmodifiableListView) return _type; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_type); + } + + @override + final XFile? image; + final List _options; + @override + List get options { + if (_options is EqualUnmodifiableListView) return _options; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_options); + } + + @override + String toString() { + return 'CreatePlanModel(name: $name, details: $details, state: $state, duration: $duration, type: $type, image: $image, options: $options)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$CreatePlanModelImpl && + (identical(other.name, name) || other.name == name) && + (identical(other.details, details) || other.details == details) && + (identical(other.state, state) || other.state == state) && + (identical(other.duration, duration) || + other.duration == duration) && + const DeepCollectionEquality().equals(other._type, _type) && + (identical(other.image, image) || other.image == image) && + const DeepCollectionEquality().equals(other._options, _options)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + name, + details, + state, + duration, + const DeepCollectionEquality().hash(_type), + image, + const DeepCollectionEquality().hash(_options)); + + /// Create a copy of CreatePlanModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$CreatePlanModelImplCopyWith<_$CreatePlanModelImpl> get copyWith => + __$$CreatePlanModelImplCopyWithImpl<_$CreatePlanModelImpl>( + this, _$identity); +} + +abstract class _CreatePlanModel extends CreatePlanModel { + const factory _CreatePlanModel( + {final String? name, + final String? details, + final String? state, + final int? duration, + required final List type, + final XFile? image, + required final List options}) = _$CreatePlanModelImpl; + const _CreatePlanModel._() : super._(); + + @override + String? get name; + @override + String? get details; + @override + String? get state; + @override + int? get duration; + @override + List get type; + @override + XFile? get image; + @override + List get options; + + /// Create a copy of CreatePlanModel + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$CreatePlanModelImplCopyWith<_$CreatePlanModelImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/features/create_plan/presentation/create_plan_screen.dart b/lib/features/create_plan/presentation/create_plan_screen.dart new file mode 100644 index 00000000..5eda713c --- /dev/null +++ b/lib/features/create_plan/presentation/create_plan_screen.dart @@ -0,0 +1,145 @@ +import 'package:eatvio_app/core/locator_service.dart'; +import 'package:eatvio_app/core/theme/custom_text_style.dart'; +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/core/widgets/custom_text_form_field.dart'; +import 'package:eatvio_app/core/widgets/main_background_bouble.dart'; +import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_cubit.dart'; +import 'package:eatvio_app/features/create_plan/presentation/widgets/options_count_button.dart'; +import 'package:eatvio_app/features/create_plan/presentation/widgets/plan_duration_drop_down_widget.dart'; +import 'package:eatvio_app/features/create_plan/presentation/widgets/plan_image_picker_widget.dart'; +import 'package:eatvio_app/features/create_plan/presentation/widgets/plan_type_choice_widget.dart'; +import 'package:eatvio_app/features/create_plan/presentation/widgets/select_exact_option_widget.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class CreatePlanScreen extends StatelessWidget { + static const name = 'createPlanScreen'; + static const path = '/create-Plan-screen'; + const CreatePlanScreen({super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => sl(), + child: const CreatePlanScreenContent(), + ); + } +} + +class CreatePlanScreenContent extends StatelessWidget { + const CreatePlanScreenContent({super.key}); + + @override + Widget build(BuildContext context) { + final s = S.of(context); + return Scaffold( + backgroundColor: appTheme.gray100, + appBar: AppBar( + centerTitle: true, + title: Text( + s.adding_plan, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + softWrap: true, + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), + ), + ), + body: Stack( + children: [ + SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 8), + MainBackgroundBouble( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 16), + Text( + s.label, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + softWrap: true, + style: TextStyles.textSMedium.copyWith(color: appTheme.black), + ), + SizedBox(height: 4), + CustomTextFormField( + hintText: s.plan_label_desc, + onChanged: (value) { + context.read().setName(value); + }, + maxLength: 150, + ), + SizedBox(height: 12), + Text( + s.plan_duration, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + softWrap: true, + style: TextStyles.textSMedium.copyWith(color: appTheme.black), + ), + SizedBox(height: 4), + PlanDurationDropDownWidget(), + SizedBox(height: 20), + PlanImagePickerWidget(), + SizedBox(height: 16), + PlanTypeChoiceWidget(), + SizedBox(height: 16), + Text( + s.plan_desc, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + softWrap: true, + style: TextStyles.textSMedium.copyWith(color: appTheme.black), + ), + SizedBox(height: 4), + SizedBox( + height: 132, + child: CustomTextFormField( + textAlignVertical: TextAlignVertical.top, + maxLength: 1000, + maxLines: null, + minLines: null, + expands: true, + hintText: s.plan_desc_example, + onChanged: (value) { + context.read().setDetails(value); + }, + ), + ), + SizedBox(height: 16), + ], + ), + ), + SizedBox(height: 20), + Padding( + padding: const EdgeInsets.only(left: 16), + child: Text( + s.plan_desc, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + softWrap: true, + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.blueGray400), + ), + ), + SizedBox(height: 8), + OptionsCountButton(), + SizedBox(height: 200), + ], + ), + ), + Padding( + padding: const EdgeInsets.only(bottom: 24.0), + child: Align( + alignment: Alignment.bottomCenter, + child: SelectExactOptionWidget(), + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/create_plan/presentation/cubit/create_plan_cubit.dart b/lib/features/create_plan/presentation/cubit/create_plan_cubit.dart new file mode 100644 index 00000000..19f782dd --- /dev/null +++ b/lib/features/create_plan/presentation/cubit/create_plan_cubit.dart @@ -0,0 +1,43 @@ +import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_state.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_image_compress/flutter_image_compress.dart'; + +class CreatePlanCubit extends Cubit { + CreatePlanCubit() : super(CreatePlanState.initial()); + + void setDetails(String text) { + emit(state.copyWith(planData: state.planData.copyWith(details: text))); + } + + void setName(String name) { + emit(state.copyWith(planData: state.planData.copyWith(name: name))); + } + + void setDuration(int index) { + emit( + state.copyWith( + durationIndex: index, + planData: state.planData.copyWith( + duration: durationValues[index], + ), + ), + ); + } + + void setImage(XFile? image) { + emit(state.copyWith(planData: state.planData.copyWith(image: image))); + } + + void setPlanState(String planState) { + emit(state.copyWith(planData: state.planData.copyWith(state: planState))); + } + + void setPlanType(List type) { + emit(state.copyWith(planData: state.planData.copyWith(type: type))); + } + + void setSelectedOptionIndex(int? index) { + emit(state.copyWith(selectedOptionIndex: index)); + } +} diff --git a/lib/features/create_plan/presentation/cubit/create_plan_state.dart b/lib/features/create_plan/presentation/cubit/create_plan_state.dart new file mode 100644 index 00000000..fc8edf18 --- /dev/null +++ b/lib/features/create_plan/presentation/cubit/create_plan_state.dart @@ -0,0 +1,25 @@ +import 'package:eatvio_app/features/create_plan/models/create_plan_model.dart'; +import 'package:eatvio_app/features/plans/models/plan_pagination_model.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'create_plan_state.freezed.dart'; + +@freezed +class CreatePlanState with _$CreatePlanState { + const factory CreatePlanState({ + required bool isLoading, + required String? errorMessage, + required CreatePlanModel planData, + int? durationIndex, + int? selectedOptionIndex, + }) = _CreatePlanState; + + factory CreatePlanState.initial() => CreatePlanState( + isLoading: false, + errorMessage: null, + planData: CreatePlanModel( + type: [false, false, false, false], + options: [PacketOptions.empty(), PacketOptions.empty(), PacketOptions.empty()], + ), + ); +} diff --git a/lib/features/create_plan/presentation/cubit/create_plan_state.freezed.dart b/lib/features/create_plan/presentation/cubit/create_plan_state.freezed.dart new file mode 100644 index 00000000..bb7eba17 --- /dev/null +++ b/lib/features/create_plan/presentation/cubit/create_plan_state.freezed.dart @@ -0,0 +1,249 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'create_plan_state.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$CreatePlanState { + bool get isLoading => throw _privateConstructorUsedError; + String? get errorMessage => throw _privateConstructorUsedError; + CreatePlanModel get planData => throw _privateConstructorUsedError; + int? get durationIndex => throw _privateConstructorUsedError; + int? get selectedOptionIndex => throw _privateConstructorUsedError; + + /// Create a copy of CreatePlanState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $CreatePlanStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $CreatePlanStateCopyWith<$Res> { + factory $CreatePlanStateCopyWith( + CreatePlanState value, $Res Function(CreatePlanState) then) = + _$CreatePlanStateCopyWithImpl<$Res, CreatePlanState>; + @useResult + $Res call( + {bool isLoading, + String? errorMessage, + CreatePlanModel planData, + int? durationIndex, + int? selectedOptionIndex}); + + $CreatePlanModelCopyWith<$Res> get planData; +} + +/// @nodoc +class _$CreatePlanStateCopyWithImpl<$Res, $Val extends CreatePlanState> + implements $CreatePlanStateCopyWith<$Res> { + _$CreatePlanStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of CreatePlanState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? isLoading = null, + Object? errorMessage = freezed, + Object? planData = null, + Object? durationIndex = freezed, + Object? selectedOptionIndex = freezed, + }) { + return _then(_value.copyWith( + isLoading: null == isLoading + ? _value.isLoading + : isLoading // ignore: cast_nullable_to_non_nullable + as bool, + errorMessage: freezed == errorMessage + ? _value.errorMessage + : errorMessage // ignore: cast_nullable_to_non_nullable + as String?, + planData: null == planData + ? _value.planData + : planData // ignore: cast_nullable_to_non_nullable + as CreatePlanModel, + durationIndex: freezed == durationIndex + ? _value.durationIndex + : durationIndex // ignore: cast_nullable_to_non_nullable + as int?, + selectedOptionIndex: freezed == selectedOptionIndex + ? _value.selectedOptionIndex + : selectedOptionIndex // ignore: cast_nullable_to_non_nullable + as int?, + ) as $Val); + } + + /// Create a copy of CreatePlanState + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $CreatePlanModelCopyWith<$Res> get planData { + return $CreatePlanModelCopyWith<$Res>(_value.planData, (value) { + return _then(_value.copyWith(planData: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$CreatePlanStateImplCopyWith<$Res> + implements $CreatePlanStateCopyWith<$Res> { + factory _$$CreatePlanStateImplCopyWith(_$CreatePlanStateImpl value, + $Res Function(_$CreatePlanStateImpl) then) = + __$$CreatePlanStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {bool isLoading, + String? errorMessage, + CreatePlanModel planData, + int? durationIndex, + int? selectedOptionIndex}); + + @override + $CreatePlanModelCopyWith<$Res> get planData; +} + +/// @nodoc +class __$$CreatePlanStateImplCopyWithImpl<$Res> + extends _$CreatePlanStateCopyWithImpl<$Res, _$CreatePlanStateImpl> + implements _$$CreatePlanStateImplCopyWith<$Res> { + __$$CreatePlanStateImplCopyWithImpl( + _$CreatePlanStateImpl _value, $Res Function(_$CreatePlanStateImpl) _then) + : super(_value, _then); + + /// Create a copy of CreatePlanState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? isLoading = null, + Object? errorMessage = freezed, + Object? planData = null, + Object? durationIndex = freezed, + Object? selectedOptionIndex = freezed, + }) { + return _then(_$CreatePlanStateImpl( + isLoading: null == isLoading + ? _value.isLoading + : isLoading // ignore: cast_nullable_to_non_nullable + as bool, + errorMessage: freezed == errorMessage + ? _value.errorMessage + : errorMessage // ignore: cast_nullable_to_non_nullable + as String?, + planData: null == planData + ? _value.planData + : planData // ignore: cast_nullable_to_non_nullable + as CreatePlanModel, + durationIndex: freezed == durationIndex + ? _value.durationIndex + : durationIndex // ignore: cast_nullable_to_non_nullable + as int?, + selectedOptionIndex: freezed == selectedOptionIndex + ? _value.selectedOptionIndex + : selectedOptionIndex // ignore: cast_nullable_to_non_nullable + as int?, + )); + } +} + +/// @nodoc + +class _$CreatePlanStateImpl implements _CreatePlanState { + const _$CreatePlanStateImpl( + {required this.isLoading, + required this.errorMessage, + required this.planData, + this.durationIndex, + this.selectedOptionIndex}); + + @override + final bool isLoading; + @override + final String? errorMessage; + @override + final CreatePlanModel planData; + @override + final int? durationIndex; + @override + final int? selectedOptionIndex; + + @override + String toString() { + return 'CreatePlanState(isLoading: $isLoading, errorMessage: $errorMessage, planData: $planData, durationIndex: $durationIndex, selectedOptionIndex: $selectedOptionIndex)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$CreatePlanStateImpl && + (identical(other.isLoading, isLoading) || + other.isLoading == isLoading) && + (identical(other.errorMessage, errorMessage) || + other.errorMessage == errorMessage) && + (identical(other.planData, planData) || + other.planData == planData) && + (identical(other.durationIndex, durationIndex) || + other.durationIndex == durationIndex) && + (identical(other.selectedOptionIndex, selectedOptionIndex) || + other.selectedOptionIndex == selectedOptionIndex)); + } + + @override + int get hashCode => Object.hash(runtimeType, isLoading, errorMessage, + planData, durationIndex, selectedOptionIndex); + + /// Create a copy of CreatePlanState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$CreatePlanStateImplCopyWith<_$CreatePlanStateImpl> get copyWith => + __$$CreatePlanStateImplCopyWithImpl<_$CreatePlanStateImpl>( + this, _$identity); +} + +abstract class _CreatePlanState implements CreatePlanState { + const factory _CreatePlanState( + {required final bool isLoading, + required final String? errorMessage, + required final CreatePlanModel planData, + final int? durationIndex, + final int? selectedOptionIndex}) = _$CreatePlanStateImpl; + + @override + bool get isLoading; + @override + String? get errorMessage; + @override + CreatePlanModel get planData; + @override + int? get durationIndex; + @override + int? get selectedOptionIndex; + + /// Create a copy of CreatePlanState + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$CreatePlanStateImplCopyWith<_$CreatePlanStateImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/features/create_plan/presentation/widgets/option_widget.dart b/lib/features/create_plan/presentation/widgets/option_widget.dart new file mode 100644 index 00000000..8224e871 --- /dev/null +++ b/lib/features/create_plan/presentation/widgets/option_widget.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; + +class OptionWidget extends StatelessWidget { + const OptionWidget({super.key}); + + @override + Widget build(BuildContext context) { + return Placeholder(); + } +} \ No newline at end of file diff --git a/lib/features/create_plan/presentation/widgets/options_count_button.dart b/lib/features/create_plan/presentation/widgets/options_count_button.dart new file mode 100644 index 00000000..35140514 --- /dev/null +++ b/lib/features/create_plan/presentation/widgets/options_count_button.dart @@ -0,0 +1,82 @@ +import 'package:eatvio_app/core/theme/custom_text_style.dart'; +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_cubit.dart'; +import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_state.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class OptionsCountButton extends StatelessWidget { + const OptionsCountButton({super.key}); + + @override + Widget build(BuildContext context) { + final s = S.of(context); + return BlocBuilder( + builder: (context, state) { + final selectedOption = state.selectedOptionIndex; + return Container( + margin: EdgeInsets.symmetric(horizontal: 16), + decoration: BoxDecoration( + color: appTheme.whiteA700, + borderRadius: BorderRadius.circular(100), + ), + padding: const EdgeInsets.all(4), + child: Row( + children: [ + Expanded( + child: _AnimatedOptionButton( + isSelected: selectedOption == null, + text: s.one_option, + onTap: () { + context.read().setSelectedOptionIndex(null); + }, + ), + ), + Expanded( + child: _AnimatedOptionButton( + isSelected: selectedOption != null, + text: s.three_options, + onTap: () { + context.read().setSelectedOptionIndex(0); + }, + ), + ), + ], + ), + ); + }, + ); + } +} + +class _AnimatedOptionButton extends StatelessWidget { + const _AnimatedOptionButton({required this.text, required this.isSelected, required this.onTap}); + final String text; + final bool isSelected; + final void Function() onTap; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + height: 40, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(100), + color: isSelected ? appTheme.blueA400 : appTheme.whiteA700, + ), + child: Center( + child: AnimatedDefaultTextStyle( + duration: const Duration(milliseconds: 200), + style: TextStyles.textMSemibold.copyWith( + color: isSelected ? appTheme.whiteA700 : appTheme.black, + ), + child: Text(text), + ), + ), + ), + ); + } +} diff --git a/lib/features/create_plan/presentation/widgets/plan_duration_drop_down_widget.dart b/lib/features/create_plan/presentation/widgets/plan_duration_drop_down_widget.dart new file mode 100644 index 00000000..132c2ffd --- /dev/null +++ b/lib/features/create_plan/presentation/widgets/plan_duration_drop_down_widget.dart @@ -0,0 +1,65 @@ +import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/theme/custom_text_style.dart'; +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/core/utils/localization_utils.dart'; +import 'package:eatvio_app/core/widgets/custom_drop_down_button.dart'; +import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_cubit.dart'; +import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_state.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class PlanDurationDropDownWidget extends StatefulWidget { + const PlanDurationDropDownWidget({super.key}); + + @override + State createState() => _PlanDurationDropDownWidgetState(); +} + +class _PlanDurationDropDownWidgetState extends State { + final List durationDropDownTitles = []; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + final s = S.of(context); + if (durationDropDownTitles.isEmpty) { + durationValues.forEach((element) { + durationDropDownTitles.add('$element ${getDayForm(element, s)}'); + }); + } + } + + @override + Widget build(BuildContext context) { + final s = S.of(context); + return BlocBuilder( + builder: (context, state) { + return CustomDropDownButton( + hint: Text( + s.choose_plan_duration, + style: TextStyles.textMMedium.copyWith(color: appTheme.black), + ), + onChanged: (value) { + if (value != null) { + context.read().setDuration(durationDropDownTitles.indexOf(value)); + } + }, + items: durationDropDownTitles + .mapIndexed( + (index, value) => DropdownMenuItem( + child: Text( + durationDropDownTitles[index], + style: TextStyles.textMMedium.copyWith(color: appTheme.black), + ), + value: durationDropDownTitles[index], + ), + ) + .toList(), + value: state.durationIndex == null ? null : durationDropDownTitles[state.durationIndex ?? 0], + ); + }, + ); + } +} diff --git a/lib/features/create_plan/presentation/widgets/plan_image_picker_widget.dart b/lib/features/create_plan/presentation/widgets/plan_image_picker_widget.dart new file mode 100644 index 00000000..37b9e9a6 --- /dev/null +++ b/lib/features/create_plan/presentation/widgets/plan_image_picker_widget.dart @@ -0,0 +1,145 @@ +import 'dart:io'; + +import 'package:dotted_border/dotted_border.dart'; +import 'package:eatvio_app/core/theme/custom_text_style.dart'; +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_cubit.dart'; +import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_state.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:image_picker/image_picker.dart'; + +class PlanImagePickerWidget extends StatefulWidget { + const PlanImagePickerWidget({super.key}); + + @override + State createState() => _PlanImagePickerWidgetState(); +} + +class _PlanImagePickerWidgetState extends State { + void _showImagePickerOptions(S s, CreatePlanCubit cubit) { + showCupertinoModalPopup( + context: context, + builder: (context) => CupertinoActionSheet( + actions: [ + CupertinoActionSheetAction( + onPressed: () async { + final picker = ImagePicker(); + final image = await picker.pickImage(source: ImageSource.camera); + cubit.setImage(image); + Navigator.of(context).pop(); + }, + child: Row( + children: [ + Icon(Icons.photo_camera), + SizedBox(width: 8), + Text(s.photo), + ], + ), + ), + CupertinoActionSheetAction( + onPressed: () async { + final picker = ImagePicker(); + final image = await picker.pickImage(source: ImageSource.gallery); + cubit.setImage(image); + Navigator.of(context).pop(); + }, + child: Row( + children: [ + Icon(Icons.photo), + SizedBox(width: 8), + Text(s.gallery), + ], + ), + ), + ], + cancelButton: CupertinoActionSheetAction( + onPressed: () => Navigator.of(context).pop(), + isDestructiveAction: true, + child: const Text('Отмена'), + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + final s = S.of(context); + return BlocBuilder( + builder: (context, state) { + return GestureDetector( + onTap: () { + _showImagePickerOptions(s, context.read()); + }, + child: Container( + height: 240, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + image: state.planData.image != null ? DecorationImage(fit: BoxFit.cover, image: FileImage(File(state.planData.image?.path ?? ''))) : null, + color: appTheme.gray50, + ), + width: MediaQuery.sizeOf(context).width, + padding: state.planData.image != null ? null : EdgeInsets.all(8), + child: state.planData.image != null + ? Padding( + padding: const EdgeInsets.only(top: 6, right: 6), + child: GestureDetector( + onTap: () { + context.read().setImage(null); + }, + child: Align( + alignment: Alignment.topRight, + child: Container( + width: 34, + height: 34, + decoration: BoxDecoration( + color: appTheme.dividerColor, + shape: BoxShape.circle, + ), + child: Icon( + Icons.close_rounded, + color: Colors.grey, + size: 16, + ), + ), + ), + ), + ) + : Stack( + children: [ + DottedBorder( + stackFit: StackFit.expand, + dashPattern: [14, 14], + color: appTheme.dottedBorder, + borderType: BorderType.RRect, + radius: Radius.circular(12), + child: SizedBox.expand(), + ), + Center( + child: Column( + children: [ + SizedBox( + height: 48, + ), + SvgPicture.asset('assets/icons/eatvio_logo.svg'), + SizedBox( + height: 24, + ), + Text( + s.add_cover, + style: TextStyles.textMMedium.copyWith(color: appTheme.gray), + ), + ], + ), + ), + ], + ), + ), + ); + }, + ); + } +} diff --git a/lib/features/create_plan/presentation/widgets/plan_type_choice_widget.dart b/lib/features/create_plan/presentation/widgets/plan_type_choice_widget.dart new file mode 100644 index 00000000..44811953 --- /dev/null +++ b/lib/features/create_plan/presentation/widgets/plan_type_choice_widget.dart @@ -0,0 +1,101 @@ +import 'package:eatvio_app/core/theme/custom_text_style.dart'; +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_cubit.dart'; +import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_state.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class PlanTypeChoiceWidget extends StatefulWidget { + PlanTypeChoiceWidget({super.key}); + + @override + State createState() => _PlanTypeChoiceWidgetState(); +} + +class _PlanTypeChoiceWidgetState extends State { + final List<(String, String)> choiceContent = []; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + final s = S.of(context); + if (choiceContent.isEmpty) { + choiceContent.add( + ( + 'assets/icons/slim.png', + s.slim, + ), + ); + choiceContent.add( + ( + 'assets/icons/fitness.png', + s.fitness, + ), + ); + choiceContent.add( + ( + 'assets/icons/yoga-pose.png', + s.wellness, + ), + ); + choiceContent.add( + ( + 'assets/icons/diet.png', + s.eating_behavior, + ), + ); + } + } + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return Row( + children: [ + ...List.generate(4, (index) { + return Expanded( + child: GestureDetector( + onTap: () { + final planTypes = [...state.planData.type]; + planTypes[index] = !planTypes[index]; + context.read().setPlanType(planTypes); + }, + child: Container( + margin: EdgeInsets.only(right: index == 3 ? 0 : 4), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5), + color: state.planData.type[index] ? appTheme.cardBackground : appTheme.whiteA700, + border: Border.all( + width: 1, + color: state.planData.type[index] ? appTheme.blueA400 : appTheme.lightGray00, + ), + ), + height: 104, + child: Column( + children: [ + SizedBox(height: 16), + Image.asset( + choiceContent[index].$1, + height: 30, + ), + SizedBox(height: 8), + Text( + choiceContent[index].$2, + overflow: TextOverflow.clip, + textAlign: TextAlign.center, + style: TextStyles.textFS10FW500.copyWith(color: appTheme.black), + ), + ], + ), + ), + ), + ); + }), + ], + ); + }, + ); + } +} diff --git a/lib/features/create_plan/presentation/widgets/select_exact_option_widget.dart b/lib/features/create_plan/presentation/widgets/select_exact_option_widget.dart new file mode 100644 index 00000000..12cf43ff --- /dev/null +++ b/lib/features/create_plan/presentation/widgets/select_exact_option_widget.dart @@ -0,0 +1,42 @@ +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:flutter/material.dart'; + +class SelectExactOptionWidget extends StatelessWidget { + const SelectExactOptionWidget({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + width: 140, + height: 48, + decoration: BoxDecoration( + color: appTheme.black, + borderRadius: BorderRadius.circular(100), + ), + child: Row( + children: [ + _OptionButton(isSelected: true, text: '1'), + _OptionButton(isSelected: true, text: '2'), + _OptionButton(isSelected: true, text: '3'), + ], + ), + ); + } +} + +class _OptionButton extends StatelessWidget { + const _OptionButton({required this.isSelected, required this.text}); + final bool isSelected; + final String text; + @override + Widget build(BuildContext context) { + return Container( + + decoration: BoxDecoration( + color: appTheme.whiteA700, + shape: BoxShape.circle, + ), + child: Text(text), + ); + } +} diff --git a/lib/features/create_plan/repositories/create_plan_repository.dart b/lib/features/create_plan/repositories/create_plan_repository.dart new file mode 100644 index 00000000..447f1d3a --- /dev/null +++ b/lib/features/create_plan/repositories/create_plan_repository.dart @@ -0,0 +1,27 @@ +import 'package:eatvio_app/features/create_plan/services/create_plan_service.dart'; + +class CreatePlanRepository { + final CreatePlanService createPlanService; + + CreatePlanRepository({required this.createPlanService}); + + Future createPlan({ + required Map model, + }) async { + await createPlanService..createPlan(model: model); + } + + Future createPacketOption({ + required String slug, + required Map model, + }) async { + await createPlanService.createPacketOption(slug: slug, model: model); + } + + Future createPacketOptions({ + required String slug, + required Map model, + }) async { + await createPlanService.createPacketOptions(slug: slug, model: model); + } +} diff --git a/lib/features/create_plan/services/create_plan_service.dart b/lib/features/create_plan/services/create_plan_service.dart new file mode 100644 index 00000000..f5d1891e --- /dev/null +++ b/lib/features/create_plan/services/create_plan_service.dart @@ -0,0 +1,27 @@ +import 'package:dio/dio.dart'; +import 'package:retrofit/retrofit.dart'; + +part 'create_plan_service.g.dart'; + +@RestApi() +abstract class CreatePlanService { + factory CreatePlanService(Dio dio, {String baseUrl}) = _CreatePlanService; + + @POST('/api/plans/create') + @MultiPart() + Future createPlan({ + @Body() required Map model, + }); + + @POST('/api/plans/create/{slug}/packet_option') + Future createPacketOption({ + @Path() required String slug, + @Body() required Map model, + }); + + @POST('/api/plans/create/{slug}/packet_options') + Future createPacketOptions({ + @Path() required String slug, + @Body() required Map model, + }); +} diff --git a/lib/features/create_plan/services/create_plan_service.g.dart b/lib/features/create_plan/services/create_plan_service.g.dart new file mode 100644 index 00000000..af44f549 --- /dev/null +++ b/lib/features/create_plan/services/create_plan_service.g.dart @@ -0,0 +1,138 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'create_plan_service.dart'; + +// ************************************************************************** +// RetrofitGenerator +// ************************************************************************** + +// ignore_for_file: unnecessary_brace_in_string_interps,no_leading_underscores_for_local_identifiers,unused_element,unnecessary_string_interpolations + +class _CreatePlanService implements CreatePlanService { + _CreatePlanService( + this._dio, { + this.baseUrl, + this.errorLogger, + }); + + final Dio _dio; + + String? baseUrl; + + final ParseErrorLogger? errorLogger; + + @override + Future createPlan({required Map model}) async { + final _extra = {}; + final queryParameters = {}; + final _headers = {}; + final _data = {}; + _data.addAll(model); + final _options = _setStreamType(Options( + method: 'POST', + headers: _headers, + extra: _extra, + contentType: 'multipart/form-data', + ) + .compose( + _dio.options, + '/api/plans/create', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + ))); + await _dio.fetch(_options); + } + + @override + Future createPacketOption({ + required String slug, + required Map model, + }) async { + final _extra = {}; + final queryParameters = {}; + final _headers = {}; + final _data = {}; + _data.addAll(model); + final _options = _setStreamType(Options( + method: 'POST', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/api/plans/create/${slug}/packet_option', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + ))); + await _dio.fetch(_options); + } + + @override + Future createPacketOptions({ + required String slug, + required Map model, + }) async { + final _extra = {}; + final queryParameters = {}; + final _headers = {}; + final _data = {}; + _data.addAll(model); + final _options = _setStreamType(Options( + method: 'POST', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/api/plans/create/${slug}/packet_options', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + ))); + await _dio.fetch(_options); + } + + RequestOptions _setStreamType(RequestOptions requestOptions) { + if (T != dynamic && + !(requestOptions.responseType == ResponseType.bytes || + requestOptions.responseType == ResponseType.stream)) { + if (T == String) { + requestOptions.responseType = ResponseType.plain; + } else { + requestOptions.responseType = ResponseType.json; + } + } + return requestOptions; + } + + String _combineBaseUrls( + String dioBaseUrl, + String? baseUrl, + ) { + if (baseUrl == null || baseUrl.trim().isEmpty) { + return dioBaseUrl; + } + + final url = Uri.parse(baseUrl); + + if (url.isAbsolute) { + return url.toString(); + } + + return Uri.parse(dioBaseUrl).resolveUri(url).toString(); + } +} diff --git a/lib/features/diary/presentation/bloc/diary_cubit.dart b/lib/features/diary/presentation/bloc/diary_cubit.dart index 54128921..9e667a03 100644 --- a/lib/features/diary/presentation/bloc/diary_cubit.dart +++ b/lib/features/diary/presentation/bloc/diary_cubit.dart @@ -155,7 +155,7 @@ class DiaryCubit extends Cubit { await fetchDiary(); } - sendProductToModeration({ + Future sendProductToModeration({ required String name, required double energy, required double proteins, diff --git a/lib/features/diary/presentation/widgets/add_product_modal.dart b/lib/features/diary/presentation/widgets/add_product_modal.dart index 1ff48abd..6dfbc53c 100644 --- a/lib/features/diary/presentation/widgets/add_product_modal.dart +++ b/lib/features/diary/presentation/widgets/add_product_modal.dart @@ -41,6 +41,14 @@ class _AddProductModalState extends State { id: widget.portion!.id, name: widget.portion!.productName, grInPortion: widget.portion!.portion == 'gr' ? 0 : 1, + energyOn100Gr: 0, + proteinsOn100Gr: 0, + fatsOn100Gr: 0, + carbohydratesOn100Gr: 0, + energyOnPortion: 0, + proteinsOnPortion: 0, + fatsOnPortion: 0, + carbohydratesOnPortion: 0, ); } } diff --git a/lib/features/diary/presentation/widgets/glossary_moderation_request_modal_widget.dart b/lib/features/diary/presentation/widgets/glossary_moderation_request_modal_widget.dart index 8e68c21b..e069b3d9 100644 --- a/lib/features/diary/presentation/widgets/glossary_moderation_request_modal_widget.dart +++ b/lib/features/diary/presentation/widgets/glossary_moderation_request_modal_widget.dart @@ -1,6 +1,7 @@ import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/widgets/custom_text_form_field.dart'; +import 'package:eatvio_app/features/glossary/models/glossary_model.dart'; import 'package:flutter/material.dart'; import 'package:lottie/lottie.dart'; @@ -10,7 +11,8 @@ class GlossaryModerationRequestModalWidget extends StatefulWidget { const GlossaryModerationRequestModalWidget({ super.key, - required this.productName, required this.diaryCubit, + required this.productName, + required this.diaryCubit, }); @override @@ -26,7 +28,7 @@ class _GlossaryModerationRequestModalWidgetState extends State sendProductToModeration({ + Future sendProductToModeration({ required String name, required double energy, required double proteins, diff --git a/lib/features/diary/services/diary_service.dart b/lib/features/diary/services/diary_service.dart index e484ee08..bb14e8cf 100644 --- a/lib/features/diary/services/diary_service.dart +++ b/lib/features/diary/services/diary_service.dart @@ -3,6 +3,7 @@ import 'package:dio/dio.dart'; import 'package:eatvio_app/features/diary/models/diary_day_model.dart'; import 'package:eatvio_app/features/diary/models/finish_diary_day_model.dart'; import 'package:eatvio_app/features/diary/models/nutrition_model.dart'; +import 'package:eatvio_app/features/glossary/models/glossary_model.dart'; import 'package:retrofit/retrofit.dart'; part 'diary_service.g.dart'; @@ -87,7 +88,7 @@ abstract class DiaryService { }); @POST('/api/diary/product_nutritional_values/sendToModeration') - Future sendProductToModeration({ + Future sendProductToModeration({ @Body() required Map data, }); } diff --git a/lib/features/diary/services/diary_service.g.dart b/lib/features/diary/services/diary_service.g.dart index 19253e5c..13951947 100644 --- a/lib/features/diary/services/diary_service.g.dart +++ b/lib/features/diary/services/diary_service.g.dart @@ -387,14 +387,14 @@ class _DiaryService implements DiaryService { } @override - Future sendProductToModeration( + Future sendProductToModeration( {required Map data}) async { final _extra = {}; final queryParameters = {}; final _headers = {}; final _data = {}; _data.addAll(data); - final _options = _setStreamType(Options( + final _options = _setStreamType(Options( method: 'POST', headers: _headers, extra: _extra, @@ -410,7 +410,15 @@ class _DiaryService implements DiaryService { _dio.options.baseUrl, baseUrl, ))); - await _dio.fetch(_options); + final _result = await _dio.fetch>(_options); + late GlossaryModerationModel _value; + try { + _value = GlossaryModerationModel.fromJson(_result.data!); + } on Object catch (e, s) { + errorLogger?.logError(e, s, _options); + rethrow; + } + return _value; } RequestOptions _setStreamType(RequestOptions requestOptions) { diff --git a/lib/features/diets/presentation/bloc/about_diet_cubit.dart b/lib/features/diets/presentation/bloc/about_diet_cubit.dart index 470da0de..8beaf5cc 100644 --- a/lib/features/diets/presentation/bloc/about_diet_cubit.dart +++ b/lib/features/diets/presentation/bloc/about_diet_cubit.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:developer'; import 'package:easy_debounce/easy_debounce.dart'; +import 'package:eatvio_app/core/utils/toasts.dart'; import 'package:eatvio_app/features/diets/repositories/diet_repository.dart'; import 'package:eatvio_app/features/diets/presentation/bloc/about_diet_state.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -59,13 +60,7 @@ class AboutDietCubit extends Cubit { Duration(seconds: 1), () async { try { - final dietLikeStateData = await dietsRepository.likeDiet(id); - final actualDiet = currentDiet.copyWith( - wasLikedByMe: dietLikeStateData.isLiked, - totalLikes: dietLikeStateData.totalLikes, - ); - emit(AboutDietState.loaded(actualDiet)); - dietsRepository.emitLikesStateToFeed(DietLikesState(wasLikedByMe: actualDiet.wasLikedByMe, totalLikes: actualDiet.totalLikes, index: dietIndexInFeed)); + await dietsRepository.likeDiet(id); } catch (e) { log(e.toString()); dietsRepository @@ -92,11 +87,10 @@ class AboutDietCubit extends Cubit { Duration(seconds: 1), () async { try { - final diet = await dietsRepository.addToFavorite(id); - emit(AboutDietState.loaded(diet)); - dietsRepository.emitFavoriteStateToFeed(DietFavoriteState(isFavorite: diet.isFavorite, index: dietIndexInFeed)); + await dietsRepository.addToFavorite(id); } catch (e) { log(e.toString()); + showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время'); dietsRepository.emitFavoriteStateToFeed(DietFavoriteState(isFavorite: currentDiet.isFavorite, index: dietIndexInFeed)); emit(AboutDietState.loaded(currentDiet)); } @@ -120,11 +114,10 @@ class AboutDietCubit extends Cubit { Duration(seconds: 1), () async { try { - final diet = await dietsRepository.removeFromFavorite(id); - emit(AboutDietState.loaded(diet)); - dietsRepository.emitFavoriteStateToFeed(DietFavoriteState(isFavorite: diet.isFavorite, index: dietIndexInFeed)); + await dietsRepository.removeFromFavorite(id); } catch (e) { log(e.toString()); + showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время'); dietsRepository.emitFavoriteStateToFeed(DietFavoriteState(isFavorite: currentDiet.isFavorite, index: dietIndexInFeed)); emit(AboutDietState.loaded(currentDiet)); } diff --git a/lib/features/diets/presentation/pages/about_diet_screen.dart b/lib/features/diets/presentation/pages/about_diet_screen.dart index 0b645aea..26c723c9 100644 --- a/lib/features/diets/presentation/pages/about_diet_screen.dart +++ b/lib/features/diets/presentation/pages/about_diet_screen.dart @@ -11,10 +11,10 @@ import 'package:eatvio_app/core/widgets/loader.dart'; import 'package:eatvio_app/features/diets/presentation/bloc/about_diet_cubit.dart'; import 'package:eatvio_app/features/diets/presentation/bloc/about_diet_state.dart'; import 'package:eatvio_app/features/diets/presentation/widgets/author_card.dart'; -import 'package:eatvio_app/features/diets/presentation/widgets/cpfc_container.dart'; +import 'package:eatvio_app/core/widgets/cpfc_container.dart'; import 'package:eatvio_app/features/diets/presentation/widgets/expandable_text.dart'; import 'package:eatvio_app/features/diets/presentation/widgets/info_bouble.dart'; -import 'package:eatvio_app/features/diets/presentation/widgets/main_background_bouble.dart'; +import 'package:eatvio_app/core/widgets/main_background_bouble.dart'; import 'package:eatvio_app/features/diets/presentation/widgets/more_options.dart'; import 'package:eatvio_app/features/diets/presentation/widgets/start_diet_bottom_sheet.dart'; import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; @@ -132,7 +132,12 @@ class _AboutDietScreenState extends State { ), backgroundColor: appTheme.greenA700, onPressed: () { - showModalBottomSheet(context: context, builder: (context) => StartDietBottomSheet()); + showModalBottomSheet( + context: context, + builder: (context) => StartDietBottomSheet( + aboutDietCubit: context.read(), + ), + ); }, label: Text( s.startDiet, @@ -320,6 +325,9 @@ class _AboutDietScreenState extends State { children: [ CachedNetworkImage( imageUrl: baseUrl + '/storage/' + widget.diet.image, + height: 300, + width: MediaQuery.sizeOf(context).width, + fit: BoxFit.fill, ), Positioned( bottom: 16, @@ -440,8 +448,7 @@ class _AboutDietScreenState extends State { ), ), SizedBox(height: 16), - if (state is CommentsStateLoading) - const Loader(), + if (state is CommentsStateLoading) const Loader(), if (state is CommentsStateLoaded) CommentsWidget( id: widget.diet.id.toString(), @@ -545,8 +552,8 @@ class _AboutDietScreenState extends State { ), CpfcContainer( calories: widget.diet.dailyNutrition[dayNumber].energy, - protein: widget.diet.dailyNutrition[dayNumber].proteins, - fat: widget.diet.dailyNutrition[dayNumber].fats, + proteins: widget.diet.dailyNutrition[dayNumber].proteins, + fats: widget.diet.dailyNutrition[dayNumber].fats, carbohydrates: widget.diet.dailyNutrition[dayNumber].carbohydrates, ), ], diff --git a/lib/features/diets/presentation/widgets/calendar_widget.dart b/lib/features/diets/presentation/widgets/calendar_widget.dart index c3ff1ee1..bd3fdca2 100644 --- a/lib/features/diets/presentation/widgets/calendar_widget.dart +++ b/lib/features/diets/presentation/widgets/calendar_widget.dart @@ -23,7 +23,7 @@ class _CalendarWidgetState extends State { Padding( padding: const EdgeInsets.only(top: 5), child: TableCalendar( - firstDay: DateTime.utc(2010, 10, 16), + firstDay: DateTime.now(), lastDay: DateTime.utc(2030, 3, 14), locale: 'ru_RU', startingDayOfWeek: StartingDayOfWeek.monday, diff --git a/lib/features/diets/presentation/widgets/more_options.dart b/lib/features/diets/presentation/widgets/more_options.dart index 862405f2..b087f9f0 100644 --- a/lib/features/diets/presentation/widgets/more_options.dart +++ b/lib/features/diets/presentation/widgets/more_options.dart @@ -1,7 +1,7 @@ import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/utils/toasts.dart'; import 'package:eatvio_app/core/widgets/custom_divider.dart'; -import 'package:eatvio_app/features/diets/presentation/widgets/main_background_bouble.dart'; +import 'package:eatvio_app/core/widgets/main_background_bouble.dart'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; @@ -12,8 +12,9 @@ import 'package:go_router/go_router.dart'; import 'package:share_plus/share_plus.dart'; class MoreOptions extends StatelessWidget { - const MoreOptions({super.key, required this.id}); + const MoreOptions({super.key, required this.id, this.disableActions}); final int id; + final bool? disableActions; @override Widget build(BuildContext context) { final s = S.of(context); @@ -25,7 +26,7 @@ class MoreOptions extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ GestureDetector( - onTap: () { + onTap:disableActions == true ? null : () { Clipboard.setData(ClipboardData(text: baseUrl + '/ru/diet/' + id.toString())); showInfoToast(s.link_was_copied); }, @@ -34,7 +35,6 @@ class MoreOptions extends StatelessWidget { Text( s.copy_link, style: TextStyles.textMMedium.copyWith( - color: appTheme.black, ), ), @@ -48,7 +48,7 @@ class MoreOptions extends StatelessWidget { child: CustomDivider(), ), GestureDetector( - onTap: () => Share.shareUri( + onTap: disableActions == true ? null : () => Share.shareUri( Uri.parse( baseUrl + '/ru/diet/' + id.toString(), ), @@ -58,7 +58,6 @@ class MoreOptions extends StatelessWidget { Text( s.share, style: TextStyles.textMMedium.copyWith( - color: appTheme.black, ), ), @@ -72,7 +71,7 @@ class MoreOptions extends StatelessWidget { child: CustomDivider(), ), GestureDetector( - onTap: () { + onTap: disableActions == true ? null : () { context.push('complainingScreen'); }, child: Row( diff --git a/lib/features/diets/presentation/widgets/start_diet_bottom_sheet.dart b/lib/features/diets/presentation/widgets/start_diet_bottom_sheet.dart index db2ecc95..ae9bc696 100644 --- a/lib/features/diets/presentation/widgets/start_diet_bottom_sheet.dart +++ b/lib/features/diets/presentation/widgets/start_diet_bottom_sheet.dart @@ -1,5 +1,6 @@ import 'package:eatvio_app/core/widgets/bottom_sheet_handler_bar.dart'; import 'package:eatvio_app/core/widgets/custom_divider.dart'; +import 'package:eatvio_app/features/diets/presentation/bloc/about_diet_cubit.dart'; import 'package:eatvio_app/features/diets/presentation/widgets/calendar_widget.dart'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:eatvio_app/core/theme/custom_text_style.dart'; @@ -8,8 +9,8 @@ import 'package:eatvio_app/core/widgets/custom_outlined_button.dart'; import 'package:flutter/material.dart'; class StartDietBottomSheet extends StatelessWidget { - const StartDietBottomSheet({super.key}); - + const StartDietBottomSheet({super.key, required this.aboutDietCubit}); + final AboutDietCubit aboutDietCubit; @override Widget build(BuildContext context) { final s = S.of(context); @@ -45,6 +46,7 @@ class StartDietBottomSheet extends StatelessWidget { Padding( padding: EdgeInsets.only(left: 16, right: 16, top: 16, bottom: 28), child: CustomOutlinedButton( + onPressed: () {}, height: 60, text: s.startDiet, buttonTextStyle: TextStyles.textLSemibold.copyWith( diff --git a/lib/features/diets/presentation/widgets/type_badges.dart b/lib/features/diets/presentation/widgets/type_badges.dart index b1d21fc4..2cf526c8 100644 --- a/lib/features/diets/presentation/widgets/type_badges.dart +++ b/lib/features/diets/presentation/widgets/type_badges.dart @@ -1,4 +1,4 @@ -import 'package:eatvio_app/features/diets/presentation/widgets/main_background_bouble.dart'; +import 'package:eatvio_app/core/widgets/main_background_bouble.dart'; import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:eatvio_app/core/theme/custom_text_style.dart'; @@ -23,15 +23,15 @@ class _TypeBadgesState extends State { Widget build(BuildContext context) { final s = S.of(context); final List<(String, Widget)> badgeSource = []; - if (widget.type.alko == true) { + if (widget.type.light == true) { badgeSource.add( ( 'assets/icons/cocktail.svg', _InfoOverlayEntry( onClose: closeOverlay, assetPath: 'assets/icons/cocktail.svg', - title: s.alko_type, - description: s.alko_type_desc, + title: s.light_type, + description: s.light_type_desc, ) ), ); @@ -49,15 +49,15 @@ class _TypeBadgesState extends State { ), ); } - if (widget.type.corb == true) { + if (widget.type.carb == true) { badgeSource.add( ( 'assets/icons/bread.svg', _InfoOverlayEntry( onClose: closeOverlay, assetPath: 'assets/icons/bread.svg', - title: s.corb_type, - description: s.corb_type_desc, + title: s.carb_type, + description: s.carb_type_desc, ) ), ); diff --git a/lib/features/feed/models/diets_pagination_model.dart b/lib/features/feed/models/diets_pagination_model.dart index 426306cd..7518573d 100644 --- a/lib/features/feed/models/diets_pagination_model.dart +++ b/lib/features/feed/models/diets_pagination_model.dart @@ -56,9 +56,11 @@ class Author with _$Author { class DailySchedule with _$DailySchedule { const factory DailySchedule({ @JsonKey(name: "name") required String name, - @JsonKey(name: "uniq") required String uniq, + @JsonKey(name: "uniq", includeToJson: false) required String uniq, @JsonKey(name: "portion") required Portion portion, @JsonKey(name: "quantity") required int quantity, + @JsonKey(includeToJson: false, includeFromJson: false) DailyNutrition? dailyNutrition, + @JsonKey(name: 'product_nutritional_value_id', includeFromJson: false, includeIfNull: false) String? productNutritionalValueId, }) = _DailySchedule; factory DailySchedule.fromJson(Map json) => _$DailyScheduleFromJson(json); @@ -78,9 +80,9 @@ final portionValues = EnumValues({"grams": Portion.GRAMS, "milliliters": Portion @freezed class DietType with _$DietType { const factory DietType({ - @JsonKey(name: "alko") bool? alko, + @JsonKey(name: "light") bool? light, @JsonKey(name: "veg") bool? veg, - @JsonKey(name: "corb") bool? corb, + @JsonKey(name: "carb") bool? carb, @JsonKey(name: "meat") bool? meat, }) = _DietType; diff --git a/lib/features/feed/models/diets_pagination_model.freezed.dart b/lib/features/feed/models/diets_pagination_model.freezed.dart index 34cd67d0..d30a57cc 100644 --- a/lib/features/feed/models/diets_pagination_model.freezed.dart +++ b/lib/features/feed/models/diets_pagination_model.freezed.dart @@ -1061,12 +1061,19 @@ DailySchedule _$DailyScheduleFromJson(Map json) { mixin _$DailySchedule { @JsonKey(name: "name") String get name => throw _privateConstructorUsedError; - @JsonKey(name: "uniq") + @JsonKey(name: "uniq", includeToJson: false) String get uniq => throw _privateConstructorUsedError; @JsonKey(name: "portion") Portion get portion => throw _privateConstructorUsedError; @JsonKey(name: "quantity") int get quantity => throw _privateConstructorUsedError; + @JsonKey(includeToJson: false, includeFromJson: false) + DailyNutrition? get dailyNutrition => throw _privateConstructorUsedError; + @JsonKey( + name: 'product_nutritional_value_id', + includeFromJson: false, + includeIfNull: false) + String? get productNutritionalValueId => throw _privateConstructorUsedError; /// Serializes this DailySchedule to a JSON map. Map toJson() => throw _privateConstructorUsedError; @@ -1086,9 +1093,18 @@ abstract class $DailyScheduleCopyWith<$Res> { @useResult $Res call( {@JsonKey(name: "name") String name, - @JsonKey(name: "uniq") String uniq, + @JsonKey(name: "uniq", includeToJson: false) String uniq, @JsonKey(name: "portion") Portion portion, - @JsonKey(name: "quantity") int quantity}); + @JsonKey(name: "quantity") int quantity, + @JsonKey(includeToJson: false, includeFromJson: false) + DailyNutrition? dailyNutrition, + @JsonKey( + name: 'product_nutritional_value_id', + includeFromJson: false, + includeIfNull: false) + String? productNutritionalValueId}); + + $DailyNutritionCopyWith<$Res>? get dailyNutrition; } /// @nodoc @@ -1110,6 +1126,8 @@ class _$DailyScheduleCopyWithImpl<$Res, $Val extends DailySchedule> Object? uniq = null, Object? portion = null, Object? quantity = null, + Object? dailyNutrition = freezed, + Object? productNutritionalValueId = freezed, }) { return _then(_value.copyWith( name: null == name @@ -1128,8 +1146,30 @@ class _$DailyScheduleCopyWithImpl<$Res, $Val extends DailySchedule> ? _value.quantity : quantity // ignore: cast_nullable_to_non_nullable as int, + dailyNutrition: freezed == dailyNutrition + ? _value.dailyNutrition + : dailyNutrition // ignore: cast_nullable_to_non_nullable + as DailyNutrition?, + productNutritionalValueId: freezed == productNutritionalValueId + ? _value.productNutritionalValueId + : productNutritionalValueId // ignore: cast_nullable_to_non_nullable + as String?, ) as $Val); } + + /// Create a copy of DailySchedule + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $DailyNutritionCopyWith<$Res>? get dailyNutrition { + if (_value.dailyNutrition == null) { + return null; + } + + return $DailyNutritionCopyWith<$Res>(_value.dailyNutrition!, (value) { + return _then(_value.copyWith(dailyNutrition: value) as $Val); + }); + } } /// @nodoc @@ -1142,9 +1182,19 @@ abstract class _$$DailyScheduleImplCopyWith<$Res> @useResult $Res call( {@JsonKey(name: "name") String name, - @JsonKey(name: "uniq") String uniq, + @JsonKey(name: "uniq", includeToJson: false) String uniq, @JsonKey(name: "portion") Portion portion, - @JsonKey(name: "quantity") int quantity}); + @JsonKey(name: "quantity") int quantity, + @JsonKey(includeToJson: false, includeFromJson: false) + DailyNutrition? dailyNutrition, + @JsonKey( + name: 'product_nutritional_value_id', + includeFromJson: false, + includeIfNull: false) + String? productNutritionalValueId}); + + @override + $DailyNutritionCopyWith<$Res>? get dailyNutrition; } /// @nodoc @@ -1164,6 +1214,8 @@ class __$$DailyScheduleImplCopyWithImpl<$Res> Object? uniq = null, Object? portion = null, Object? quantity = null, + Object? dailyNutrition = freezed, + Object? productNutritionalValueId = freezed, }) { return _then(_$DailyScheduleImpl( name: null == name @@ -1182,6 +1234,14 @@ class __$$DailyScheduleImplCopyWithImpl<$Res> ? _value.quantity : quantity // ignore: cast_nullable_to_non_nullable as int, + dailyNutrition: freezed == dailyNutrition + ? _value.dailyNutrition + : dailyNutrition // ignore: cast_nullable_to_non_nullable + as DailyNutrition?, + productNutritionalValueId: freezed == productNutritionalValueId + ? _value.productNutritionalValueId + : productNutritionalValueId // ignore: cast_nullable_to_non_nullable + as String?, )); } } @@ -1191,9 +1251,16 @@ class __$$DailyScheduleImplCopyWithImpl<$Res> class _$DailyScheduleImpl implements _DailySchedule { const _$DailyScheduleImpl( {@JsonKey(name: "name") required this.name, - @JsonKey(name: "uniq") required this.uniq, + @JsonKey(name: "uniq", includeToJson: false) required this.uniq, @JsonKey(name: "portion") required this.portion, - @JsonKey(name: "quantity") required this.quantity}); + @JsonKey(name: "quantity") required this.quantity, + @JsonKey(includeToJson: false, includeFromJson: false) + this.dailyNutrition, + @JsonKey( + name: 'product_nutritional_value_id', + includeFromJson: false, + includeIfNull: false) + this.productNutritionalValueId}); factory _$DailyScheduleImpl.fromJson(Map json) => _$$DailyScheduleImplFromJson(json); @@ -1202,7 +1269,7 @@ class _$DailyScheduleImpl implements _DailySchedule { @JsonKey(name: "name") final String name; @override - @JsonKey(name: "uniq") + @JsonKey(name: "uniq", includeToJson: false) final String uniq; @override @JsonKey(name: "portion") @@ -1210,10 +1277,19 @@ class _$DailyScheduleImpl implements _DailySchedule { @override @JsonKey(name: "quantity") final int quantity; + @override + @JsonKey(includeToJson: false, includeFromJson: false) + final DailyNutrition? dailyNutrition; + @override + @JsonKey( + name: 'product_nutritional_value_id', + includeFromJson: false, + includeIfNull: false) + final String? productNutritionalValueId; @override String toString() { - return 'DailySchedule(name: $name, uniq: $uniq, portion: $portion, quantity: $quantity)'; + return 'DailySchedule(name: $name, uniq: $uniq, portion: $portion, quantity: $quantity, dailyNutrition: $dailyNutrition, productNutritionalValueId: $productNutritionalValueId)'; } @override @@ -1225,12 +1301,18 @@ class _$DailyScheduleImpl implements _DailySchedule { (identical(other.uniq, uniq) || other.uniq == uniq) && (identical(other.portion, portion) || other.portion == portion) && (identical(other.quantity, quantity) || - other.quantity == quantity)); + other.quantity == quantity) && + (identical(other.dailyNutrition, dailyNutrition) || + other.dailyNutrition == dailyNutrition) && + (identical(other.productNutritionalValueId, + productNutritionalValueId) || + other.productNutritionalValueId == productNutritionalValueId)); } @JsonKey(includeFromJson: false, includeToJson: false) @override - int get hashCode => Object.hash(runtimeType, name, uniq, portion, quantity); + int get hashCode => Object.hash(runtimeType, name, uniq, portion, quantity, + dailyNutrition, productNutritionalValueId); /// Create a copy of DailySchedule /// with the given fields replaced by the non-null parameter values. @@ -1250,11 +1332,17 @@ class _$DailyScheduleImpl implements _DailySchedule { abstract class _DailySchedule implements DailySchedule { const factory _DailySchedule( - {@JsonKey(name: "name") required final String name, - @JsonKey(name: "uniq") required final String uniq, - @JsonKey(name: "portion") required final Portion portion, - @JsonKey(name: "quantity") required final int quantity}) = - _$DailyScheduleImpl; + {@JsonKey(name: "name") required final String name, + @JsonKey(name: "uniq", includeToJson: false) required final String uniq, + @JsonKey(name: "portion") required final Portion portion, + @JsonKey(name: "quantity") required final int quantity, + @JsonKey(includeToJson: false, includeFromJson: false) + final DailyNutrition? dailyNutrition, + @JsonKey( + name: 'product_nutritional_value_id', + includeFromJson: false, + includeIfNull: false) + final String? productNutritionalValueId}) = _$DailyScheduleImpl; factory _DailySchedule.fromJson(Map json) = _$DailyScheduleImpl.fromJson; @@ -1263,7 +1351,7 @@ abstract class _DailySchedule implements DailySchedule { @JsonKey(name: "name") String get name; @override - @JsonKey(name: "uniq") + @JsonKey(name: "uniq", includeToJson: false) String get uniq; @override @JsonKey(name: "portion") @@ -1271,6 +1359,15 @@ abstract class _DailySchedule implements DailySchedule { @override @JsonKey(name: "quantity") int get quantity; + @override + @JsonKey(includeToJson: false, includeFromJson: false) + DailyNutrition? get dailyNutrition; + @override + @JsonKey( + name: 'product_nutritional_value_id', + includeFromJson: false, + includeIfNull: false) + String? get productNutritionalValueId; /// Create a copy of DailySchedule /// with the given fields replaced by the non-null parameter values. @@ -1286,12 +1383,12 @@ DietType _$DietTypeFromJson(Map json) { /// @nodoc mixin _$DietType { - @JsonKey(name: "alko") - bool? get alko => throw _privateConstructorUsedError; + @JsonKey(name: "light") + bool? get light => throw _privateConstructorUsedError; @JsonKey(name: "veg") bool? get veg => throw _privateConstructorUsedError; - @JsonKey(name: "corb") - bool? get corb => throw _privateConstructorUsedError; + @JsonKey(name: "carb") + bool? get carb => throw _privateConstructorUsedError; @JsonKey(name: "meat") bool? get meat => throw _privateConstructorUsedError; @@ -1311,9 +1408,9 @@ abstract class $DietTypeCopyWith<$Res> { _$DietTypeCopyWithImpl<$Res, DietType>; @useResult $Res call( - {@JsonKey(name: "alko") bool? alko, + {@JsonKey(name: "light") bool? light, @JsonKey(name: "veg") bool? veg, - @JsonKey(name: "corb") bool? corb, + @JsonKey(name: "carb") bool? carb, @JsonKey(name: "meat") bool? meat}); } @@ -1332,23 +1429,23 @@ class _$DietTypeCopyWithImpl<$Res, $Val extends DietType> @pragma('vm:prefer-inline') @override $Res call({ - Object? alko = freezed, + Object? light = freezed, Object? veg = freezed, - Object? corb = freezed, + Object? carb = freezed, Object? meat = freezed, }) { return _then(_value.copyWith( - alko: freezed == alko - ? _value.alko - : alko // ignore: cast_nullable_to_non_nullable + light: freezed == light + ? _value.light + : light // ignore: cast_nullable_to_non_nullable as bool?, veg: freezed == veg ? _value.veg : veg // ignore: cast_nullable_to_non_nullable as bool?, - corb: freezed == corb - ? _value.corb - : corb // ignore: cast_nullable_to_non_nullable + carb: freezed == carb + ? _value.carb + : carb // ignore: cast_nullable_to_non_nullable as bool?, meat: freezed == meat ? _value.meat @@ -1367,9 +1464,9 @@ abstract class _$$DietTypeImplCopyWith<$Res> @override @useResult $Res call( - {@JsonKey(name: "alko") bool? alko, + {@JsonKey(name: "light") bool? light, @JsonKey(name: "veg") bool? veg, - @JsonKey(name: "corb") bool? corb, + @JsonKey(name: "carb") bool? carb, @JsonKey(name: "meat") bool? meat}); } @@ -1386,23 +1483,23 @@ class __$$DietTypeImplCopyWithImpl<$Res> @pragma('vm:prefer-inline') @override $Res call({ - Object? alko = freezed, + Object? light = freezed, Object? veg = freezed, - Object? corb = freezed, + Object? carb = freezed, Object? meat = freezed, }) { return _then(_$DietTypeImpl( - alko: freezed == alko - ? _value.alko - : alko // ignore: cast_nullable_to_non_nullable + light: freezed == light + ? _value.light + : light // ignore: cast_nullable_to_non_nullable as bool?, veg: freezed == veg ? _value.veg : veg // ignore: cast_nullable_to_non_nullable as bool?, - corb: freezed == corb - ? _value.corb - : corb // ignore: cast_nullable_to_non_nullable + carb: freezed == carb + ? _value.carb + : carb // ignore: cast_nullable_to_non_nullable as bool?, meat: freezed == meat ? _value.meat @@ -1416,30 +1513,30 @@ class __$$DietTypeImplCopyWithImpl<$Res> @JsonSerializable() class _$DietTypeImpl implements _DietType { const _$DietTypeImpl( - {@JsonKey(name: "alko") this.alko, + {@JsonKey(name: "light") this.light, @JsonKey(name: "veg") this.veg, - @JsonKey(name: "corb") this.corb, + @JsonKey(name: "carb") this.carb, @JsonKey(name: "meat") this.meat}); factory _$DietTypeImpl.fromJson(Map json) => _$$DietTypeImplFromJson(json); @override - @JsonKey(name: "alko") - final bool? alko; + @JsonKey(name: "light") + final bool? light; @override @JsonKey(name: "veg") final bool? veg; @override - @JsonKey(name: "corb") - final bool? corb; + @JsonKey(name: "carb") + final bool? carb; @override @JsonKey(name: "meat") final bool? meat; @override String toString() { - return 'DietType(alko: $alko, veg: $veg, corb: $corb, meat: $meat)'; + return 'DietType(light: $light, veg: $veg, carb: $carb, meat: $meat)'; } @override @@ -1447,15 +1544,15 @@ class _$DietTypeImpl implements _DietType { return identical(this, other) || (other.runtimeType == runtimeType && other is _$DietTypeImpl && - (identical(other.alko, alko) || other.alko == alko) && + (identical(other.light, light) || other.light == light) && (identical(other.veg, veg) || other.veg == veg) && - (identical(other.corb, corb) || other.corb == corb) && + (identical(other.carb, carb) || other.carb == carb) && (identical(other.meat, meat) || other.meat == meat)); } @JsonKey(includeFromJson: false, includeToJson: false) @override - int get hashCode => Object.hash(runtimeType, alko, veg, corb, meat); + int get hashCode => Object.hash(runtimeType, light, veg, carb, meat); /// Create a copy of DietType /// with the given fields replaced by the non-null parameter values. @@ -1475,23 +1572,23 @@ class _$DietTypeImpl implements _DietType { abstract class _DietType implements DietType { const factory _DietType( - {@JsonKey(name: "alko") final bool? alko, + {@JsonKey(name: "light") final bool? light, @JsonKey(name: "veg") final bool? veg, - @JsonKey(name: "corb") final bool? corb, + @JsonKey(name: "carb") final bool? carb, @JsonKey(name: "meat") final bool? meat}) = _$DietTypeImpl; factory _DietType.fromJson(Map json) = _$DietTypeImpl.fromJson; @override - @JsonKey(name: "alko") - bool? get alko; + @JsonKey(name: "light") + bool? get light; @override @JsonKey(name: "veg") bool? get veg; @override - @JsonKey(name: "corb") - bool? get corb; + @JsonKey(name: "carb") + bool? get carb; @override @JsonKey(name: "meat") bool? get meat; diff --git a/lib/features/feed/models/diets_pagination_model.g.dart b/lib/features/feed/models/diets_pagination_model.g.dart index c7e54383..776b52e5 100644 --- a/lib/features/feed/models/diets_pagination_model.g.dart +++ b/lib/features/feed/models/diets_pagination_model.g.dart @@ -97,7 +97,6 @@ _$DailyScheduleImpl _$$DailyScheduleImplFromJson(Map json) => Map _$$DailyScheduleImplToJson(_$DailyScheduleImpl instance) => { 'name': instance.name, - 'uniq': instance.uniq, 'portion': _$PortionEnumMap[instance.portion]!, 'quantity': instance.quantity, }; @@ -110,17 +109,17 @@ const _$PortionEnumMap = { _$DietTypeImpl _$$DietTypeImplFromJson(Map json) => _$DietTypeImpl( - alko: json['alko'] as bool?, + light: json['light'] as bool?, veg: json['veg'] as bool?, - corb: json['corb'] as bool?, + carb: json['carb'] as bool?, meat: json['meat'] as bool?, ); Map _$$DietTypeImplToJson(_$DietTypeImpl instance) => { - 'alko': instance.alko, + 'light': instance.light, 'veg': instance.veg, - 'corb': instance.corb, + 'carb': instance.carb, 'meat': instance.meat, }; diff --git a/lib/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart b/lib/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart index 27e97425..c299cdc0 100644 --- a/lib/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart +++ b/lib/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart @@ -2,6 +2,8 @@ import 'dart:async'; import 'dart:developer'; import 'package:easy_debounce/easy_debounce.dart'; +import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/utils/toasts.dart'; import 'package:eatvio_app/features/diets/repositories/diet_repository.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_state.dart'; import 'package:eatvio_app/features/feed/repositories/feed_repository.dart'; @@ -14,16 +16,23 @@ class DietsCubit extends Cubit { late int lastPage; bool isLoading = false; String? _query; - String? _types; - int? _duration; - String? _sorting; + static const List _sortingValues = [ + null, + 'chosen_diets_count:desc', + 'created_at:desc', + 'created_at', + 'name', + 'name:desc', + ]; + static const List _durationValues = [null, ...durationValues]; + static const List _types = [null, ...typeValues]; late final StreamSubscription likesStateSubscription; late final StreamSubscription favoriteStateSubscription; late final StreamSubscription commentsStateSubscription; DietsCubit({ required this.feedRepository, required this.dietsRepository, - }) : super(const DietsState.loading()) { + }) : super(DietsState.initial()) { likesStateSubscription = dietsRepository.dietLikeState.listen((likesState) { _changeDietLikeState(likesState); }); @@ -43,35 +52,43 @@ class DietsCubit extends Cubit { super.close(); } + void changeTypeFilter(int index) { + emit(state.copyWith(filterState: state.filterState.copyWith(typeIndex: index), errorMessage: null)); + loadDiets(query: _query); + } + + void changeSortingType(int index) { + emit(state.copyWith(filterState: state.filterState.copyWith(sortingIndex: index), errorMessage: null)); + loadDiets(query: _query); + } + + void changeDurationFilter(int index) { + emit(state.copyWith(filterState: state.filterState.copyWith(durationIndex: index), errorMessage: null)); + loadDiets(query: _query); + } + loadDiets({ String? query, - String? types, - int? duration, - String? sorting, }) async { try { _query = query; - _types = types; - _duration = duration; - _sorting = sorting; + final type = _types[state.filterState.typeIndex]; + final duration = _durationValues[state.filterState.durationIndex]; + final sorting = _sortingValues[state.filterState.sortingIndex]; final dietsPagination = await feedRepository.getDiets( query: query, - types: types, + types: type, duration: duration, sorting: sorting, ); lastPage = dietsPagination.meta.lastPage; + emit( - DietsState.loaded( - dietsList: dietsPagination.data, - isLoading: isLoading, - ), + state.copyWith(dietsList: dietsPagination.data, isLoading: isLoading, errorMessage: null), ); } catch (e) { emit( - DietsState.error( - errorMessage: e.toString(), - ), + state.copyWith(errorMessage: e.toString()), ); } } @@ -80,7 +97,7 @@ class DietsCubit extends Cubit { if (currentPage < lastPage && !isLoading) { isLoading = true; - emit((state as DietsStateLoaded).copyWith(isLoading: isLoading)); + emit((state.copyWith(isLoading: isLoading, errorMessage: null))); try { if (currentPage < lastPage) { @@ -88,31 +105,30 @@ class DietsCubit extends Cubit { final dietPagination = await feedRepository.getDiets( page: currentPage, query: _query, - types: _types, - duration: _duration, - sorting: _sorting, + types: _types[state.filterState.typeIndex], + duration: _durationValues[state.filterState.durationIndex], + sorting: _sortingValues[state.filterState.sortingIndex], ); isLoading = false; emit( - DietsState.loaded( + state.copyWith( dietsList: [ - ...(state as DietsStateLoaded).dietsList, + ...state.dietsList, ...dietPagination.data, ], isLoading: isLoading, + errorMessage: null, ), ); } } catch (e) { - emit(DietsState.error(errorMessage: e.toString())); + emit(state.copyWith(errorMessage: e.toString())); } } } Future likeDiet(int id, int index) async { - if (state is! DietsStateLoaded) return; - - final currentDiets = (state as DietsStateLoaded).dietsList; + final currentDiets = state.dietsList; final dietsForModification = [...currentDiets]; final currentDiet = currentDiets[index]; if (currentDiet.wasLikedByMe) { @@ -121,11 +137,12 @@ class DietsCubit extends Cubit { wasLikedByMe: false, ); emit( - DietsState.loaded( + state.copyWith( dietsList: [ ...dietsForModification, ], isLoading: isLoading, + errorMessage: null, ), ); } else { @@ -134,11 +151,12 @@ class DietsCubit extends Cubit { wasLikedByMe: true, ); emit( - DietsState.loaded( + state.copyWith( dietsList: [ ...dietsForModification, ], isLoading: isLoading, + errorMessage: null, ), ); } @@ -147,24 +165,11 @@ class DietsCubit extends Cubit { Duration(milliseconds: 200), () async { try { - final dietLikeStateData = await dietsRepository.likeDiet(id); - final actualDiet = currentDiet.copyWith( - wasLikedByMe: dietLikeStateData.isLiked, - totalLikes: dietLikeStateData.totalLikes, - ); - dietsForModification[index] = actualDiet; - emit( - DietsState.loaded( - dietsList: [ - ...dietsForModification, - ], - isLoading: isLoading, - ), - ); + await dietsRepository.likeDiet(id); } catch (e) { log(e.toString()); emit( - DietsState.loaded( + state.copyWith( dietsList: [ ...currentDiets, ], @@ -177,9 +182,7 @@ class DietsCubit extends Cubit { } void _changeDietLikeState(DietLikesState likesState) { - if (state is! DietsStateLoaded) return; - - final currentDiets = (state as DietsStateLoaded).dietsList; + final currentDiets = state.dietsList; final dietsForModification = [...currentDiets]; final currentDiet = currentDiets[likesState.index]; @@ -188,19 +191,18 @@ class DietsCubit extends Cubit { wasLikedByMe: likesState.wasLikedByMe, ); emit( - DietsState.loaded( + state.copyWith( dietsList: [ ...dietsForModification, ], isLoading: isLoading, + errorMessage: null, ), ); } void _changeDietFavoriteState(DietFavoriteState favoriteState) { - if (state is! DietsStateLoaded) return; - - final currentDiets = (state as DietsStateLoaded).dietsList; + final currentDiets = state.dietsList; final dietsForModification = [...currentDiets]; final currentDiet = currentDiets[favoriteState.index]; @@ -208,19 +210,18 @@ class DietsCubit extends Cubit { isFavorite: favoriteState.isFavorite, ); emit( - DietsState.loaded( + state.copyWith( dietsList: [ ...dietsForModification, ], isLoading: isLoading, + errorMessage: null, ), ); } void _changeDietCommentsState(DietCommentsState commentsState) { - if (state is! DietsStateLoaded) return; - - final currentDiets = (state as DietsStateLoaded).dietsList; + final currentDiets = state.dietsList; final dietsForModification = [...currentDiets]; final currentDiet = currentDiets[commentsState.index]; @@ -228,18 +229,18 @@ class DietsCubit extends Cubit { totalComments: commentsState.totalComments, ); emit( - DietsState.loaded( + state.copyWith( dietsList: [ ...dietsForModification, ], isLoading: isLoading, + errorMessage: null, ), ); } Future addDietToFavorite(int id, int index) async { - if (state is! DietsStateLoaded) return; - final currentDiets = (state as DietsStateLoaded).dietsList; + final currentDiets = state.dietsList; final dietsForModification = [...currentDiets]; final currentDiet = currentDiets[index]; @@ -247,11 +248,12 @@ class DietsCubit extends Cubit { isFavorite: true, ); emit( - DietsState.loaded( + state.copyWith( dietsList: [ ...dietsForModification, ], isLoading: isLoading, + errorMessage: null, ), ); EasyDebounce.debounce( @@ -259,24 +261,17 @@ class DietsCubit extends Cubit { Duration(milliseconds: 200), () async { try { - final diet = await dietsRepository.addToFavorite(id); - dietsForModification[index] = diet; - emit( - DietsState.loaded( - dietsList: [ - ...dietsForModification, - ], - isLoading: isLoading, - ), - ); + await dietsRepository.addToFavorite(id); } catch (e) { log(e.toString()); + showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время'); emit( - DietsState.loaded( + state.copyWith( dietsList: [ ...currentDiets, ], isLoading: isLoading, + errorMessage: null, ), ); } @@ -285,8 +280,7 @@ class DietsCubit extends Cubit { } Future removeDietFromFavorite(int id, int index) async { - if (state is! DietsStateLoaded) return; - final currentDiets = (state as DietsStateLoaded).dietsList; + final currentDiets = state.dietsList; final dietsForModification = [...currentDiets]; final currentDiet = currentDiets[index]; @@ -294,11 +288,12 @@ class DietsCubit extends Cubit { isFavorite: false, ); emit( - DietsState.loaded( + state.copyWith( dietsList: [ ...dietsForModification, ], isLoading: isLoading, + errorMessage: null, ), ); EasyDebounce.debounce( @@ -306,24 +301,17 @@ class DietsCubit extends Cubit { Duration(milliseconds: 200), () async { try { - final diet = await dietsRepository.removeFromFavorite(id); - dietsForModification[index] = diet; - emit( - DietsState.loaded( - dietsList: [ - ...dietsForModification, - ], - isLoading: isLoading, - ), - ); + await dietsRepository.removeFromFavorite(id); } catch (e) { log(e.toString()); + showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время'); emit( - DietsState.loaded( + state.copyWith( dietsList: [ ...currentDiets, ], isLoading: isLoading, + errorMessage: null, ), ); } diff --git a/lib/features/feed/presentation/bloc/diets_cubit/diets_state.dart b/lib/features/feed/presentation/bloc/diets_cubit/diets_state.dart index bc5bbba1..8f3888de 100644 --- a/lib/features/feed/presentation/bloc/diets_cubit/diets_state.dart +++ b/lib/features/feed/presentation/bloc/diets_cubit/diets_state.dart @@ -4,17 +4,31 @@ import 'package:freezed_annotation/freezed_annotation.dart'; part 'diets_state.freezed.dart'; @freezed -abstract class DietsState with _$DietsState { - const factory DietsState() = _DietsState; - - const factory DietsState.loading() = DietsStateLoading; - - const factory DietsState.loaded({ +class DietsState with _$DietsState { + const factory DietsState({ + required DietsFilterState filterState, required List dietsList, required bool isLoading, - }) = DietsStateLoaded; + required String? errorMessage, + }) = _DietsState; - const factory DietsState.error({ - required String errorMessage, - }) = DietsStateError; + factory DietsState.initial() => DietsState( + filterState: DietsFilterState( + durationIndex: 0, + sortingIndex: 0, + typeIndex: 0, + ), + dietsList: [], + isLoading: true, + errorMessage: null, + ); +} + +@freezed +class DietsFilterState with _$DietsFilterState { + const factory DietsFilterState({ + required int sortingIndex, + required int durationIndex, + required int typeIndex, + }) = _DietsFilterState; } diff --git a/lib/features/feed/presentation/bloc/diets_cubit/diets_state.freezed.dart b/lib/features/feed/presentation/bloc/diets_cubit/diets_state.freezed.dart index 8edde15b..aea14d0b 100644 --- a/lib/features/feed/presentation/bloc/diets_cubit/diets_state.freezed.dart +++ b/lib/features/feed/presentation/bloc/diets_cubit/diets_state.freezed.dart @@ -16,55 +16,15 @@ final _privateConstructorUsedError = UnsupportedError( /// @nodoc mixin _$DietsState { - @optionalTypeArgs - TResult when( - TResult Function() $default, { - required TResult Function() loading, - required TResult Function(List dietsList, bool isLoading) loaded, - required TResult Function(String errorMessage) error, - }) => - throw _privateConstructorUsedError; - @optionalTypeArgs - TResult? whenOrNull( - TResult? Function()? $default, { - TResult? Function()? loading, - TResult? Function(List dietsList, bool isLoading)? loaded, - TResult? Function(String errorMessage)? error, - }) => - throw _privateConstructorUsedError; - @optionalTypeArgs - TResult maybeWhen( - TResult Function()? $default, { - TResult Function()? loading, - TResult Function(List dietsList, bool isLoading)? loaded, - TResult Function(String errorMessage)? error, - required TResult orElse(), - }) => - throw _privateConstructorUsedError; - @optionalTypeArgs - TResult map( - TResult Function(_DietsState value) $default, { - required TResult Function(DietsStateLoading value) loading, - required TResult Function(DietsStateLoaded value) loaded, - required TResult Function(DietsStateError value) error, - }) => - throw _privateConstructorUsedError; - @optionalTypeArgs - TResult? mapOrNull( - TResult? Function(_DietsState value)? $default, { - TResult? Function(DietsStateLoading value)? loading, - TResult? Function(DietsStateLoaded value)? loaded, - TResult? Function(DietsStateError value)? error, - }) => - throw _privateConstructorUsedError; - @optionalTypeArgs - TResult maybeMap( - TResult Function(_DietsState value)? $default, { - TResult Function(DietsStateLoading value)? loading, - TResult Function(DietsStateLoaded value)? loaded, - TResult Function(DietsStateError value)? error, - required TResult orElse(), - }) => + DietsFilterState get filterState => throw _privateConstructorUsedError; + List get dietsList => throw _privateConstructorUsedError; + bool get isLoading => throw _privateConstructorUsedError; + String? get errorMessage => throw _privateConstructorUsedError; + + /// Create a copy of DietsState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $DietsStateCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -73,6 +33,14 @@ abstract class $DietsStateCopyWith<$Res> { factory $DietsStateCopyWith( DietsState value, $Res Function(DietsState) then) = _$DietsStateCopyWithImpl<$Res, DietsState>; + @useResult + $Res call( + {DietsFilterState filterState, + List dietsList, + bool isLoading, + String? errorMessage}); + + $DietsFilterStateCopyWith<$Res> get filterState; } /// @nodoc @@ -87,13 +55,61 @@ class _$DietsStateCopyWithImpl<$Res, $Val extends DietsState> /// Create a copy of DietsState /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? filterState = null, + Object? dietsList = null, + Object? isLoading = null, + Object? errorMessage = freezed, + }) { + return _then(_value.copyWith( + filterState: null == filterState + ? _value.filterState + : filterState // ignore: cast_nullable_to_non_nullable + as DietsFilterState, + dietsList: null == dietsList + ? _value.dietsList + : dietsList // ignore: cast_nullable_to_non_nullable + as List, + isLoading: null == isLoading + ? _value.isLoading + : isLoading // ignore: cast_nullable_to_non_nullable + as bool, + errorMessage: freezed == errorMessage + ? _value.errorMessage + : errorMessage // ignore: cast_nullable_to_non_nullable + as String?, + ) as $Val); + } + + /// Create a copy of DietsState + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $DietsFilterStateCopyWith<$Res> get filterState { + return $DietsFilterStateCopyWith<$Res>(_value.filterState, (value) { + return _then(_value.copyWith(filterState: value) as $Val); + }); + } } /// @nodoc -abstract class _$$DietsStateImplCopyWith<$Res> { +abstract class _$$DietsStateImplCopyWith<$Res> + implements $DietsStateCopyWith<$Res> { factory _$$DietsStateImplCopyWith( _$DietsStateImpl value, $Res Function(_$DietsStateImpl) then) = __$$DietsStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {DietsFilterState filterState, + List dietsList, + bool isLoading, + String? errorMessage}); + + @override + $DietsFilterStateCopyWith<$Res> get filterState; } /// @nodoc @@ -106,249 +122,19 @@ class __$$DietsStateImplCopyWithImpl<$Res> /// Create a copy of DietsState /// with the given fields replaced by the non-null parameter values. -} - -/// @nodoc - -class _$DietsStateImpl implements _DietsState { - const _$DietsStateImpl(); - - @override - String toString() { - return 'DietsState()'; - } - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && other is _$DietsStateImpl); - } - - @override - int get hashCode => runtimeType.hashCode; - - @override - @optionalTypeArgs - TResult when( - TResult Function() $default, { - required TResult Function() loading, - required TResult Function(List dietsList, bool isLoading) loaded, - required TResult Function(String errorMessage) error, - }) { - return $default(); - } - - @override - @optionalTypeArgs - TResult? whenOrNull( - TResult? Function()? $default, { - TResult? Function()? loading, - TResult? Function(List dietsList, bool isLoading)? loaded, - TResult? Function(String errorMessage)? error, - }) { - return $default?.call(); - } - - @override - @optionalTypeArgs - TResult maybeWhen( - TResult Function()? $default, { - TResult Function()? loading, - TResult Function(List dietsList, bool isLoading)? loaded, - TResult Function(String errorMessage)? error, - required TResult orElse(), - }) { - if ($default != null) { - return $default(); - } - return orElse(); - } - - @override - @optionalTypeArgs - TResult map( - TResult Function(_DietsState value) $default, { - required TResult Function(DietsStateLoading value) loading, - required TResult Function(DietsStateLoaded value) loaded, - required TResult Function(DietsStateError value) error, - }) { - return $default(this); - } - - @override - @optionalTypeArgs - TResult? mapOrNull( - TResult? Function(_DietsState value)? $default, { - TResult? Function(DietsStateLoading value)? loading, - TResult? Function(DietsStateLoaded value)? loaded, - TResult? Function(DietsStateError value)? error, - }) { - return $default?.call(this); - } - - @override - @optionalTypeArgs - TResult maybeMap( - TResult Function(_DietsState value)? $default, { - TResult Function(DietsStateLoading value)? loading, - TResult Function(DietsStateLoaded value)? loaded, - TResult Function(DietsStateError value)? error, - required TResult orElse(), - }) { - if ($default != null) { - return $default(this); - } - return orElse(); - } -} - -abstract class _DietsState implements DietsState { - const factory _DietsState() = _$DietsStateImpl; -} - -/// @nodoc -abstract class _$$DietsStateLoadingImplCopyWith<$Res> { - factory _$$DietsStateLoadingImplCopyWith(_$DietsStateLoadingImpl value, - $Res Function(_$DietsStateLoadingImpl) then) = - __$$DietsStateLoadingImplCopyWithImpl<$Res>; -} - -/// @nodoc -class __$$DietsStateLoadingImplCopyWithImpl<$Res> - extends _$DietsStateCopyWithImpl<$Res, _$DietsStateLoadingImpl> - implements _$$DietsStateLoadingImplCopyWith<$Res> { - __$$DietsStateLoadingImplCopyWithImpl(_$DietsStateLoadingImpl _value, - $Res Function(_$DietsStateLoadingImpl) _then) - : super(_value, _then); - - /// Create a copy of DietsState - /// with the given fields replaced by the non-null parameter values. -} - -/// @nodoc - -class _$DietsStateLoadingImpl implements DietsStateLoading { - const _$DietsStateLoadingImpl(); - - @override - String toString() { - return 'DietsState.loading()'; - } - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && other is _$DietsStateLoadingImpl); - } - - @override - int get hashCode => runtimeType.hashCode; - - @override - @optionalTypeArgs - TResult when( - TResult Function() $default, { - required TResult Function() loading, - required TResult Function(List dietsList, bool isLoading) loaded, - required TResult Function(String errorMessage) error, - }) { - return loading(); - } - - @override - @optionalTypeArgs - TResult? whenOrNull( - TResult? Function()? $default, { - TResult? Function()? loading, - TResult? Function(List dietsList, bool isLoading)? loaded, - TResult? Function(String errorMessage)? error, - }) { - return loading?.call(); - } - - @override - @optionalTypeArgs - TResult maybeWhen( - TResult Function()? $default, { - TResult Function()? loading, - TResult Function(List dietsList, bool isLoading)? loaded, - TResult Function(String errorMessage)? error, - required TResult orElse(), - }) { - if (loading != null) { - return loading(); - } - return orElse(); - } - - @override - @optionalTypeArgs - TResult map( - TResult Function(_DietsState value) $default, { - required TResult Function(DietsStateLoading value) loading, - required TResult Function(DietsStateLoaded value) loaded, - required TResult Function(DietsStateError value) error, - }) { - return loading(this); - } - - @override - @optionalTypeArgs - TResult? mapOrNull( - TResult? Function(_DietsState value)? $default, { - TResult? Function(DietsStateLoading value)? loading, - TResult? Function(DietsStateLoaded value)? loaded, - TResult? Function(DietsStateError value)? error, - }) { - return loading?.call(this); - } - - @override - @optionalTypeArgs - TResult maybeMap( - TResult Function(_DietsState value)? $default, { - TResult Function(DietsStateLoading value)? loading, - TResult Function(DietsStateLoaded value)? loaded, - TResult Function(DietsStateError value)? error, - required TResult orElse(), - }) { - if (loading != null) { - return loading(this); - } - return orElse(); - } -} - -abstract class DietsStateLoading implements DietsState { - const factory DietsStateLoading() = _$DietsStateLoadingImpl; -} - -/// @nodoc -abstract class _$$DietsStateLoadedImplCopyWith<$Res> { - factory _$$DietsStateLoadedImplCopyWith(_$DietsStateLoadedImpl value, - $Res Function(_$DietsStateLoadedImpl) then) = - __$$DietsStateLoadedImplCopyWithImpl<$Res>; - @useResult - $Res call({List dietsList, bool isLoading}); -} - -/// @nodoc -class __$$DietsStateLoadedImplCopyWithImpl<$Res> - extends _$DietsStateCopyWithImpl<$Res, _$DietsStateLoadedImpl> - implements _$$DietsStateLoadedImplCopyWith<$Res> { - __$$DietsStateLoadedImplCopyWithImpl(_$DietsStateLoadedImpl _value, - $Res Function(_$DietsStateLoadedImpl) _then) - : super(_value, _then); - - /// Create a copy of DietsState - /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ + Object? filterState = null, Object? dietsList = null, Object? isLoading = null, + Object? errorMessage = freezed, }) { - return _then(_$DietsStateLoadedImpl( + return _then(_$DietsStateImpl( + filterState: null == filterState + ? _value.filterState + : filterState // ignore: cast_nullable_to_non_nullable + as DietsFilterState, dietsList: null == dietsList ? _value._dietsList : dietsList // ignore: cast_nullable_to_non_nullable @@ -357,17 +143,26 @@ class __$$DietsStateLoadedImplCopyWithImpl<$Res> ? _value.isLoading : isLoading // ignore: cast_nullable_to_non_nullable as bool, + errorMessage: freezed == errorMessage + ? _value.errorMessage + : errorMessage // ignore: cast_nullable_to_non_nullable + as String?, )); } } /// @nodoc -class _$DietsStateLoadedImpl implements DietsStateLoaded { - const _$DietsStateLoadedImpl( - {required final List dietsList, required this.isLoading}) +class _$DietsStateImpl implements _DietsState { + const _$DietsStateImpl( + {required this.filterState, + required final List dietsList, + required this.isLoading, + required this.errorMessage}) : _dietsList = dietsList; + @override + final DietsFilterState filterState; final List _dietsList; @override List get dietsList { @@ -378,277 +173,233 @@ class _$DietsStateLoadedImpl implements DietsStateLoaded { @override final bool isLoading; + @override + final String? errorMessage; @override String toString() { - return 'DietsState.loaded(dietsList: $dietsList, isLoading: $isLoading)'; + return 'DietsState(filterState: $filterState, dietsList: $dietsList, isLoading: $isLoading, errorMessage: $errorMessage)'; } @override bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && - other is _$DietsStateLoadedImpl && + other is _$DietsStateImpl && + (identical(other.filterState, filterState) || + other.filterState == filterState) && const DeepCollectionEquality() .equals(other._dietsList, _dietsList) && (identical(other.isLoading, isLoading) || - other.isLoading == isLoading)); + other.isLoading == isLoading) && + (identical(other.errorMessage, errorMessage) || + other.errorMessage == errorMessage)); } @override - int get hashCode => Object.hash( - runtimeType, const DeepCollectionEquality().hash(_dietsList), isLoading); + int get hashCode => Object.hash(runtimeType, filterState, + const DeepCollectionEquality().hash(_dietsList), isLoading, errorMessage); /// Create a copy of DietsState /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') - _$$DietsStateLoadedImplCopyWith<_$DietsStateLoadedImpl> get copyWith => - __$$DietsStateLoadedImplCopyWithImpl<_$DietsStateLoadedImpl>( - this, _$identity); - - @override - @optionalTypeArgs - TResult when( - TResult Function() $default, { - required TResult Function() loading, - required TResult Function(List dietsList, bool isLoading) loaded, - required TResult Function(String errorMessage) error, - }) { - return loaded(dietsList, isLoading); - } - - @override - @optionalTypeArgs - TResult? whenOrNull( - TResult? Function()? $default, { - TResult? Function()? loading, - TResult? Function(List dietsList, bool isLoading)? loaded, - TResult? Function(String errorMessage)? error, - }) { - return loaded?.call(dietsList, isLoading); - } - - @override - @optionalTypeArgs - TResult maybeWhen( - TResult Function()? $default, { - TResult Function()? loading, - TResult Function(List dietsList, bool isLoading)? loaded, - TResult Function(String errorMessage)? error, - required TResult orElse(), - }) { - if (loaded != null) { - return loaded(dietsList, isLoading); - } - return orElse(); - } - - @override - @optionalTypeArgs - TResult map( - TResult Function(_DietsState value) $default, { - required TResult Function(DietsStateLoading value) loading, - required TResult Function(DietsStateLoaded value) loaded, - required TResult Function(DietsStateError value) error, - }) { - return loaded(this); - } - - @override - @optionalTypeArgs - TResult? mapOrNull( - TResult? Function(_DietsState value)? $default, { - TResult? Function(DietsStateLoading value)? loading, - TResult? Function(DietsStateLoaded value)? loaded, - TResult? Function(DietsStateError value)? error, - }) { - return loaded?.call(this); - } - - @override - @optionalTypeArgs - TResult maybeMap( - TResult Function(_DietsState value)? $default, { - TResult Function(DietsStateLoading value)? loading, - TResult Function(DietsStateLoaded value)? loaded, - TResult Function(DietsStateError value)? error, - required TResult orElse(), - }) { - if (loaded != null) { - return loaded(this); - } - return orElse(); - } + _$$DietsStateImplCopyWith<_$DietsStateImpl> get copyWith => + __$$DietsStateImplCopyWithImpl<_$DietsStateImpl>(this, _$identity); } -abstract class DietsStateLoaded implements DietsState { - const factory DietsStateLoaded( - {required final List dietsList, - required final bool isLoading}) = _$DietsStateLoadedImpl; +abstract class _DietsState implements DietsState { + const factory _DietsState( + {required final DietsFilterState filterState, + required final List dietsList, + required final bool isLoading, + required final String? errorMessage}) = _$DietsStateImpl; + @override + DietsFilterState get filterState; + @override List get dietsList; + @override bool get isLoading; + @override + String? get errorMessage; /// Create a copy of DietsState /// with the given fields replaced by the non-null parameter values. + @override @JsonKey(includeFromJson: false, includeToJson: false) - _$$DietsStateLoadedImplCopyWith<_$DietsStateLoadedImpl> get copyWith => + _$$DietsStateImplCopyWith<_$DietsStateImpl> get copyWith => throw _privateConstructorUsedError; } /// @nodoc -abstract class _$$DietsStateErrorImplCopyWith<$Res> { - factory _$$DietsStateErrorImplCopyWith(_$DietsStateErrorImpl value, - $Res Function(_$DietsStateErrorImpl) then) = - __$$DietsStateErrorImplCopyWithImpl<$Res>; - @useResult - $Res call({String errorMessage}); +mixin _$DietsFilterState { + int get sortingIndex => throw _privateConstructorUsedError; + int get durationIndex => throw _privateConstructorUsedError; + int get typeIndex => throw _privateConstructorUsedError; + + /// Create a copy of DietsFilterState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $DietsFilterStateCopyWith get copyWith => + throw _privateConstructorUsedError; } /// @nodoc -class __$$DietsStateErrorImplCopyWithImpl<$Res> - extends _$DietsStateCopyWithImpl<$Res, _$DietsStateErrorImpl> - implements _$$DietsStateErrorImplCopyWith<$Res> { - __$$DietsStateErrorImplCopyWithImpl( - _$DietsStateErrorImpl _value, $Res Function(_$DietsStateErrorImpl) _then) - : super(_value, _then); +abstract class $DietsFilterStateCopyWith<$Res> { + factory $DietsFilterStateCopyWith( + DietsFilterState value, $Res Function(DietsFilterState) then) = + _$DietsFilterStateCopyWithImpl<$Res, DietsFilterState>; + @useResult + $Res call({int sortingIndex, int durationIndex, int typeIndex}); +} - /// Create a copy of DietsState +/// @nodoc +class _$DietsFilterStateCopyWithImpl<$Res, $Val extends DietsFilterState> + implements $DietsFilterStateCopyWith<$Res> { + _$DietsFilterStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of DietsFilterState /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ - Object? errorMessage = null, + Object? sortingIndex = null, + Object? durationIndex = null, + Object? typeIndex = null, }) { - return _then(_$DietsStateErrorImpl( - errorMessage: null == errorMessage - ? _value.errorMessage - : errorMessage // ignore: cast_nullable_to_non_nullable - as String, + return _then(_value.copyWith( + sortingIndex: null == sortingIndex + ? _value.sortingIndex + : sortingIndex // ignore: cast_nullable_to_non_nullable + as int, + durationIndex: null == durationIndex + ? _value.durationIndex + : durationIndex // ignore: cast_nullable_to_non_nullable + as int, + typeIndex: null == typeIndex + ? _value.typeIndex + : typeIndex // ignore: cast_nullable_to_non_nullable + as int, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$DietsFilterStateImplCopyWith<$Res> + implements $DietsFilterStateCopyWith<$Res> { + factory _$$DietsFilterStateImplCopyWith(_$DietsFilterStateImpl value, + $Res Function(_$DietsFilterStateImpl) then) = + __$$DietsFilterStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({int sortingIndex, int durationIndex, int typeIndex}); +} + +/// @nodoc +class __$$DietsFilterStateImplCopyWithImpl<$Res> + extends _$DietsFilterStateCopyWithImpl<$Res, _$DietsFilterStateImpl> + implements _$$DietsFilterStateImplCopyWith<$Res> { + __$$DietsFilterStateImplCopyWithImpl(_$DietsFilterStateImpl _value, + $Res Function(_$DietsFilterStateImpl) _then) + : super(_value, _then); + + /// Create a copy of DietsFilterState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? sortingIndex = null, + Object? durationIndex = null, + Object? typeIndex = null, + }) { + return _then(_$DietsFilterStateImpl( + sortingIndex: null == sortingIndex + ? _value.sortingIndex + : sortingIndex // ignore: cast_nullable_to_non_nullable + as int, + durationIndex: null == durationIndex + ? _value.durationIndex + : durationIndex // ignore: cast_nullable_to_non_nullable + as int, + typeIndex: null == typeIndex + ? _value.typeIndex + : typeIndex // ignore: cast_nullable_to_non_nullable + as int, )); } } /// @nodoc -class _$DietsStateErrorImpl implements DietsStateError { - const _$DietsStateErrorImpl({required this.errorMessage}); +class _$DietsFilterStateImpl implements _DietsFilterState { + const _$DietsFilterStateImpl( + {required this.sortingIndex, + required this.durationIndex, + required this.typeIndex}); @override - final String errorMessage; + final int sortingIndex; + @override + final int durationIndex; + @override + final int typeIndex; @override String toString() { - return 'DietsState.error(errorMessage: $errorMessage)'; + return 'DietsFilterState(sortingIndex: $sortingIndex, durationIndex: $durationIndex, typeIndex: $typeIndex)'; } @override bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && - other is _$DietsStateErrorImpl && - (identical(other.errorMessage, errorMessage) || - other.errorMessage == errorMessage)); + other is _$DietsFilterStateImpl && + (identical(other.sortingIndex, sortingIndex) || + other.sortingIndex == sortingIndex) && + (identical(other.durationIndex, durationIndex) || + other.durationIndex == durationIndex) && + (identical(other.typeIndex, typeIndex) || + other.typeIndex == typeIndex)); } @override - int get hashCode => Object.hash(runtimeType, errorMessage); + int get hashCode => + Object.hash(runtimeType, sortingIndex, durationIndex, typeIndex); - /// Create a copy of DietsState + /// Create a copy of DietsFilterState /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') - _$$DietsStateErrorImplCopyWith<_$DietsStateErrorImpl> get copyWith => - __$$DietsStateErrorImplCopyWithImpl<_$DietsStateErrorImpl>( + _$$DietsFilterStateImplCopyWith<_$DietsFilterStateImpl> get copyWith => + __$$DietsFilterStateImplCopyWithImpl<_$DietsFilterStateImpl>( this, _$identity); - - @override - @optionalTypeArgs - TResult when( - TResult Function() $default, { - required TResult Function() loading, - required TResult Function(List dietsList, bool isLoading) loaded, - required TResult Function(String errorMessage) error, - }) { - return error(errorMessage); - } - - @override - @optionalTypeArgs - TResult? whenOrNull( - TResult? Function()? $default, { - TResult? Function()? loading, - TResult? Function(List dietsList, bool isLoading)? loaded, - TResult? Function(String errorMessage)? error, - }) { - return error?.call(errorMessage); - } - - @override - @optionalTypeArgs - TResult maybeWhen( - TResult Function()? $default, { - TResult Function()? loading, - TResult Function(List dietsList, bool isLoading)? loaded, - TResult Function(String errorMessage)? error, - required TResult orElse(), - }) { - if (error != null) { - return error(errorMessage); - } - return orElse(); - } - - @override - @optionalTypeArgs - TResult map( - TResult Function(_DietsState value) $default, { - required TResult Function(DietsStateLoading value) loading, - required TResult Function(DietsStateLoaded value) loaded, - required TResult Function(DietsStateError value) error, - }) { - return error(this); - } - - @override - @optionalTypeArgs - TResult? mapOrNull( - TResult? Function(_DietsState value)? $default, { - TResult? Function(DietsStateLoading value)? loading, - TResult? Function(DietsStateLoaded value)? loaded, - TResult? Function(DietsStateError value)? error, - }) { - return error?.call(this); - } - - @override - @optionalTypeArgs - TResult maybeMap( - TResult Function(_DietsState value)? $default, { - TResult Function(DietsStateLoading value)? loading, - TResult Function(DietsStateLoaded value)? loaded, - TResult Function(DietsStateError value)? error, - required TResult orElse(), - }) { - if (error != null) { - return error(this); - } - return orElse(); - } } -abstract class DietsStateError implements DietsState { - const factory DietsStateError({required final String errorMessage}) = - _$DietsStateErrorImpl; +abstract class _DietsFilterState implements DietsFilterState { + const factory _DietsFilterState( + {required final int sortingIndex, + required final int durationIndex, + required final int typeIndex}) = _$DietsFilterStateImpl; - String get errorMessage; + @override + int get sortingIndex; + @override + int get durationIndex; + @override + int get typeIndex; - /// Create a copy of DietsState + /// Create a copy of DietsFilterState /// with the given fields replaced by the non-null parameter values. + @override @JsonKey(includeFromJson: false, includeToJson: false) - _$$DietsStateErrorImplCopyWith<_$DietsStateErrorImpl> get copyWith => + _$$DietsFilterStateImplCopyWith<_$DietsFilterStateImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/features/feed/presentation/pages/feed_screen.dart b/lib/features/feed/presentation/pages/feed_screen.dart index 89f4914c..f86430ef 100644 --- a/lib/features/feed/presentation/pages/feed_screen.dart +++ b/lib/features/feed/presentation/pages/feed_screen.dart @@ -1,5 +1,7 @@ import 'package:easy_debounce/easy_debounce.dart'; +import 'package:eatvio_app/features/create_diet/presentation/create_diet_screen.dart'; import 'package:eatvio_app/core/widgets/loader.dart'; +import 'package:eatvio_app/features/create_plan/presentation/create_plan_screen.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_state.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart'; @@ -259,9 +261,25 @@ class _FeedScreenState extends State { onRefresh: () => dietsCubit.loadDiets(), child: BlocBuilder( builder: (BuildContext context, DietsState state) { - if (state is DietsStateLoaded) { + if (state.isLoading) { + return Scaffold( + backgroundColor: Colors.grey.shade200, + body: const Loader(), + ); + } else if (state.errorMessage != null) { + return Scaffold( + backgroundColor: Colors.grey.shade200, + body: Padding( + padding: const EdgeInsets.all(16), + child: Center( + child: Text(state.errorMessage ?? ''), + ), + ), + ); + } else { return Stack( children: [ + SizedBox.expand(), ListView.separated( padding: EdgeInsets.symmetric(vertical: 8), shrinkWrap: true, @@ -278,26 +296,13 @@ class _FeedScreenState extends State { right: 0, bottom: 74, child: FilterBadge( - filterBottomSheet: DietFilterBottomSheet(), + filterBottomSheet: DietFilterBottomSheet( + dietsCubit: dietsCubit, + ), ), ), ], ); - } else if (state is DietsStateError) { - return Scaffold( - backgroundColor: Colors.grey.shade200, - body: Padding( - padding: const EdgeInsets.all(16), - child: Center( - child: Text(state.errorMessage), - ), - ), - ); - } else { - return Scaffold( - backgroundColor: Colors.grey.shade200, - body: const Loader(), - ); } }, ), @@ -431,9 +436,12 @@ class _FeedScreenState extends State { _addItemButton(FeedCubit feedCubit) { return InkWell( onTap: () { - switch (_selectedChipIndex) { - case 1: - GoRouter.of(context).pushNamed('recipeCreateScreen'); + switch (_selectedChipIndex) { + case 2: + GoRouter.of(context).pushNamed(CreateDietScreen.name); + break; + case 3: + GoRouter.of(context).pushNamed(CreatePlanScreen.name); break; default: GoRouter.of(context).pushNamed('blogPostCreateScreen', extra: feedCubit); diff --git a/lib/features/feed/presentation/widgets/choice_chip_widget.dart b/lib/features/feed/presentation/widgets/choice_chip_widget.dart index d20a2dad..a2e7fc30 100644 --- a/lib/features/feed/presentation/widgets/choice_chip_widget.dart +++ b/lib/features/feed/presentation/widgets/choice_chip_widget.dart @@ -2,11 +2,27 @@ import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:flutter/material.dart'; class ChoiceChipWidget extends StatelessWidget { - const ChoiceChipWidget({super.key, required this.chipsLabelList, required this.index, required this.selectedChipIndex, required this.onSelected}); + const ChoiceChipWidget({ + super.key, + required this.chipsLabelList, + required this.index, + required this.selectedChipIndex, + required this.onSelected, + this.padding, + this.fontSize, + this.activeColor, + this.inactiveColor, + this.fontWeight, + }); final List chipsLabelList; final int index; final int selectedChipIndex; + final double? fontSize; + final Color? activeColor; + final Color? inactiveColor; + final FontWeight? fontWeight; final void Function(bool) onSelected; + final EdgeInsetsGeometry? padding; @override Widget build(BuildContext context) { return ChoiceChip( @@ -17,12 +33,12 @@ class ChoiceChipWidget extends StatelessWidget { shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + padding: padding ?? const EdgeInsets.symmetric(horizontal: 8, vertical: 4), label: Text(chipsLabelList[index]), labelStyle: TextStyle( - color: index == selectedChipIndex ? Colors.white : appTheme.gray600, - fontWeight: FontWeight.w500, - fontSize: 12, + color: index == selectedChipIndex ? activeColor ?? Colors.white : inactiveColor ?? appTheme.gray600, + fontWeight: fontWeight ?? FontWeight.w500, + fontSize: fontSize ?? 12, ), selected: selectedChipIndex == index, backgroundColor: appTheme.gray100, diff --git a/lib/features/feed/presentation/widgets/diet_filter_bottom_sheet.dart b/lib/features/feed/presentation/widgets/diet_filter_bottom_sheet.dart index 37d720d3..cdd87f8a 100644 --- a/lib/features/feed/presentation/widgets/diet_filter_bottom_sheet.dart +++ b/lib/features/feed/presentation/widgets/diet_filter_bottom_sheet.dart @@ -1,5 +1,7 @@ import 'package:eatvio_app/core/widgets/bottom_sheet_handler_bar.dart'; import 'package:eatvio_app/core/widgets/custom_divider.dart'; +import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart'; +import 'package:eatvio_app/features/feed/presentation/widgets/duration_choice_filter_widget.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/sorting_drop_down_filter.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/type_choice_filter_widget.dart'; import 'package:eatvio_app/generated/l10n.dart'; @@ -8,8 +10,8 @@ import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:flutter/material.dart'; class DietFilterBottomSheet extends StatelessWidget { - const DietFilterBottomSheet({super.key}); - + const DietFilterBottomSheet({super.key, required this.dietsCubit}); + final DietsCubit dietsCubit; @override Widget build(BuildContext context) { final s = S.of(context); @@ -29,7 +31,7 @@ class DietFilterBottomSheet extends StatelessWidget { style: TextStyles.textMSemibold.copyWith(color: appTheme.black), ), SizedBox(height: 12), - SortingDropDownFilter(), + SortingDropDownFilter(cubit: dietsCubit,), SizedBox( height: 16, ), @@ -44,7 +46,7 @@ class DietFilterBottomSheet extends StatelessWidget { SizedBox( height: 12, ), - TypeChoiceFilterWidget(), + TypeChoiceFilterWidget(cubit: dietsCubit,), SizedBox( height: 16, ), @@ -59,7 +61,7 @@ class DietFilterBottomSheet extends StatelessWidget { SizedBox( height: 12, ), - TypeChoiceFilterWidget(), + DurationChoiceFilterWidget(cubit: dietsCubit,), SizedBox( height: 24, ), diff --git a/lib/features/feed/presentation/widgets/duration_choice_filter_widget.dart b/lib/features/feed/presentation/widgets/duration_choice_filter_widget.dart index 7be17e8e..d3a5ec9b 100644 --- a/lib/features/feed/presentation/widgets/duration_choice_filter_widget.dart +++ b/lib/features/feed/presentation/widgets/duration_choice_filter_widget.dart @@ -1,84 +1,85 @@ +import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/utils/localization_utils.dart'; +import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart'; +import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_state.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/choice_chip_widget.dart'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/widgets.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; class DurationChoiceFilterWidget extends StatefulWidget { - const DurationChoiceFilterWidget({super.key}); - + const DurationChoiceFilterWidget({super.key, required this.cubit}); + final DietsCubit cubit; @override State createState() => _DurationChoiceFilterWidgetState(); } class _DurationChoiceFilterWidgetState extends State { final List chipsLabelList = []; - int selectedChipIndex = 0; + @override void didChangeDependencies() { final s = S.of(context); super.didChangeDependencies(); if (chipsLabelList.isEmpty) { chipsLabelList.add(s.all); - chipsLabelList.add('7 ${getDayForm(7, s)}'); - chipsLabelList.add('14 ${getDayForm(14, s)}'); - chipsLabelList.add('21 ${getDayForm(21, s)}'); + durationValues.forEach((element) { + chipsLabelList.add('${element} ${getDayForm(element, s)}'); + }); } } @override Widget build(BuildContext context) { - return Wrap( - children: [ - ChoiceChipWidget( - chipsLabelList: chipsLabelList, - index: 0, - selectedChipIndex: selectedChipIndex, - onSelected: (selectedValue) { - setState(() { - selectedChipIndex = 0; - }); - }, - ), - SizedBox( - width: 8, - ), - ChoiceChipWidget( - chipsLabelList: chipsLabelList, - index: 1, - selectedChipIndex: selectedChipIndex, - onSelected: (selectedValue) { - setState(() { - selectedChipIndex = 1; - }); - }, - ), - SizedBox( - width: 8, - ), - ChoiceChipWidget( - chipsLabelList: chipsLabelList, - index: 2, - selectedChipIndex: selectedChipIndex, - onSelected: (selectedValue) { - setState(() { - selectedChipIndex = 2; - }); - }, - ), - SizedBox( - width: 8, - ), - ChoiceChipWidget( - chipsLabelList: chipsLabelList, - index: 3, - selectedChipIndex: selectedChipIndex, - onSelected: (selectedValue) { - setState(() { - selectedChipIndex = 3; - }); - }, - ), - ], + return BlocBuilder( + bloc: widget.cubit, + builder: (context, state) { + return Wrap( + children: [ + ChoiceChipWidget( + chipsLabelList: chipsLabelList, + index: 0, + selectedChipIndex: state.filterState.durationIndex, + onSelected: (selectedValue) { + widget.cubit.changeDurationFilter(0); + }, + ), + SizedBox( + width: 8, + ), + ChoiceChipWidget( + chipsLabelList: chipsLabelList, + index: 1, + selectedChipIndex: state.filterState.durationIndex, + onSelected: (selectedValue) { + widget.cubit.changeDurationFilter(1); + }, + ), + SizedBox( + width: 8, + ), + ChoiceChipWidget( + chipsLabelList: chipsLabelList, + index: 2, + selectedChipIndex: state.filterState.durationIndex, + onSelected: (selectedValue) { + widget.cubit.changeDurationFilter(2); + }, + ), + SizedBox( + width: 8, + ), + ChoiceChipWidget( + chipsLabelList: chipsLabelList, + index: 3, + selectedChipIndex: state.filterState.durationIndex, + onSelected: (selectedValue) { + widget.cubit.changeDurationFilter(3); + }, + ), + ], + ); + }, ); } } diff --git a/lib/features/feed/presentation/widgets/sorting_drop_down_filter.dart b/lib/features/feed/presentation/widgets/sorting_drop_down_filter.dart index 9563130d..ec85409d 100644 --- a/lib/features/feed/presentation/widgets/sorting_drop_down_filter.dart +++ b/lib/features/feed/presentation/widgets/sorting_drop_down_filter.dart @@ -1,20 +1,22 @@ import 'package:eatvio_app/core/theme/custom_text_style.dart'; +import 'package:eatvio_app/core/widgets/custom_drop_down_button.dart'; +import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart'; +import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_state.dart'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:flutter/material.dart'; import 'package:collection/collection.dart'; -import 'package:flutter_svg/flutter_svg.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; class SortingDropDownFilter extends StatefulWidget { - const SortingDropDownFilter({super.key}); - + const SortingDropDownFilter({super.key, required this.cubit}); + final DietsCubit cubit; @override State createState() => _SortingDropDownFilterState(); } class _SortingDropDownFilterState extends State { final List sortingTypes = []; - int selectedIndex = 0; @override void didChangeDependencies() { super.didChangeDependencies(); @@ -24,46 +26,35 @@ class _SortingDropDownFilterState extends State { sortingTypes.add(s.popular); sortingTypes.add(s.most_new); sortingTypes.add(s.most_old); - sortingTypes.add(s.most_liked); + sortingTypes.add(s.from_a_to_z); + sortingTypes.add(s.from_z_to_a); } } @override Widget build(BuildContext context) { - return DropdownButtonFormField( - icon: SvgPicture.asset( - 'assets/icons/arrow_down2.svg', - width: 12, - height: 8, - ), - borderRadius: BorderRadius.circular(20), - dropdownColor: appTheme.lightGray0, - decoration: InputDecoration( - border: OutlineInputBorder( - borderRadius: const BorderRadius.all(Radius.circular(20)), - borderSide: BorderSide.none, - ), - fillColor: appTheme.lightGray0, - filled: true, - ), - value: sortingTypes[selectedIndex], - items: sortingTypes - .mapIndexed( - (index, value) => DropdownMenuItem( - child: Text( - sortingTypes[index], - style: TextStyles.textMMedium.copyWith(color: appTheme.black), - ), - value: sortingTypes[index], - ), - ) - .toList(), - onChanged: (value) { - if (value != null) { - setState(() { - selectedIndex = sortingTypes.indexOf(value); - }); - } + return BlocBuilder( + bloc: widget.cubit, + builder: (context, state) { + return CustomDropDownButton( + items: sortingTypes + .mapIndexed( + (index, value) => DropdownMenuItem( + child: Text( + sortingTypes[index], + style: TextStyles.textMMedium.copyWith(color: appTheme.black), + ), + value: sortingTypes[index], + ), + ) + .toList(), + value: sortingTypes[state.filterState.sortingIndex], + onChanged: (value) { + if (value != null) { + widget.cubit.changeSortingType(sortingTypes.indexOf(value)); + } + }, + ); }, ); } diff --git a/lib/features/feed/presentation/widgets/type_choice_filter_widget.dart b/lib/features/feed/presentation/widgets/type_choice_filter_widget.dart index d7b57801..b4cd6bca 100644 --- a/lib/features/feed/presentation/widgets/type_choice_filter_widget.dart +++ b/lib/features/feed/presentation/widgets/type_choice_filter_widget.dart @@ -1,17 +1,19 @@ +import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart'; +import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_state.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/choice_chip_widget.dart'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/widgets.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; class TypeChoiceFilterWidget extends StatefulWidget { - const TypeChoiceFilterWidget({super.key}); - + const TypeChoiceFilterWidget({super.key, required this.cubit}); + final DietsCubit cubit; @override State createState() => _TypeChoiceFilterWidgetState(); } class _TypeChoiceFilterWidgetState extends State { final List chipsLabelList = []; - int selectedChipIndex = 0; @override void didChangeDependencies() { final s = S.of(context); @@ -19,79 +21,74 @@ class _TypeChoiceFilterWidgetState extends State { if (chipsLabelList.isEmpty) { chipsLabelList.add(s.all); chipsLabelList.add(s.vegan_type); - chipsLabelList.add(s.alko_type); - chipsLabelList.add(s.corb_type); + chipsLabelList.add(s.light_type); + chipsLabelList.add(s.carb_type); chipsLabelList.add(s.meat_type); } } @override Widget build(BuildContext context) { - return Wrap( - children: [ - ChoiceChipWidget( - chipsLabelList: chipsLabelList, - index: 0, - selectedChipIndex: selectedChipIndex, - onSelected: (selectedValue) { - setState(() { - selectedChipIndex = 0; - }); - }, - ), - SizedBox( - width: 8, - ), - ChoiceChipWidget( - chipsLabelList: chipsLabelList, - index: 1, - selectedChipIndex: selectedChipIndex, - onSelected: (selectedValue) { - setState(() { - selectedChipIndex = 1; - }); - }, - ), - SizedBox( - width: 8, - ), - ChoiceChipWidget( - chipsLabelList: chipsLabelList, - index: 2, - selectedChipIndex: selectedChipIndex, - onSelected: (selectedValue) { - setState(() { - selectedChipIndex = 2; - }); - }, - ), - SizedBox( - width: 8, - ), - ChoiceChipWidget( - chipsLabelList: chipsLabelList, - index: 3, - selectedChipIndex: selectedChipIndex, - onSelected: (selectedValue) { - setState(() { - selectedChipIndex = 3; - }); - }, - ), - SizedBox( - width: 8, - ), - ChoiceChipWidget( - chipsLabelList: chipsLabelList, - index: 4, - selectedChipIndex: selectedChipIndex, - onSelected: (selectedValue) { - setState(() { - selectedChipIndex = 4; - }); - }, - ), - ], + return BlocBuilder( + bloc: widget.cubit, + builder: (context, state) { + return Wrap( + children: [ + ChoiceChipWidget( + chipsLabelList: chipsLabelList, + index: 0, + selectedChipIndex: state.filterState.typeIndex, + onSelected: (selectedValue) { + widget.cubit.changeTypeFilter(0); + }, + ), + SizedBox( + width: 8, + ), + ChoiceChipWidget( + chipsLabelList: chipsLabelList, + index: 1, + selectedChipIndex: state.filterState.typeIndex, + onSelected: (selectedValue) { + widget.cubit.changeTypeFilter(1); + }, + ), + SizedBox( + width: 8, + ), + ChoiceChipWidget( + chipsLabelList: chipsLabelList, + index: 2, + selectedChipIndex: state.filterState.typeIndex, + onSelected: (selectedValue) { + widget.cubit.changeTypeFilter(2); + }, + ), + SizedBox( + width: 8, + ), + ChoiceChipWidget( + chipsLabelList: chipsLabelList, + index: 3, + selectedChipIndex: state.filterState.typeIndex, + onSelected: (selectedValue) { + widget.cubit.changeTypeFilter(3); + }, + ), + SizedBox( + width: 8, + ), + ChoiceChipWidget( + chipsLabelList: chipsLabelList, + index: 4, + selectedChipIndex: state.filterState.typeIndex, + onSelected: (selectedValue) { + widget.cubit.changeTypeFilter(4); + }, + ), + ], + ); + }, ); } } diff --git a/lib/features/glossary/models/glossary_model.dart b/lib/features/glossary/models/glossary_model.dart index 8e437d95..c1daa13a 100644 --- a/lib/features/glossary/models/glossary_model.dart +++ b/lib/features/glossary/models/glossary_model.dart @@ -12,12 +12,29 @@ class GlossaryPaginationModel with _$GlossaryPaginationModel { factory GlossaryPaginationModel.fromJson(Map json) => _$GlossaryPaginationModelFromJson(json); } +@freezed +class GlossaryModerationModel with _$GlossaryModerationModel { + const factory GlossaryModerationModel({ + @JsonKey(name: 'product_nutritional_value') required GlossaryModel glossary, + }) = _GlossaryModerationModel; + + factory GlossaryModerationModel.fromJson(Map json) => _$GlossaryModerationModelFromJson(json); +} + @freezed class GlossaryModel with _$GlossaryModel { const factory GlossaryModel({ required int id, required String name, @JsonKey(name: 'portion_mass_gr') required int grInPortion, + @JsonKey(name: 'energy_on_100gr') required double energyOn100Gr, + @JsonKey(name: 'proteins_on_100gr') required double proteinsOn100Gr, + @JsonKey(name: 'fats_on_100gr') required double fatsOn100Gr, + @JsonKey(name: 'carbohydrates_on_100gr') required double carbohydratesOn100Gr, + @JsonKey(name: 'energy_on_portion') required double energyOnPortion, + @JsonKey(name: 'proteins_on_portion') required double proteinsOnPortion, + @JsonKey(name: 'fats_on_portion') required double fatsOnPortion, + @JsonKey(name: 'carbohydrates_on_portion') required double carbohydratesOnPortion, }) = _GlossaryModel; factory GlossaryModel.fromJson(Map json) => _$GlossaryModelFromJson(json); diff --git a/lib/features/glossary/models/glossary_model.freezed.dart b/lib/features/glossary/models/glossary_model.freezed.dart index d5e1556f..ababa94f 100644 --- a/lib/features/glossary/models/glossary_model.freezed.dart +++ b/lib/features/glossary/models/glossary_model.freezed.dart @@ -185,6 +185,185 @@ abstract class _GlossaryPaginationModel implements GlossaryPaginationModel { get copyWith => throw _privateConstructorUsedError; } +GlossaryModerationModel _$GlossaryModerationModelFromJson( + Map json) { + return _GlossaryModerationModel.fromJson(json); +} + +/// @nodoc +mixin _$GlossaryModerationModel { + @JsonKey(name: 'product_nutritional_value') + GlossaryModel get glossary => throw _privateConstructorUsedError; + + /// Serializes this GlossaryModerationModel to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of GlossaryModerationModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $GlossaryModerationModelCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $GlossaryModerationModelCopyWith<$Res> { + factory $GlossaryModerationModelCopyWith(GlossaryModerationModel value, + $Res Function(GlossaryModerationModel) then) = + _$GlossaryModerationModelCopyWithImpl<$Res, GlossaryModerationModel>; + @useResult + $Res call( + {@JsonKey(name: 'product_nutritional_value') GlossaryModel glossary}); + + $GlossaryModelCopyWith<$Res> get glossary; +} + +/// @nodoc +class _$GlossaryModerationModelCopyWithImpl<$Res, + $Val extends GlossaryModerationModel> + implements $GlossaryModerationModelCopyWith<$Res> { + _$GlossaryModerationModelCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of GlossaryModerationModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? glossary = null, + }) { + return _then(_value.copyWith( + glossary: null == glossary + ? _value.glossary + : glossary // ignore: cast_nullable_to_non_nullable + as GlossaryModel, + ) as $Val); + } + + /// Create a copy of GlossaryModerationModel + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $GlossaryModelCopyWith<$Res> get glossary { + return $GlossaryModelCopyWith<$Res>(_value.glossary, (value) { + return _then(_value.copyWith(glossary: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$GlossaryModerationModelImplCopyWith<$Res> + implements $GlossaryModerationModelCopyWith<$Res> { + factory _$$GlossaryModerationModelImplCopyWith( + _$GlossaryModerationModelImpl value, + $Res Function(_$GlossaryModerationModelImpl) then) = + __$$GlossaryModerationModelImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {@JsonKey(name: 'product_nutritional_value') GlossaryModel glossary}); + + @override + $GlossaryModelCopyWith<$Res> get glossary; +} + +/// @nodoc +class __$$GlossaryModerationModelImplCopyWithImpl<$Res> + extends _$GlossaryModerationModelCopyWithImpl<$Res, + _$GlossaryModerationModelImpl> + implements _$$GlossaryModerationModelImplCopyWith<$Res> { + __$$GlossaryModerationModelImplCopyWithImpl( + _$GlossaryModerationModelImpl _value, + $Res Function(_$GlossaryModerationModelImpl) _then) + : super(_value, _then); + + /// Create a copy of GlossaryModerationModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? glossary = null, + }) { + return _then(_$GlossaryModerationModelImpl( + glossary: null == glossary + ? _value.glossary + : glossary // ignore: cast_nullable_to_non_nullable + as GlossaryModel, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$GlossaryModerationModelImpl implements _GlossaryModerationModel { + const _$GlossaryModerationModelImpl( + {@JsonKey(name: 'product_nutritional_value') required this.glossary}); + + factory _$GlossaryModerationModelImpl.fromJson(Map json) => + _$$GlossaryModerationModelImplFromJson(json); + + @override + @JsonKey(name: 'product_nutritional_value') + final GlossaryModel glossary; + + @override + String toString() { + return 'GlossaryModerationModel(glossary: $glossary)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$GlossaryModerationModelImpl && + (identical(other.glossary, glossary) || + other.glossary == glossary)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, glossary); + + /// Create a copy of GlossaryModerationModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$GlossaryModerationModelImplCopyWith<_$GlossaryModerationModelImpl> + get copyWith => __$$GlossaryModerationModelImplCopyWithImpl< + _$GlossaryModerationModelImpl>(this, _$identity); + + @override + Map toJson() { + return _$$GlossaryModerationModelImplToJson( + this, + ); + } +} + +abstract class _GlossaryModerationModel implements GlossaryModerationModel { + const factory _GlossaryModerationModel( + {@JsonKey(name: 'product_nutritional_value') + required final GlossaryModel glossary}) = _$GlossaryModerationModelImpl; + + factory _GlossaryModerationModel.fromJson(Map json) = + _$GlossaryModerationModelImpl.fromJson; + + @override + @JsonKey(name: 'product_nutritional_value') + GlossaryModel get glossary; + + /// Create a copy of GlossaryModerationModel + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$GlossaryModerationModelImplCopyWith<_$GlossaryModerationModelImpl> + get copyWith => throw _privateConstructorUsedError; +} + GlossaryModel _$GlossaryModelFromJson(Map json) { return _GlossaryModel.fromJson(json); } @@ -195,6 +374,22 @@ mixin _$GlossaryModel { String get name => throw _privateConstructorUsedError; @JsonKey(name: 'portion_mass_gr') int get grInPortion => throw _privateConstructorUsedError; + @JsonKey(name: 'energy_on_100gr') + double get energyOn100Gr => throw _privateConstructorUsedError; + @JsonKey(name: 'proteins_on_100gr') + double get proteinsOn100Gr => throw _privateConstructorUsedError; + @JsonKey(name: 'fats_on_100gr') + double get fatsOn100Gr => throw _privateConstructorUsedError; + @JsonKey(name: 'carbohydrates_on_100gr') + double get carbohydratesOn100Gr => throw _privateConstructorUsedError; + @JsonKey(name: 'energy_on_portion') + double get energyOnPortion => throw _privateConstructorUsedError; + @JsonKey(name: 'proteins_on_portion') + double get proteinsOnPortion => throw _privateConstructorUsedError; + @JsonKey(name: 'fats_on_portion') + double get fatsOnPortion => throw _privateConstructorUsedError; + @JsonKey(name: 'carbohydrates_on_portion') + double get carbohydratesOnPortion => throw _privateConstructorUsedError; /// Serializes this GlossaryModel to a JSON map. Map toJson() => throw _privateConstructorUsedError; @@ -213,7 +408,18 @@ abstract class $GlossaryModelCopyWith<$Res> { _$GlossaryModelCopyWithImpl<$Res, GlossaryModel>; @useResult $Res call( - {int id, String name, @JsonKey(name: 'portion_mass_gr') int grInPortion}); + {int id, + String name, + @JsonKey(name: 'portion_mass_gr') int grInPortion, + @JsonKey(name: 'energy_on_100gr') double energyOn100Gr, + @JsonKey(name: 'proteins_on_100gr') double proteinsOn100Gr, + @JsonKey(name: 'fats_on_100gr') double fatsOn100Gr, + @JsonKey(name: 'carbohydrates_on_100gr') double carbohydratesOn100Gr, + @JsonKey(name: 'energy_on_portion') double energyOnPortion, + @JsonKey(name: 'proteins_on_portion') double proteinsOnPortion, + @JsonKey(name: 'fats_on_portion') double fatsOnPortion, + @JsonKey(name: 'carbohydrates_on_portion') + double carbohydratesOnPortion}); } /// @nodoc @@ -234,6 +440,14 @@ class _$GlossaryModelCopyWithImpl<$Res, $Val extends GlossaryModel> Object? id = null, Object? name = null, Object? grInPortion = null, + Object? energyOn100Gr = null, + Object? proteinsOn100Gr = null, + Object? fatsOn100Gr = null, + Object? carbohydratesOn100Gr = null, + Object? energyOnPortion = null, + Object? proteinsOnPortion = null, + Object? fatsOnPortion = null, + Object? carbohydratesOnPortion = null, }) { return _then(_value.copyWith( id: null == id @@ -248,6 +462,38 @@ class _$GlossaryModelCopyWithImpl<$Res, $Val extends GlossaryModel> ? _value.grInPortion : grInPortion // ignore: cast_nullable_to_non_nullable as int, + energyOn100Gr: null == energyOn100Gr + ? _value.energyOn100Gr + : energyOn100Gr // ignore: cast_nullable_to_non_nullable + as double, + proteinsOn100Gr: null == proteinsOn100Gr + ? _value.proteinsOn100Gr + : proteinsOn100Gr // ignore: cast_nullable_to_non_nullable + as double, + fatsOn100Gr: null == fatsOn100Gr + ? _value.fatsOn100Gr + : fatsOn100Gr // ignore: cast_nullable_to_non_nullable + as double, + carbohydratesOn100Gr: null == carbohydratesOn100Gr + ? _value.carbohydratesOn100Gr + : carbohydratesOn100Gr // ignore: cast_nullable_to_non_nullable + as double, + energyOnPortion: null == energyOnPortion + ? _value.energyOnPortion + : energyOnPortion // ignore: cast_nullable_to_non_nullable + as double, + proteinsOnPortion: null == proteinsOnPortion + ? _value.proteinsOnPortion + : proteinsOnPortion // ignore: cast_nullable_to_non_nullable + as double, + fatsOnPortion: null == fatsOnPortion + ? _value.fatsOnPortion + : fatsOnPortion // ignore: cast_nullable_to_non_nullable + as double, + carbohydratesOnPortion: null == carbohydratesOnPortion + ? _value.carbohydratesOnPortion + : carbohydratesOnPortion // ignore: cast_nullable_to_non_nullable + as double, ) as $Val); } } @@ -261,7 +507,18 @@ abstract class _$$GlossaryModelImplCopyWith<$Res> @override @useResult $Res call( - {int id, String name, @JsonKey(name: 'portion_mass_gr') int grInPortion}); + {int id, + String name, + @JsonKey(name: 'portion_mass_gr') int grInPortion, + @JsonKey(name: 'energy_on_100gr') double energyOn100Gr, + @JsonKey(name: 'proteins_on_100gr') double proteinsOn100Gr, + @JsonKey(name: 'fats_on_100gr') double fatsOn100Gr, + @JsonKey(name: 'carbohydrates_on_100gr') double carbohydratesOn100Gr, + @JsonKey(name: 'energy_on_portion') double energyOnPortion, + @JsonKey(name: 'proteins_on_portion') double proteinsOnPortion, + @JsonKey(name: 'fats_on_portion') double fatsOnPortion, + @JsonKey(name: 'carbohydrates_on_portion') + double carbohydratesOnPortion}); } /// @nodoc @@ -280,6 +537,14 @@ class __$$GlossaryModelImplCopyWithImpl<$Res> Object? id = null, Object? name = null, Object? grInPortion = null, + Object? energyOn100Gr = null, + Object? proteinsOn100Gr = null, + Object? fatsOn100Gr = null, + Object? carbohydratesOn100Gr = null, + Object? energyOnPortion = null, + Object? proteinsOnPortion = null, + Object? fatsOnPortion = null, + Object? carbohydratesOnPortion = null, }) { return _then(_$GlossaryModelImpl( id: null == id @@ -294,6 +559,38 @@ class __$$GlossaryModelImplCopyWithImpl<$Res> ? _value.grInPortion : grInPortion // ignore: cast_nullable_to_non_nullable as int, + energyOn100Gr: null == energyOn100Gr + ? _value.energyOn100Gr + : energyOn100Gr // ignore: cast_nullable_to_non_nullable + as double, + proteinsOn100Gr: null == proteinsOn100Gr + ? _value.proteinsOn100Gr + : proteinsOn100Gr // ignore: cast_nullable_to_non_nullable + as double, + fatsOn100Gr: null == fatsOn100Gr + ? _value.fatsOn100Gr + : fatsOn100Gr // ignore: cast_nullable_to_non_nullable + as double, + carbohydratesOn100Gr: null == carbohydratesOn100Gr + ? _value.carbohydratesOn100Gr + : carbohydratesOn100Gr // ignore: cast_nullable_to_non_nullable + as double, + energyOnPortion: null == energyOnPortion + ? _value.energyOnPortion + : energyOnPortion // ignore: cast_nullable_to_non_nullable + as double, + proteinsOnPortion: null == proteinsOnPortion + ? _value.proteinsOnPortion + : proteinsOnPortion // ignore: cast_nullable_to_non_nullable + as double, + fatsOnPortion: null == fatsOnPortion + ? _value.fatsOnPortion + : fatsOnPortion // ignore: cast_nullable_to_non_nullable + as double, + carbohydratesOnPortion: null == carbohydratesOnPortion + ? _value.carbohydratesOnPortion + : carbohydratesOnPortion // ignore: cast_nullable_to_non_nullable + as double, )); } } @@ -304,7 +601,17 @@ class _$GlossaryModelImpl implements _GlossaryModel { const _$GlossaryModelImpl( {required this.id, required this.name, - @JsonKey(name: 'portion_mass_gr') required this.grInPortion}); + @JsonKey(name: 'portion_mass_gr') required this.grInPortion, + @JsonKey(name: 'energy_on_100gr') required this.energyOn100Gr, + @JsonKey(name: 'proteins_on_100gr') required this.proteinsOn100Gr, + @JsonKey(name: 'fats_on_100gr') required this.fatsOn100Gr, + @JsonKey(name: 'carbohydrates_on_100gr') + required this.carbohydratesOn100Gr, + @JsonKey(name: 'energy_on_portion') required this.energyOnPortion, + @JsonKey(name: 'proteins_on_portion') required this.proteinsOnPortion, + @JsonKey(name: 'fats_on_portion') required this.fatsOnPortion, + @JsonKey(name: 'carbohydrates_on_portion') + required this.carbohydratesOnPortion}); factory _$GlossaryModelImpl.fromJson(Map json) => _$$GlossaryModelImplFromJson(json); @@ -316,10 +623,34 @@ class _$GlossaryModelImpl implements _GlossaryModel { @override @JsonKey(name: 'portion_mass_gr') final int grInPortion; + @override + @JsonKey(name: 'energy_on_100gr') + final double energyOn100Gr; + @override + @JsonKey(name: 'proteins_on_100gr') + final double proteinsOn100Gr; + @override + @JsonKey(name: 'fats_on_100gr') + final double fatsOn100Gr; + @override + @JsonKey(name: 'carbohydrates_on_100gr') + final double carbohydratesOn100Gr; + @override + @JsonKey(name: 'energy_on_portion') + final double energyOnPortion; + @override + @JsonKey(name: 'proteins_on_portion') + final double proteinsOnPortion; + @override + @JsonKey(name: 'fats_on_portion') + final double fatsOnPortion; + @override + @JsonKey(name: 'carbohydrates_on_portion') + final double carbohydratesOnPortion; @override String toString() { - return 'GlossaryModel(id: $id, name: $name, grInPortion: $grInPortion)'; + return 'GlossaryModel(id: $id, name: $name, grInPortion: $grInPortion, energyOn100Gr: $energyOn100Gr, proteinsOn100Gr: $proteinsOn100Gr, fatsOn100Gr: $fatsOn100Gr, carbohydratesOn100Gr: $carbohydratesOn100Gr, energyOnPortion: $energyOnPortion, proteinsOnPortion: $proteinsOnPortion, fatsOnPortion: $fatsOnPortion, carbohydratesOnPortion: $carbohydratesOnPortion)'; } @override @@ -330,12 +661,40 @@ class _$GlossaryModelImpl implements _GlossaryModel { (identical(other.id, id) || other.id == id) && (identical(other.name, name) || other.name == name) && (identical(other.grInPortion, grInPortion) || - other.grInPortion == grInPortion)); + other.grInPortion == grInPortion) && + (identical(other.energyOn100Gr, energyOn100Gr) || + other.energyOn100Gr == energyOn100Gr) && + (identical(other.proteinsOn100Gr, proteinsOn100Gr) || + other.proteinsOn100Gr == proteinsOn100Gr) && + (identical(other.fatsOn100Gr, fatsOn100Gr) || + other.fatsOn100Gr == fatsOn100Gr) && + (identical(other.carbohydratesOn100Gr, carbohydratesOn100Gr) || + other.carbohydratesOn100Gr == carbohydratesOn100Gr) && + (identical(other.energyOnPortion, energyOnPortion) || + other.energyOnPortion == energyOnPortion) && + (identical(other.proteinsOnPortion, proteinsOnPortion) || + other.proteinsOnPortion == proteinsOnPortion) && + (identical(other.fatsOnPortion, fatsOnPortion) || + other.fatsOnPortion == fatsOnPortion) && + (identical(other.carbohydratesOnPortion, carbohydratesOnPortion) || + other.carbohydratesOnPortion == carbohydratesOnPortion)); } @JsonKey(includeFromJson: false, includeToJson: false) @override - int get hashCode => Object.hash(runtimeType, id, name, grInPortion); + int get hashCode => Object.hash( + runtimeType, + id, + name, + grInPortion, + energyOn100Gr, + proteinsOn100Gr, + fatsOn100Gr, + carbohydratesOn100Gr, + energyOnPortion, + proteinsOnPortion, + fatsOnPortion, + carbohydratesOnPortion); /// Create a copy of GlossaryModel /// with the given fields replaced by the non-null parameter values. @@ -355,10 +714,20 @@ class _$GlossaryModelImpl implements _GlossaryModel { abstract class _GlossaryModel implements GlossaryModel { const factory _GlossaryModel( - {required final int id, - required final String name, - @JsonKey(name: 'portion_mass_gr') required final int grInPortion}) = - _$GlossaryModelImpl; + {required final int id, + required final String name, + @JsonKey(name: 'portion_mass_gr') required final int grInPortion, + @JsonKey(name: 'energy_on_100gr') required final double energyOn100Gr, + @JsonKey(name: 'proteins_on_100gr') required final double proteinsOn100Gr, + @JsonKey(name: 'fats_on_100gr') required final double fatsOn100Gr, + @JsonKey(name: 'carbohydrates_on_100gr') + required final double carbohydratesOn100Gr, + @JsonKey(name: 'energy_on_portion') required final double energyOnPortion, + @JsonKey(name: 'proteins_on_portion') + required final double proteinsOnPortion, + @JsonKey(name: 'fats_on_portion') required final double fatsOnPortion, + @JsonKey(name: 'carbohydrates_on_portion') + required final double carbohydratesOnPortion}) = _$GlossaryModelImpl; factory _GlossaryModel.fromJson(Map json) = _$GlossaryModelImpl.fromJson; @@ -370,6 +739,30 @@ abstract class _GlossaryModel implements GlossaryModel { @override @JsonKey(name: 'portion_mass_gr') int get grInPortion; + @override + @JsonKey(name: 'energy_on_100gr') + double get energyOn100Gr; + @override + @JsonKey(name: 'proteins_on_100gr') + double get proteinsOn100Gr; + @override + @JsonKey(name: 'fats_on_100gr') + double get fatsOn100Gr; + @override + @JsonKey(name: 'carbohydrates_on_100gr') + double get carbohydratesOn100Gr; + @override + @JsonKey(name: 'energy_on_portion') + double get energyOnPortion; + @override + @JsonKey(name: 'proteins_on_portion') + double get proteinsOnPortion; + @override + @JsonKey(name: 'fats_on_portion') + double get fatsOnPortion; + @override + @JsonKey(name: 'carbohydrates_on_portion') + double get carbohydratesOnPortion; /// Create a copy of GlossaryModel /// with the given fields replaced by the non-null parameter values. diff --git a/lib/features/glossary/models/glossary_model.g.dart b/lib/features/glossary/models/glossary_model.g.dart index 7fbd6b30..cd3214b4 100644 --- a/lib/features/glossary/models/glossary_model.g.dart +++ b/lib/features/glossary/models/glossary_model.g.dart @@ -20,11 +20,33 @@ Map _$$GlossaryPaginationModelImplToJson( 'data': instance.glossariesList, }; +_$GlossaryModerationModelImpl _$$GlossaryModerationModelImplFromJson( + Map json) => + _$GlossaryModerationModelImpl( + glossary: GlossaryModel.fromJson( + json['product_nutritional_value'] as Map), + ); + +Map _$$GlossaryModerationModelImplToJson( + _$GlossaryModerationModelImpl instance) => + { + 'product_nutritional_value': instance.glossary, + }; + _$GlossaryModelImpl _$$GlossaryModelImplFromJson(Map json) => _$GlossaryModelImpl( id: (json['id'] as num).toInt(), name: json['name'] as String, grInPortion: (json['portion_mass_gr'] as num).toInt(), + energyOn100Gr: (json['energy_on_100gr'] as num).toDouble(), + proteinsOn100Gr: (json['proteins_on_100gr'] as num).toDouble(), + fatsOn100Gr: (json['fats_on_100gr'] as num).toDouble(), + carbohydratesOn100Gr: (json['carbohydrates_on_100gr'] as num).toDouble(), + energyOnPortion: (json['energy_on_portion'] as num).toDouble(), + proteinsOnPortion: (json['proteins_on_portion'] as num).toDouble(), + fatsOnPortion: (json['fats_on_portion'] as num).toDouble(), + carbohydratesOnPortion: + (json['carbohydrates_on_portion'] as num).toDouble(), ); Map _$$GlossaryModelImplToJson(_$GlossaryModelImpl instance) => @@ -32,4 +54,12 @@ Map _$$GlossaryModelImplToJson(_$GlossaryModelImpl instance) => 'id': instance.id, 'name': instance.name, 'portion_mass_gr': instance.grInPortion, + 'energy_on_100gr': instance.energyOn100Gr, + 'proteins_on_100gr': instance.proteinsOn100Gr, + 'fats_on_100gr': instance.fatsOn100Gr, + 'carbohydrates_on_100gr': instance.carbohydratesOn100Gr, + 'energy_on_portion': instance.energyOnPortion, + 'proteins_on_portion': instance.proteinsOnPortion, + 'fats_on_portion': instance.fatsOnPortion, + 'carbohydrates_on_portion': instance.carbohydratesOnPortion, }; diff --git a/lib/features/plans/models/plan_pagination_model.dart b/lib/features/plans/models/plan_pagination_model.dart index 637961e3..dcd0e643 100644 --- a/lib/features/plans/models/plan_pagination_model.dart +++ b/lib/features/plans/models/plan_pagination_model.dart @@ -50,6 +50,8 @@ class PacketOptions with _$PacketOptions { }) = _PacketOptions; factory PacketOptions.fromJson(Map json) => _$PacketOptionsFromJson(json); + + factory PacketOptions.empty() => PacketOptions(items: [], price: -1, short: '', description: ''); } @freezed diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart index ecffb344..215f3a55 100644 --- a/lib/generated/intl/messages_en.dart +++ b/lib/generated/intl/messages_en.dart @@ -26,13 +26,14 @@ class MessageLookup extends MessageLookupByLibrary { "Russia": MessageLookupByLibrary.simpleMessage("Россия"), "about_diet": MessageLookupByLibrary.simpleMessage("Об этой диете"), "active": MessageLookupByLibrary.simpleMessage("Активен"), + "active_diet_state": MessageLookupByLibrary.simpleMessage("Активная"), + "add": MessageLookupByLibrary.simpleMessage("Добавить"), + "add_cover": MessageLookupByLibrary.simpleMessage("Добавьте обложку"), "add_measure": MessageLookupByLibrary.simpleMessage("Добавить замер"), + "adding_plan": MessageLookupByLibrary.simpleMessage("Добавление плана"), "additional_information": MessageLookupByLibrary.simpleMessage("Дополнительная информация"), "admin": MessageLookupByLibrary.simpleMessage("Админ"), - "alko_type": MessageLookupByLibrary.simpleMessage("Алкоголь"), - "alko_type_desc": MessageLookupByLibrary.simpleMessage( - "С некоторыми видами диет можно употреблять алкоголь в умеренных количествах. Рекомендуется совмещать вместе с низкокалорийными закусками."), "all": MessageLookupByLibrary.simpleMessage("Все"), "allergies_presence": MessageLookupByLibrary.simpleMessage("Наличие аллергии"), @@ -47,11 +48,18 @@ class MessageLookup extends MessageLookupByLibrary { "bodymass_chart": MessageLookupByLibrary.simpleMessage("График ИМТ"), "by_default": MessageLookupByLibrary.simpleMessage("По умолчянию"), "calories": MessageLookupByLibrary.simpleMessage("Калории"), + "carb_type": MessageLookupByLibrary.simpleMessage("Мучное"), + "carb_type_desc": MessageLookupByLibrary.simpleMessage( + "Если вы любите мучные продукты, но хотите придерживаться диеты, вам подходит диета с добавленим мучных блюд на основе цельнозерновых вариантов. В них больше сложных углеводов и клетчатки, что способствует длительному чувству сытости и поддерживает здоровье пищеварительной системы."), "carbohydrates": MessageLookupByLibrary.simpleMessage("Углеводы"), "chat": MessageLookupByLibrary.simpleMessage("Чат"), "chest": MessageLookupByLibrary.simpleMessage("Грудь"), "chest_circumference": MessageLookupByLibrary.simpleMessage("Обхват груди"), + "choose_diet_duration": + MessageLookupByLibrary.simpleMessage("Выберите срок диеты"), + "choose_plan_duration": + MessageLookupByLibrary.simpleMessage("Выберите срок плана"), "choose_start_diet_date": MessageLookupByLibrary.simpleMessage( "Выберите дату старты диеты. Она отобразиться у Вас в дневнике"), "circumference_empty": MessageLookupByLibrary.simpleMessage( @@ -69,17 +77,17 @@ class MessageLookup extends MessageLookupByLibrary { "complain": MessageLookupByLibrary.simpleMessage("Пожаловаться"), "continueButton": MessageLookupByLibrary.simpleMessage("Продолжить"), "continue_": MessageLookupByLibrary.simpleMessage("Продолжить"), + "copied": MessageLookupByLibrary.simpleMessage("cкопирован"), "copy_link": MessageLookupByLibrary.simpleMessage("Скопировать ссылку"), - "corb_type": MessageLookupByLibrary.simpleMessage("Мучное"), - "corb_type_desc": MessageLookupByLibrary.simpleMessage( - "Если вы любите мучные продукты, но хотите придерживаться диеты, вам подходит диета с добавленим мучных блюд на основе цельнозерновых вариантов. В них больше сложных углеводов и клетчатки, что способствует длительному чувству сытости и поддерживает здоровье пищеварительной системы."), "country": MessageLookupByLibrary.simpleMessage("Страна"), + "create_diet": MessageLookupByLibrary.simpleMessage("Создание диеты"), "date_of_birth": MessageLookupByLibrary.simpleMessage("Дата рождения"), "day": MessageLookupByLibrary.simpleMessage("День"), "days": MessageLookupByLibrary.simpleMessage("дней"), "diary": MessageLookupByLibrary.simpleMessage("Дневник"), "diet_author": MessageLookupByLibrary.simpleMessage("Автор диеты"), "diet_plan": MessageLookupByLibrary.simpleMessage("План диеты"), + "diet_state": MessageLookupByLibrary.simpleMessage("Статус диеты"), "diet_time": MessageLookupByLibrary.simpleMessage("Время прохождения"), "diet_type": MessageLookupByLibrary.simpleMessage("Тип диеты"), "do_you_have_hypertension_tendency": @@ -91,6 +99,8 @@ class MessageLookup extends MessageLookupByLibrary { "down_pressure": MessageLookupByLibrary.simpleMessage("Нижние показатели давления:"), "draft": MessageLookupByLibrary.simpleMessage("Черновик"), + "eating_behavior": + MessageLookupByLibrary.simpleMessage("Пищевое поведение"), "email": MessageLookupByLibrary.simpleMessage("Элетронная почта"), "email_empty": MessageLookupByLibrary.simpleMessage( "Необходимо ввести адрес электронной почты"), @@ -98,6 +108,10 @@ class MessageLookup extends MessageLookupByLibrary { "Некорректный адрес электронной почты"), "enter_additional_information": MessageLookupByLibrary.simpleMessage( "Введите любую дополнительную информацию на своё усмотрение:"), + "enter_diet_description": + MessageLookupByLibrary.simpleMessage("Введите описание диеты"), + "enter_diet_name": + MessageLookupByLibrary.simpleMessage("Выведите название диеты"), "enter_information": MessageLookupByLibrary.simpleMessage("Введите информацию..."), "exit": MessageLookupByLibrary.simpleMessage("Выйти"), @@ -116,6 +130,10 @@ class MessageLookup extends MessageLookupByLibrary { "Имя обязательно для заполнения"), "first_name_short": MessageLookupByLibrary.simpleMessage( "В имени должен быть минимум 1 символ"), + "fitness": MessageLookupByLibrary.simpleMessage("Поддержка формы"), + "from_a_to_z": MessageLookupByLibrary.simpleMessage("Название: А-Я"), + "from_z_to_a": MessageLookupByLibrary.simpleMessage("Название: Я-А"), + "gallery": MessageLookupByLibrary.simpleMessage("Галерея"), "gr": MessageLookupByLibrary.simpleMessage("гр"), "grams": MessageLookupByLibrary.simpleMessage("гр"), "height_measurements": @@ -125,8 +143,10 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Обхват бедер"), "hips": MessageLookupByLibrary.simpleMessage("Бедра"), "in_progress": MessageLookupByLibrary.simpleMessage("В процессе"), + "inactive_diet_state": MessageLookupByLibrary.simpleMessage("Черновик"), "initial_measurements": MessageLookupByLibrary.simpleMessage("Первичные замеры"), + "label": MessageLookupByLibrary.simpleMessage("Название"), "language": MessageLookupByLibrary.simpleMessage("Язык"), "last_name": MessageLookupByLibrary.simpleMessage("Фамилия"), "last_name_empty": MessageLookupByLibrary.simpleMessage( @@ -134,19 +154,19 @@ class MessageLookup extends MessageLookupByLibrary { "last_name_short": MessageLookupByLibrary.simpleMessage( "В фамилии должен быть минимум 6 символов"), "lbl_english": MessageLookupByLibrary.simpleMessage("English"), + "light_type": MessageLookupByLibrary.simpleMessage("Легкая"), + "light_type_desc": MessageLookupByLibrary.simpleMessage( + "С некоторыми видами диет можно употреблять алкоголь в умеренных количествах. Рекомендуется совмещать вместе с низкокалорийными закусками."), "link_was_copied": MessageLookupByLibrary.simpleMessage("Ссылка успешно скопирована"), "loading": MessageLookupByLibrary.simpleMessage("Загрузка..."), "male": MessageLookupByLibrary.simpleMessage("Мужской"), "meal": MessageLookupByLibrary.simpleMessage("Приём пищи"), + "meat": MessageLookupByLibrary.simpleMessage("Мясо"), "meat_type": MessageLookupByLibrary.simpleMessage("С мясом"), "meat_type_desc": MessageLookupByLibrary.simpleMessage( "Диета с мясом полезная и сбалансированная. Включает в себя нежирное мясо: курицу, индейку, кролика или говядину. В сочетании с овощами получаете все необходимые витамины и минералы. Не забывайте о порциях и старайтесь есть медленно, прислушиваясь к своему организму."), "milliliter": MessageLookupByLibrary.simpleMessage("мл"), - "most_copleted": - MessageLookupByLibrary.simpleMessage("Больше всего пройденных"), - "most_liked": - MessageLookupByLibrary.simpleMessage("Больше всего лайкнутых"), "most_new": MessageLookupByLibrary.simpleMessage("Самые новые"), "most_old": MessageLookupByLibrary.simpleMessage("Самые старые"), "msg_network_err": @@ -162,6 +182,7 @@ class MessageLookup extends MessageLookupByLibrary { "no": MessageLookupByLibrary.simpleMessage("Нет"), "no_account": MessageLookupByLibrary.simpleMessage("У меня нет аккаунта"), + "one_option": MessageLookupByLibrary.simpleMessage("1 опция"), "order": MessageLookupByLibrary.simpleMessage("Порядок"), "password": MessageLookupByLibrary.simpleMessage("Пароль"), "password_confirmation": @@ -173,8 +194,17 @@ class MessageLookup extends MessageLookupByLibrary { "password_short": MessageLookupByLibrary.simpleMessage( "В пароле должно быть минимум 6 символов"), "pcs": MessageLookupByLibrary.simpleMessage("шт"), + "personal_diet_state": + MessageLookupByLibrary.simpleMessage("Персональная"), + "photo": MessageLookupByLibrary.simpleMessage("Фото"), "place_of_living": MessageLookupByLibrary.simpleMessage("Место проживания"), + "plan_desc": MessageLookupByLibrary.simpleMessage("Описание плана"), + "plan_desc_example": MessageLookupByLibrary.simpleMessage( + "Большое подробное, привлекательное описание плана"), + "plan_duration": MessageLookupByLibrary.simpleMessage("Срок плана"), + "plan_label_desc": + MessageLookupByLibrary.simpleMessage("План для поддержания формы"), "plans_complete": MessageLookupByLibrary.simpleMessage("Пройденные планы"), "popular": MessageLookupByLibrary.simpleMessage("Популярные"), @@ -184,15 +214,20 @@ class MessageLookup extends MessageLookupByLibrary { "Все показатели давления обязательны к заполнению"), "pressure_is_not_a_number": MessageLookupByLibrary.simpleMessage( "Все показатели давления должны быть числами"), + "preview": MessageLookupByLibrary.simpleMessage("Превью"), "profile": MessageLookupByLibrary.simpleMessage("Профиль"), "proteins": MessageLookupByLibrary.simpleMessage("Белки"), + "publish": MessageLookupByLibrary.simpleMessage("Опубликовать"), "save": MessageLookupByLibrary.simpleMessage("Сохранить"), "settings": MessageLookupByLibrary.simpleMessage("Настройки"), "share": MessageLookupByLibrary.simpleMessage("Поделиться"), "skip": MessageLookupByLibrary.simpleMessage("Пропустить"), + "slim": MessageLookupByLibrary.simpleMessage("Похудение"), "startDiet": MessageLookupByLibrary.simpleMessage("Начать диету"), "tell_your_schedule": MessageLookupByLibrary.simpleMessage( "Расскажите в свободной форме о том как просыпаетесь, когда едите, когда ложитесь спать:"), + "three_options": MessageLookupByLibrary.simpleMessage("3 опции"), + "to_draft": MessageLookupByLibrary.simpleMessage("В черновик"), "unfavorite_foods": MessageLookupByLibrary.simpleMessage("Нелюбимые продукты"), "up_pressure": MessageLookupByLibrary.simpleMessage( @@ -202,6 +237,7 @@ class MessageLookup extends MessageLookupByLibrary { "user_registration": MessageLookupByLibrary.simpleMessage("Регистрация пользователя"), "username": MessageLookupByLibrary.simpleMessage("Имя пользователя"), + "vegan": MessageLookupByLibrary.simpleMessage("Веган."), "vegan_type": MessageLookupByLibrary.simpleMessage("Вегетарианская"), "vegan_type_desc": MessageLookupByLibrary.simpleMessage( "Диета основана на потреблении растительной пищи и отказе от продуктов животного происхождения. Такой тип питания может быть разнообразным и вкусным, а также полезным для здоровья."), @@ -212,6 +248,8 @@ class MessageLookup extends MessageLookupByLibrary { "weight_chart": MessageLookupByLibrary.simpleMessage("График веса"), "weight_measurements": MessageLookupByLibrary.simpleMessage("Замер веса"), + "wellness": + MessageLookupByLibrary.simpleMessage("Улучшение самочувствия"), "what_is_your_name": MessageLookupByLibrary.simpleMessage("Как вас зовут?"), "wrong_credentials": MessageLookupByLibrary.simpleMessage( diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index 125ec2f1..a0837964 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -1210,11 +1210,11 @@ class S { ); } - /// `Алкоголь` - String get alko_type { + /// `Легкая` + String get light_type { return Intl.message( - 'Алкоголь', - name: 'alko_type', + 'Легкая', + name: 'light_type', desc: '', args: [], ); @@ -1231,10 +1231,10 @@ class S { } /// `Мучное` - String get corb_type { + String get carb_type { return Intl.message( 'Мучное', - name: 'corb_type', + name: 'carb_type', desc: '', args: [], ); @@ -1251,20 +1251,20 @@ class S { } /// `С некоторыми видами диет можно употреблять алкоголь в умеренных количествах. Рекомендуется совмещать вместе с низкокалорийными закусками.` - String get alko_type_desc { + String get light_type_desc { return Intl.message( 'С некоторыми видами диет можно употреблять алкоголь в умеренных количествах. Рекомендуется совмещать вместе с низкокалорийными закусками.', - name: 'alko_type_desc', + name: 'light_type_desc', desc: '', args: [], ); } /// `Если вы любите мучные продукты, но хотите придерживаться диеты, вам подходит диета с добавленим мучных блюд на основе цельнозерновых вариантов. В них больше сложных углеводов и клетчатки, что способствует длительному чувству сытости и поддерживает здоровье пищеварительной системы.` - String get corb_type_desc { + String get carb_type_desc { return Intl.message( 'Если вы любите мучные продукты, но хотите придерживаться диеты, вам подходит диета с добавленим мучных блюд на основе цельнозерновых вариантов. В них больше сложных углеводов и клетчатки, что способствует длительному чувству сытости и поддерживает здоровье пищеварительной системы.', - name: 'corb_type_desc', + name: 'carb_type_desc', desc: '', args: [], ); @@ -1440,21 +1440,21 @@ class S { ); } - /// `Больше всего лайкнутых` - String get most_liked { + /// `Название: А-Я` + String get from_a_to_z { return Intl.message( - 'Больше всего лайкнутых', - name: 'most_liked', + 'Название: А-Я', + name: 'from_a_to_z', desc: '', args: [], ); } - /// `Больше всего пройденных` - String get most_copleted { + /// `Название: Я-А` + String get from_z_to_a { return Intl.message( - 'Больше всего пройденных', - name: 'most_copleted', + 'Название: Я-А', + name: 'from_z_to_a', desc: '', args: [], ); @@ -1469,6 +1469,316 @@ class S { args: [], ); } + + /// `Создание диеты` + String get create_diet { + return Intl.message( + 'Создание диеты', + name: 'create_diet', + desc: '', + args: [], + ); + } + + /// `Выведите название диеты` + String get enter_diet_name { + return Intl.message( + 'Выведите название диеты', + name: 'enter_diet_name', + desc: '', + args: [], + ); + } + + /// `Введите описание диеты` + String get enter_diet_description { + return Intl.message( + 'Введите описание диеты', + name: 'enter_diet_description', + desc: '', + args: [], + ); + } + + /// `Выберите срок диеты` + String get choose_diet_duration { + return Intl.message( + 'Выберите срок диеты', + name: 'choose_diet_duration', + desc: '', + args: [], + ); + } + + /// `Статус диеты` + String get diet_state { + return Intl.message( + 'Статус диеты', + name: 'diet_state', + desc: '', + args: [], + ); + } + + /// `Активная` + String get active_diet_state { + return Intl.message( + 'Активная', + name: 'active_diet_state', + desc: '', + args: [], + ); + } + + /// `Черновик` + String get inactive_diet_state { + return Intl.message( + 'Черновик', + name: 'inactive_diet_state', + desc: '', + args: [], + ); + } + + /// `Персональная` + String get personal_diet_state { + return Intl.message( + 'Персональная', + name: 'personal_diet_state', + desc: '', + args: [], + ); + } + + /// `Мясо` + String get meat { + return Intl.message( + 'Мясо', + name: 'meat', + desc: '', + args: [], + ); + } + + /// `Веган.` + String get vegan { + return Intl.message( + 'Веган.', + name: 'vegan', + desc: '', + args: [], + ); + } + + /// `Фото` + String get photo { + return Intl.message( + 'Фото', + name: 'photo', + desc: '', + args: [], + ); + } + + /// `Галерея` + String get gallery { + return Intl.message( + 'Галерея', + name: 'gallery', + desc: '', + args: [], + ); + } + + /// `Добавьте обложку` + String get add_cover { + return Intl.message( + 'Добавьте обложку', + name: 'add_cover', + desc: '', + args: [], + ); + } + + /// `Добавить` + String get add { + return Intl.message( + 'Добавить', + name: 'add', + desc: '', + args: [], + ); + } + + /// `cкопирован` + String get copied { + return Intl.message( + 'cкопирован', + name: 'copied', + desc: '', + args: [], + ); + } + + /// `В черновик` + String get to_draft { + return Intl.message( + 'В черновик', + name: 'to_draft', + desc: '', + args: [], + ); + } + + /// `Превью` + String get preview { + return Intl.message( + 'Превью', + name: 'preview', + desc: '', + args: [], + ); + } + + /// `Опубликовать` + String get publish { + return Intl.message( + 'Опубликовать', + name: 'publish', + desc: '', + args: [], + ); + } + + /// `Добавление плана` + String get adding_plan { + return Intl.message( + 'Добавление плана', + name: 'adding_plan', + desc: '', + args: [], + ); + } + + /// `Название` + String get label { + return Intl.message( + 'Название', + name: 'label', + desc: '', + args: [], + ); + } + + /// `План для поддержания формы` + String get plan_label_desc { + return Intl.message( + 'План для поддержания формы', + name: 'plan_label_desc', + desc: '', + args: [], + ); + } + + /// `Срок плана` + String get plan_duration { + return Intl.message( + 'Срок плана', + name: 'plan_duration', + desc: '', + args: [], + ); + } + + /// `Похудение` + String get slim { + return Intl.message( + 'Похудение', + name: 'slim', + desc: '', + args: [], + ); + } + + /// `Поддержка формы` + String get fitness { + return Intl.message( + 'Поддержка формы', + name: 'fitness', + desc: '', + args: [], + ); + } + + /// `Улучшение самочувствия` + String get wellness { + return Intl.message( + 'Улучшение самочувствия', + name: 'wellness', + desc: '', + args: [], + ); + } + + /// `Пищевое поведение` + String get eating_behavior { + return Intl.message( + 'Пищевое поведение', + name: 'eating_behavior', + desc: '', + args: [], + ); + } + + /// `Выберите срок плана` + String get choose_plan_duration { + return Intl.message( + 'Выберите срок плана', + name: 'choose_plan_duration', + desc: '', + args: [], + ); + } + + /// `Описание плана` + String get plan_desc { + return Intl.message( + 'Описание плана', + name: 'plan_desc', + desc: '', + args: [], + ); + } + + /// `Большое подробное, привлекательное описание плана` + String get plan_desc_example { + return Intl.message( + 'Большое подробное, привлекательное описание плана', + name: 'plan_desc_example', + desc: '', + args: [], + ); + } + + /// `1 опция` + String get one_option { + return Intl.message( + '1 опция', + name: 'one_option', + desc: '', + args: [], + ); + } + + /// `3 опции` + String get three_options { + return Intl.message( + '3 опции', + name: 'three_options', + desc: '', + args: [], + ); + } } class AppLocalizationDelegate extends LocalizationsDelegate { diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index b29cc1c2..f47a166f 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -115,12 +115,12 @@ "share": "Поделиться", "complain": "Пожаловаться", "vegan_type": "Вегетарианская", - "alko_type": "Алкоголь", + "light_type": "Легкая", "meat_type": "С мясом", - "corb_type": "Мучное", + "carb_type": "Мучное", "vegan_type_desc": "Диета основана на потреблении растительной пищи и отказе от продуктов животного происхождения. Такой тип питания может быть разнообразным и вкусным, а также полезным для здоровья.", - "alko_type_desc": "С некоторыми видами диет можно употреблять алкоголь в умеренных количествах. Рекомендуется совмещать вместе с низкокалорийными закусками.", - "corb_type_desc": "Если вы любите мучные продукты, но хотите придерживаться диеты, вам подходит диета с добавленим мучных блюд на основе цельнозерновых вариантов. В них больше сложных углеводов и клетчатки, что способствует длительному чувству сытости и поддерживает здоровье пищеварительной системы.", + "light_type_desc": "С некоторыми видами диет можно употреблять алкоголь в умеренных количествах. Рекомендуется совмещать вместе с низкокалорийными закусками.", + "carb_type_desc": "Если вы любите мучные продукты, но хотите придерживаться диеты, вам подходит диета с добавленим мучных блюд на основе цельнозерновых вариантов. В них больше сложных углеводов и клетчатки, что способствует длительному чувству сытости и поддерживает здоровье пищеварительной системы.", "meat_type_desc": "Диета с мясом полезная и сбалансированная. Включает в себя нежирное мясо: курицу, индейку, кролика или говядину. В сочетании с овощами получаете все необходимые витамины и минералы. Не забывайте о порциях и старайтесь есть медленно, прислушиваясь к своему организму.", "diet_plan": "План диеты", "calories": "Калории", @@ -138,7 +138,39 @@ "popular": "Популярные", "most_new": "Самые новые", "most_old": "Самые старые", - "most_liked": "Больше всего лайкнутых", - "most_copleted": "Больше всего пройденных", - "by_default": "По умолчянию" + "from_a_to_z": "Название: А-Я", + "from_z_to_a": "Название: Я-А", + "by_default": "По умолчянию", + "create_diet": "Создание диеты", + "enter_diet_name": "Выведите название диеты", + "enter_diet_description": "Введите описание диеты", + "choose_diet_duration": "Выберите срок диеты", + "diet_state": "Статус диеты", + "active_diet_state": "Активная", + "inactive_diet_state": "Черновик", + "personal_diet_state": "Персональная", + "meat": "Мясо", + "vegan": "Веган.", + "photo": "Фото", + "gallery": "Галерея", + "add_cover": "Добавьте обложку", + "add": "Добавить", + "copied": "cкопирован", + "to_draft": "В черновик", + "preview": "Превью", + "publish": "Опубликовать", + "adding_plan": "Добавление плана", + "label": "Название", + "plan_label_desc": "План для поддержания формы", + "plan_duration": "Срок плана", + "slim": "Похудение", + "fitness": "Поддержка формы", + "wellness": "Улучшение самочувствия", + "eating_behavior": "Пищевое поведение", + "choose_plan_duration": "Выберите срок плана", + "plan_desc": "Описание плана", + "plan_desc_example": "Большое подробное, привлекательное описание плана", + "one_option": "1 опция", + "three_options": "3 опции" + } diff --git a/pubspec.yaml b/pubspec.yaml index 5c121e67..6a455e12 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -57,6 +57,7 @@ dependencies: flutter_image_compress: ^2.3.0 image: ^4.3.0 flutter_native_splash: ^2.4.3 + dotted_border: ^2.1.0 dev_dependencies: build_runner: ^2.4.13 diff --git a/test/auth_test.dart b/test/auth_test.dart index 5058e6aa..626a1ce1 100644 --- a/test/auth_test.dart +++ b/test/auth_test.dart @@ -7,6 +7,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_test/flutter_test.dart'; + void main() { group('AuthScreen', () { testWidgets('renders AuthScreen with initial state', (WidgetTester tester) async {