From 5ef1f3a660a348994cfbba508d7f7c34ed2ba982 Mon Sep 17 00:00:00 2001 From: Username1548 Date: Mon, 23 Dec 2024 15:36:00 +0700 Subject: [PATCH] my plans and my diets --- lib/core/app_router.dart | 16 +- lib/core/locator_service.dart | 12 + lib/core/theme/custom_text_style.dart | 2 +- lib/core/widgets/add_item_button.dart | 30 ++ lib/core/widgets/animated_option_button.dart | 13 +- lib/core/widgets/bottom_sheet_with_blur.dart | 45 ++ lib/core/widgets/custom_outlined_button.dart | 2 +- lib/core/widgets/custom_search_bar.dart | 59 +++ .../{diet_info_row.dart => info_row.dart} | 0 .../presentation/create_diet_screen.dart | 243 ++++------ .../presentation/diet_preview_screen.dart | 2 +- .../presentation/cubit/about_diet_cubit.dart | 36 +- .../presentation/pages/about_diet_screen.dart | 9 +- .../presentation/widgets/expandable_text.dart | 4 +- .../presentation/widgets/info_bouble.dart | 5 +- .../feed/models/plan_pagination_model.dart | 11 +- .../models/plan_pagination_model.freezed.dart | 110 +++-- .../feed/models/plan_pagination_model.g.dart | 6 +- .../bloc/diets_cubit/diets_cubit.dart | 11 +- .../feed/presentation/pages/feed_screen.dart | 186 +++----- .../widgets/diet_filter_bottom_sheet.dart | 11 +- .../presentation/widgets/diets_widget.dart | 2 +- .../duration_choice_filter_widget.dart | 170 +++---- .../presentation/widgets/plan_widget.dart | 60 ++- .../widgets/plans_filter_bottom_sheet.dart | 10 +- .../widgets/price_choice_filter_widget.dart | 179 +++++++- .../widgets/sorting_drop_down_filter.dart | 46 +- .../widgets/type_choice_filter_widget.dart | 192 ++++---- .../menu/presentation/pages/menu_screen.dart | 6 +- .../presentation/cubit/my_diets_cubit.dart | 250 +++++++++++ .../presentation/cubit/my_diets_state.dart | 41 ++ .../cubit/my_diets_state.freezed.dart | 400 +++++++++++++++++ .../presentation/my_diets_screen.dart | 194 ++++++++ .../presentation/widgets/my_diet_widget.dart | 252 +++++++++++ .../presentation/widgets/my_diets_tab.dart | 74 +++ .../repositories/my_diets_repository.dart | 36 ++ .../my_diets/services/my_diets_service.dart | 27 ++ .../my_diets/services/my_diets_service.g.dart | 124 +++++ .../presentation/cubit/my_plans_cubit.dart | 207 +++++++++ .../presentation/cubit/my_plans_state.dart | 36 ++ .../cubit/my_plans_state.freezed.dart | 424 ++++++++++++++++++ .../presentation/my_plans_screen.dart | 189 ++++++++ .../presentation/widgets/my_plan_widget.dart | 259 +++++++++++ .../presentation/widgets/my_plans_tab.dart | 71 +++ .../repositories/my_diets_repository.dart | 39 ++ .../my_plans/services/my_plans_service.dart | 28 ++ .../my_plans/services/my_plans_service.g.dart | 126 ++++++ .../presentation/cubit/about_plan_cubit.dart | 34 +- .../presentation/pages/about_plan_screen.dart | 4 +- lib/generated/intl/messages_en.dart | 12 + lib/generated/l10n.dart | 110 +++++ lib/l10n/intl_en.arb | 15 +- 52 files changed, 3857 insertions(+), 573 deletions(-) create mode 100644 lib/core/widgets/add_item_button.dart create mode 100644 lib/core/widgets/bottom_sheet_with_blur.dart create mode 100644 lib/core/widgets/custom_search_bar.dart rename lib/core/widgets/{diet_info_row.dart => info_row.dart} (100%) create mode 100644 lib/features/my_diets/presentation/cubit/my_diets_cubit.dart create mode 100644 lib/features/my_diets/presentation/cubit/my_diets_state.dart create mode 100644 lib/features/my_diets/presentation/cubit/my_diets_state.freezed.dart create mode 100644 lib/features/my_diets/presentation/my_diets_screen.dart create mode 100644 lib/features/my_diets/presentation/widgets/my_diet_widget.dart create mode 100644 lib/features/my_diets/presentation/widgets/my_diets_tab.dart create mode 100644 lib/features/my_diets/repositories/my_diets_repository.dart create mode 100644 lib/features/my_diets/services/my_diets_service.dart create mode 100644 lib/features/my_diets/services/my_diets_service.g.dart create mode 100644 lib/features/my_plans/presentation/cubit/my_plans_cubit.dart create mode 100644 lib/features/my_plans/presentation/cubit/my_plans_state.dart create mode 100644 lib/features/my_plans/presentation/cubit/my_plans_state.freezed.dart create mode 100644 lib/features/my_plans/presentation/my_plans_screen.dart create mode 100644 lib/features/my_plans/presentation/widgets/my_plan_widget.dart create mode 100644 lib/features/my_plans/presentation/widgets/my_plans_tab.dart create mode 100644 lib/features/my_plans/repositories/my_diets_repository.dart create mode 100644 lib/features/my_plans/services/my_plans_service.dart create mode 100644 lib/features/my_plans/services/my_plans_service.g.dart diff --git a/lib/core/app_router.dart b/lib/core/app_router.dart index f939208a..b41077d8 100644 --- a/lib/core/app_router.dart +++ b/lib/core/app_router.dart @@ -22,6 +22,8 @@ import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart'; import 'package:eatvio_app/features/feed/models/recipes_model.dart'; import 'package:eatvio_app/features/feed/presentation/pages/feed_screen.dart'; import 'package:eatvio_app/features/menu/presentation/pages/menu_screen.dart'; +import 'package:eatvio_app/features/my_diets/presentation/my_diets_screen.dart'; +import 'package:eatvio_app/features/my_plans/presentation/my_plans_screen.dart'; import 'package:eatvio_app/features/notifications/presentation/pages/notifications_screen.dart'; import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart'; import 'package:eatvio_app/features/plans/presentation/pages/about_plan_screen.dart'; @@ -116,7 +118,7 @@ class AppRouter { final data = state.extra as Map; return AboutPlanScreen( plan: data['plan'] as PlanModel, - index: data['index'] as int, + index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool, ); }, @@ -162,7 +164,7 @@ class AppRouter { final data = state.extra as Map; return AboutDietScreen( diet: data['diet'] as Diet, - index: data['index'] as int, + index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool, ); }, @@ -290,6 +292,16 @@ class AppRouter { name: CreatePlanScreen.name, builder: (context, state) => CreatePlanScreen(), ), + GoRoute( + path: MyDietsScreen.path, + name: MyDietsScreen.name, + builder: (context, state) => MyDietsScreen(), + ), + GoRoute( + path: MyPlansScreen.path, + name: MyPlansScreen.name, + builder: (context, state) => MyPlansScreen(), + ), ], ); } diff --git a/lib/core/locator_service.dart b/lib/core/locator_service.dart index 4b2106f8..4a6bc28a 100644 --- a/lib/core/locator_service.dart +++ b/lib/core/locator_service.dart @@ -37,6 +37,12 @@ import 'package:eatvio_app/features/feed/services/feed_service.dart'; import 'package:eatvio_app/features/glossary/repositories/glossary_repository.dart'; import 'package:eatvio_app/features/glossary/services/glossary_service.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_cubit.dart'; +import 'package:eatvio_app/features/my_diets/presentation/cubit/my_diets_cubit.dart'; +import 'package:eatvio_app/features/my_diets/repositories/my_diets_repository.dart'; +import 'package:eatvio_app/features/my_diets/services/my_diets_service.dart'; +import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_cubit.dart'; +import 'package:eatvio_app/features/my_plans/repositories/my_diets_repository.dart'; +import 'package:eatvio_app/features/my_plans/services/my_plans_service.dart'; import 'package:eatvio_app/features/plans/repositories/plans_repository.dart'; import 'package:eatvio_app/features/plans/services/plans_service.dart'; import 'package:eatvio_app/features/plans/presentation/cubit/about_plan_cubit.dart'; @@ -90,6 +96,8 @@ Future init() async { sl.registerFactory(() => CreatePlanCubit()); sl.registerFactory(() => FeedProfileCubit(profileRepository: sl())); sl.registerFactory(() => AboutPlanCubit(planRepository: sl())); + sl.registerFactory(() => MyDietsCubit(myDietsRepository: sl())); + sl.registerFactory(() => MyPlansCubit(myPlansRepository: sl())); // Repository sl.registerLazySingleton(() => ChatRepository(chatService: sl())); @@ -107,6 +115,8 @@ Future init() async { sl.registerLazySingleton(() => CreateDietRepository(createDietService: sl())); sl.registerLazySingleton(() => PlansRepository(plansService: sl())); sl.registerLazySingleton(() => PlanRepository(planService: sl())); + sl.registerLazySingleton(() => MyDietsRepository(myDietsService: sl())); + sl.registerLazySingleton(() => MyPlansRepository(myPlansService: sl())); // Services sl.registerLazySingleton(() => ChatService(sl())); @@ -124,6 +134,8 @@ Future init() async { sl.registerLazySingleton(() => CreateDietService(sl())); sl.registerLazySingleton(() => PlansService(sl())); sl.registerLazySingleton(() => PlanService(sl())); + sl.registerLazySingleton(() => MyDietsService(sl())); + sl.registerLazySingleton(() => MyPlansService(sl())); // External sl.registerLazySingleton( diff --git a/lib/core/theme/custom_text_style.dart b/lib/core/theme/custom_text_style.dart index 7f00ad9b..a4e2539d 100644 --- a/lib/core/theme/custom_text_style.dart +++ b/lib/core/theme/custom_text_style.dart @@ -224,7 +224,7 @@ abstract class TextStyles { fontSize: 12, fontWeight: FontWeight.w500, height: 1.255, - ); + ); static const textFS10FW500 = TextStyle( fontSize: 12, diff --git a/lib/core/widgets/add_item_button.dart b/lib/core/widgets/add_item_button.dart new file mode 100644 index 00000000..4a0f9816 --- /dev/null +++ b/lib/core/widgets/add_item_button.dart @@ -0,0 +1,30 @@ +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +class AddItemButton extends StatelessWidget { + const AddItemButton({super.key, this.onTap}); + final Function()? onTap; + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap ?? + () { + GoRouter.of(context).pushNamed('blogPostCreateScreen'); + }, + borderRadius: BorderRadius.circular(64), + child: Ink( + decoration: BoxDecoration( + color: appTheme.blueA400, + shape: BoxShape.circle, + ), + child: Icon( + Icons.add_rounded, + color: appTheme.gray100, + ), + width: 40, + height: 40, + ), + ); + } +} \ No newline at end of file diff --git a/lib/core/widgets/animated_option_button.dart b/lib/core/widgets/animated_option_button.dart index f196c042..4491293b 100644 --- a/lib/core/widgets/animated_option_button.dart +++ b/lib/core/widgets/animated_option_button.dart @@ -3,11 +3,11 @@ import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:flutter/material.dart'; class AnimatedOptionButton extends StatelessWidget { - const AnimatedOptionButton({required this.text, required this.isSelected, required this.onTap}); + const AnimatedOptionButton({required this.text, required this.isSelected, required this.onTap, this.textStyle}); final String text; final bool isSelected; final void Function() onTap; - + final TextStyle? textStyle; @override Widget build(BuildContext context) { return GestureDetector( @@ -22,9 +22,12 @@ class AnimatedOptionButton extends StatelessWidget { child: Center( child: AnimatedDefaultTextStyle( duration: const Duration(milliseconds: 300), - style: TextStyles.textMSemibold.copyWith( - color: isSelected ? appTheme.whiteA700 : appTheme.black, - ), + style: textStyle?.copyWith( + color: isSelected ? appTheme.whiteA700 : appTheme.black, + ) ?? + TextStyles.textMSemibold.copyWith( + color: isSelected ? appTheme.whiteA700 : appTheme.black, + ), child: Text(text), ), ), diff --git a/lib/core/widgets/bottom_sheet_with_blur.dart b/lib/core/widgets/bottom_sheet_with_blur.dart new file mode 100644 index 00000000..303170c4 --- /dev/null +++ b/lib/core/widgets/bottom_sheet_with_blur.dart @@ -0,0 +1,45 @@ +import 'dart:ui'; + +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:flutter/material.dart'; + +Future showBottomSheetWithBlur({ + required BuildContext context, + required Widget child, + double height = 200, + bool hasBlur = true, + bool enableDrag = true, + }) { + return showModalBottomSheet( + context: context, + isScrollControlled: true, + barrierColor: appTheme.transparentBlack, + backgroundColor: Colors.transparent, + elevation: 0, + useSafeArea: true, + enableDrag: enableDrag, + builder: (context) => GestureDetector( + behavior: HitTestBehavior.opaque, + child: BackdropFilter( + filter: ImageFilter.blur( + sigmaX: hasBlur ? 24 : 0, + sigmaY: hasBlur ? 24 : 0, + tileMode: TileMode.mirror, + ), + child: SizedBox( + child: Container( + height: height, + decoration: BoxDecoration( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ), + color: appTheme.whiteA700, + ), + child: child, + ), + ), + ), + ), + ); + } \ No newline at end of file diff --git a/lib/core/widgets/custom_outlined_button.dart b/lib/core/widgets/custom_outlined_button.dart index 38a9be27..e8692a87 100644 --- a/lib/core/widgets/custom_outlined_button.dart +++ b/lib/core/widgets/custom_outlined_button.dart @@ -69,7 +69,7 @@ class CustomOutlinedButton extends BaseButton { width: this.width ?? double.maxFinite, margin: margin, decoration: decoration, - child: OutlinedButton( + child: OutlinedButton( style: buttonStyle!, onPressed: isDisabled ?? false ? null : onPressed ?? () {}, child: Row( diff --git a/lib/core/widgets/custom_search_bar.dart b/lib/core/widgets/custom_search_bar.dart new file mode 100644 index 00000000..94b3c5c4 --- /dev/null +++ b/lib/core/widgets/custom_search_bar.dart @@ -0,0 +1,59 @@ +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:flutter/material.dart'; + +class CustomSearchBar extends StatelessWidget { + const CustomSearchBar({super.key, required this.searchController, required this.onChanged, this.backgroundColor, this.borderRadius, this.enableIcon = true, this.width}); + final Color? backgroundColor; + final double? width; + final double? borderRadius; + final TextEditingController searchController; + final void Function(String) onChanged; + final bool enableIcon; + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 40, + width: width ?? MediaQuery.of(context).size.width - 84, + child: TextFormField( + controller: searchController, + autofocus: false, + style: TextStyle( + fontSize: 12, + ), + onChanged: onChanged, + decoration: InputDecoration( + hintText: 'Поиск', + hintStyle: TextStyle( + fontSize: 12, + color: appTheme.gray600, + fontWeight: FontWeight.w400, + ), + prefixIcon: enableIcon + ? Icon( + Icons.search, + color: appTheme.gray600, + size: 16, + ) + : null, + isDense: false, + contentPadding: enableIcon ? EdgeInsets.all(8) : EdgeInsets.only(top: 8, left: 16, right: 8, bottom: 8), + fillColor: backgroundColor ?? appTheme.gray100, + filled: true, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(borderRadius ?? 16), + borderSide: BorderSide.none, + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(borderRadius ?? 16), + borderSide: BorderSide.none, + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(borderRadius ?? 16), + borderSide: BorderSide.none, + ), + ), + ), + ); + } +} diff --git a/lib/core/widgets/diet_info_row.dart b/lib/core/widgets/info_row.dart similarity index 100% rename from lib/core/widgets/diet_info_row.dart rename to lib/core/widgets/info_row.dart diff --git a/lib/features/create_diet/presentation/create_diet_screen.dart b/lib/features/create_diet/presentation/create_diet_screen.dart index a4425e10..49b23577 100644 --- a/lib/features/create_diet/presentation/create_diet_screen.dart +++ b/lib/features/create_diet/presentation/create_diet_screen.dart @@ -1,10 +1,9 @@ -import 'dart:ui'; - 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/bottom_sheet_handler_bar.dart'; +import 'package:eatvio_app/core/widgets/bottom_sheet_with_blur.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'; @@ -47,17 +46,92 @@ class CreateDietScreenContent extends StatelessWidget { if (state.published) { WidgetsBinding.instance.addPostFrameCallback((_) { context.pop(); - showPublishSuccessBottomSheet( + showBottomSheetWithBlur( context: context, - text: '${state.dietData.name ?? 'Диета'}', + child: Column( + children: [ + SizedBox( + height: 20, + ), + BottomSheetHandlerBar(), + SizedBox( + height: 20, + ), + Text( + '${state.dietData.name ?? 'Диета'}', + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), + ), + Text( + 'опубликованна', + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), + ), + SizedBox( + height: 20, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: CustomOutlinedButton( + onPressed: () { + context.pop(); + }, + text: 'Хорошо', + height: 52, + buttonTextStyle: TextStyles.textSSemibold.copyWith(color: appTheme.whiteA700), + buttonStyle: ButtonStyle( + side: WidgetStatePropertyAll(BorderSide.none), + backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), + ), + ), + ), + SizedBox( + height: 20, + ), + ], + ), ); }); } else if (state.drafted) { WidgetsBinding.instance.addPostFrameCallback((_) { context.pop(); - showDraftSuccessBottomSheet( + showBottomSheetWithBlur( context: context, - text: '${state.dietData.name ?? 'Диета'} сохраненна в черновиках', + child: Column( + children: [ + SizedBox( + height: 20, + ), + BottomSheetHandlerBar(), + SizedBox( + height: 20, + ), + Text( + '${state.dietData.name ?? 'Диета'} сохраненна в черновиках', + textAlign: TextAlign.center, + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), + ), + SizedBox( + height: 20, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: CustomOutlinedButton( + onPressed: () { + context.pop(); + }, + text: 'Хорошо', + height: 52, + buttonTextStyle: TextStyles.textSSemibold.copyWith(color: appTheme.whiteA700), + buttonStyle: ButtonStyle( + side: WidgetStatePropertyAll(BorderSide.none), + backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), + ), + ), + ), + SizedBox( + height: 20, + ), + ], + ), ); }); } @@ -208,160 +282,5 @@ class CreateDietScreenContent extends StatelessWidget { ); }, ); - } - - Future showDraftSuccessBottomSheet({ - required BuildContext context, - required String text, - bool hasBlur = true, - bool enableDrag = true, - }) { - return showModalBottomSheet( - context: context, - isScrollControlled: true, - barrierColor: appTheme.transparentBlack, - backgroundColor: Colors.transparent, - elevation: 0, - useSafeArea: true, - enableDrag: enableDrag, - builder: (context) => GestureDetector( - behavior: HitTestBehavior.opaque, - child: BackdropFilter( - filter: ImageFilter.blur( - sigmaX: hasBlur ? 24 : 0, - sigmaY: hasBlur ? 24 : 0, - tileMode: TileMode.mirror, - ), - child: SizedBox( - child: Container( - height: 200, - decoration: BoxDecoration( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(20), - topRight: Radius.circular(20), - ), - color: appTheme.whiteA700, - ), - child: Column( - children: [ - SizedBox( - height: 20, - ), - BottomSheetHandlerBar(), - SizedBox( - height: 20, - ), - Text( - text, - textAlign: TextAlign.center, - style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), - ), - SizedBox( - height: 20, - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: CustomOutlinedButton( - onPressed: () { - context.pop(); - }, - text: 'Хорошо', - height: 52, - buttonTextStyle: TextStyles.textSSemibold.copyWith(color: appTheme.whiteA700), - buttonStyle: ButtonStyle( - side: WidgetStatePropertyAll(BorderSide.none), - backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), - ), - ), - ), - SizedBox( - height: 20, - ), - ], - ), - ), - ), - ), - ), - ); - } - - Future showPublishSuccessBottomSheet({ - required BuildContext context, - required String text, - bool hasBlur = true, - bool enableDrag = true, - }) { - return showModalBottomSheet( - context: context, - isScrollControlled: true, - barrierColor: appTheme.transparentBlack, - backgroundColor: Colors.transparent, - elevation: 0, - useSafeArea: true, - enableDrag: enableDrag, - builder: (context) => GestureDetector( - behavior: HitTestBehavior.opaque, - child: BackdropFilter( - filter: ImageFilter.blur( - sigmaX: hasBlur ? 24 : 0, - sigmaY: hasBlur ? 24 : 0, - tileMode: TileMode.mirror, - ), - child: SizedBox( - child: Container( - height: 200, - decoration: BoxDecoration( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(20), - topRight: Radius.circular(20), - ), - color: appTheme.whiteA700, - ), - child: Column( - children: [ - SizedBox( - height: 20, - ), - BottomSheetHandlerBar(), - SizedBox( - height: 20, - ), - Text( - text, - style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), - ), - Text( - 'опубликованна', - style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), - ), - SizedBox( - height: 20, - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: CustomOutlinedButton( - onPressed: () { - context.pop(); - }, - text: 'Хорошо', - height: 52, - buttonTextStyle: TextStyles.textSSemibold.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/diet_preview_screen.dart b/lib/features/create_diet/presentation/diet_preview_screen.dart index f06c247e..3c26b953 100644 --- a/lib/features/create_diet/presentation/diet_preview_screen.dart +++ b/lib/features/create_diet/presentation/diet_preview_screen.dart @@ -5,7 +5,7 @@ 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/core/widgets/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'; diff --git a/lib/features/diets/presentation/cubit/about_diet_cubit.dart b/lib/features/diets/presentation/cubit/about_diet_cubit.dart index 2e31c681..54215fe9 100644 --- a/lib/features/diets/presentation/cubit/about_diet_cubit.dart +++ b/lib/features/diets/presentation/cubit/about_diet_cubit.dart @@ -32,7 +32,7 @@ class AboutDietCubit extends Cubit { } } - Future likeDiet(int id, int dietIndexInFeed) async { + Future likeDiet(int id, int? dietIndexInFeed) async { if (state is! DietStateLoaded) return; final currentDiet = (state as DietStateLoaded).diet; @@ -42,15 +42,18 @@ class AboutDietCubit extends Cubit { totalLikes: currentDiet.totalLikes - 1, wasLikedByMe: false, ); - dietsRepository.emitLikesStateToFeed(LikesState(wasLikedByMe: updatedDiet.wasLikedByMe, totalLikes: updatedDiet.totalLikes, index: dietIndexInFeed)); + if (dietIndexInFeed != null) { + dietsRepository.emitLikesStateToFeed(LikesState(wasLikedByMe: updatedDiet.wasLikedByMe, totalLikes: updatedDiet.totalLikes, index: dietIndexInFeed)); + } emit(AboutDietState.loaded(updatedDiet)); } else { final updatedDiet = currentDiet.copyWith( totalLikes: currentDiet.totalLikes + 1, wasLikedByMe: true, ); - - dietsRepository.emitLikesStateToFeed(LikesState(wasLikedByMe: updatedDiet.wasLikedByMe, totalLikes: updatedDiet.totalLikes, index: dietIndexInFeed)); + if (dietIndexInFeed != null) { + dietsRepository.emitLikesStateToFeed(LikesState(wasLikedByMe: updatedDiet.wasLikedByMe, totalLikes: updatedDiet.totalLikes, index: dietIndexInFeed)); + } emit(AboutDietState.loaded(updatedDiet)); } @@ -62,14 +65,17 @@ class AboutDietCubit extends Cubit { await dietsRepository.likeDiet(id); } catch (e) { log(e.toString()); - dietsRepository.emitLikesStateToFeed(LikesState(wasLikedByMe: currentDiet.wasLikedByMe, totalLikes: currentDiet.totalLikes, index: dietIndexInFeed)); + if (dietIndexInFeed != null) { + dietsRepository + .emitLikesStateToFeed(LikesState(wasLikedByMe: currentDiet.wasLikedByMe, totalLikes: currentDiet.totalLikes, index: dietIndexInFeed)); + } emit(AboutDietState.loaded(currentDiet)); } }, ); } - Future addToFavorite(int id, int dietIndex) async { + Future addToFavorite(int id, int? dietIndex) async { if (state is! DietStateLoaded) return; final currentDiet = (state as DietStateLoaded).diet; @@ -77,7 +83,9 @@ class AboutDietCubit extends Cubit { final updatedDiet = currentDiet.copyWith( isFavorite: true, ); - dietsRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: updatedDiet.isFavorite, index: dietIndex)); + if (dietIndex != null) { + dietsRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: updatedDiet.isFavorite, index: dietIndex)); + } emit(AboutDietState.loaded(updatedDiet)); EasyDebounce.debounce( @@ -89,14 +97,16 @@ class AboutDietCubit extends Cubit { } catch (e) { log(e.toString()); showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время'); - dietsRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: currentDiet.isFavorite, index: dietIndex)); + if (dietIndex != null) { + dietsRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: currentDiet.isFavorite, index: dietIndex)); + } emit(AboutDietState.loaded(currentDiet)); } }, ); } - Future removeFromFavorite(int id, int dietIndex) async { + Future removeFromFavorite(int id, int? dietIndex) async { if (state is! DietStateLoaded) return; final currentDiet = (state as DietStateLoaded).diet; @@ -104,7 +114,9 @@ class AboutDietCubit extends Cubit { final updatedDiet = currentDiet.copyWith( isFavorite: false, ); - dietsRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: updatedDiet.isFavorite, index: dietIndex)); + if (dietIndex != null) { + dietsRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: updatedDiet.isFavorite, index: dietIndex)); + } emit(AboutDietState.loaded(updatedDiet)); EasyDebounce.debounce( @@ -116,7 +128,9 @@ class AboutDietCubit extends Cubit { } catch (e) { log(e.toString()); showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время'); - dietsRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: currentDiet.isFavorite, index: dietIndex)); + if (dietIndex != null) { + dietsRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: currentDiet.isFavorite, index: dietIndex)); + } 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 8e84e933..24c9fa60 100644 --- a/lib/features/diets/presentation/pages/about_diet_screen.dart +++ b/lib/features/diets/presentation/pages/about_diet_screen.dart @@ -6,7 +6,7 @@ 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/core/widgets/info_row.dart'; import 'package:eatvio_app/core/widgets/loader.dart'; import 'package:eatvio_app/features/diets/presentation/cubit/about_diet_cubit.dart'; import 'package:eatvio_app/features/diets/presentation/cubit/about_diet_state.dart'; @@ -32,7 +32,7 @@ import 'package:flutter_svg/svg.dart'; class AboutDietScreen extends StatefulWidget { final Diet diet; - final int index; + final int? index; final bool navigateToComments; late final List selectedIndexes; @@ -132,10 +132,11 @@ class _AboutDietScreenState extends State { ), backgroundColor: appTheme.greenA700, onPressed: () { + final cubit = context.read(); showModalBottomSheet( context: context, builder: (context) => StartDietBottomSheet( - aboutDietCubit: context.read(), + aboutDietCubit: cubit, ), ); }, @@ -150,7 +151,7 @@ class _AboutDietScreenState extends State { BookmarkButton( isFavorite: dietState.diet.isFavorite, onTap: () { - if (dietState.diet.isFavorite) { + if (dietState.diet.isFavorite) { dietsCubit.removeFromFavorite(widget.diet.id, widget.index); } else { dietsCubit.addToFavorite(widget.diet.id, widget.index); diff --git a/lib/features/diets/presentation/widgets/expandable_text.dart b/lib/features/diets/presentation/widgets/expandable_text.dart index 44a16b0c..ab490df4 100644 --- a/lib/features/diets/presentation/widgets/expandable_text.dart +++ b/lib/features/diets/presentation/widgets/expandable_text.dart @@ -57,8 +57,8 @@ class _ExpandableTextState extends State { begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ - Colors.white.withOpacity(0.0), - Colors.white.withOpacity(0.6), + Colors.white.withValues(alpha: 0.0), + Colors.white.withValues(alpha: 0.6), Colors.white, ], ), diff --git a/lib/features/diets/presentation/widgets/info_bouble.dart b/lib/features/diets/presentation/widgets/info_bouble.dart index 7f339420..fc27b4b2 100644 --- a/lib/features/diets/presentation/widgets/info_bouble.dart +++ b/lib/features/diets/presentation/widgets/info_bouble.dart @@ -2,14 +2,15 @@ import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:flutter/material.dart'; class InfoBouble extends StatelessWidget { - const InfoBouble({super.key, required this.children, this.backgroundColor, this.radius = 100}); + const InfoBouble({super.key, required this.children, this.backgroundColor, this.radius = 100, this.padding}); final List children; final Color? backgroundColor; final double radius; + final EdgeInsetsGeometry? padding; @override Widget build(BuildContext context) { return Container( - padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8.5), + padding: padding ?? EdgeInsets.symmetric(horizontal: 12, vertical: 8.5), decoration: BoxDecoration( color: backgroundColor ?? appTheme.gray50, borderRadius: BorderRadius.circular(radius), diff --git a/lib/features/feed/models/plan_pagination_model.dart b/lib/features/feed/models/plan_pagination_model.dart index 6d8aa4ad..083bb162 100644 --- a/lib/features/feed/models/plan_pagination_model.dart +++ b/lib/features/feed/models/plan_pagination_model.dart @@ -26,7 +26,7 @@ class PlanModel with _$PlanModel { @JsonKey(name: "image") required String image, @JsonKey(name: "duration") required int duration, @JsonKey(name: "state") required String state, - @JsonKey(name: "type") required PlanType type, + @JsonKey(name: "type", fromJson: planTypeFromJson) required PlanType type, @JsonKey(name: "details") required String details, @JsonKey(name: "packets_count") required int packetsCount, @JsonKey(name: "packet1_options") PacketOptions? packet1Options, @@ -37,12 +37,21 @@ class PlanModel with _$PlanModel { @JsonKey(name: "total_views") required int totalViews, @JsonKey(name: "total_comments") required int totalComments, @JsonKey(name: "is_favorite") required bool isFavorite, + @JsonKey(name: "min_price_of_options") required double minPriceOfOptions, + @JsonKey(name: "max_price_of_options") required double maxPriceOfOptions, @Default(false) bool wasLikedByMe, }) = _PlanModel; factory PlanModel.fromJson(Map json) => _$PlanModelFromJson(json); } +PlanType planTypeFromJson(dynamic json) { + if (json is List) { + return PlanType(fit: false, slim: false, behavior: false, wellness: false); + } + return PlanType.fromJson(json); +} + @freezed class PacketOptions with _$PacketOptions { const factory PacketOptions({ diff --git a/lib/features/feed/models/plan_pagination_model.freezed.dart b/lib/features/feed/models/plan_pagination_model.freezed.dart index 16fa0ec3..0a338d10 100644 --- a/lib/features/feed/models/plan_pagination_model.freezed.dart +++ b/lib/features/feed/models/plan_pagination_model.freezed.dart @@ -272,7 +272,7 @@ mixin _$PlanModel { int get duration => throw _privateConstructorUsedError; @JsonKey(name: "state") String get state => throw _privateConstructorUsedError; - @JsonKey(name: "type") + @JsonKey(name: "type", fromJson: planTypeFromJson) PlanType get type => throw _privateConstructorUsedError; @JsonKey(name: "details") String get details => throw _privateConstructorUsedError; @@ -294,6 +294,10 @@ mixin _$PlanModel { int get totalComments => throw _privateConstructorUsedError; @JsonKey(name: "is_favorite") bool get isFavorite => throw _privateConstructorUsedError; + @JsonKey(name: "min_price_of_options") + double get minPriceOfOptions => throw _privateConstructorUsedError; + @JsonKey(name: "max_price_of_options") + double get maxPriceOfOptions => throw _privateConstructorUsedError; bool get wasLikedByMe => throw _privateConstructorUsedError; /// Serializes this PlanModel to a JSON map. @@ -318,7 +322,7 @@ abstract class $PlanModelCopyWith<$Res> { @JsonKey(name: "image") String image, @JsonKey(name: "duration") int duration, @JsonKey(name: "state") String state, - @JsonKey(name: "type") PlanType type, + @JsonKey(name: "type", fromJson: planTypeFromJson) PlanType type, @JsonKey(name: "details") String details, @JsonKey(name: "packets_count") int packetsCount, @JsonKey(name: "packet1_options") PacketOptions? packet1Options, @@ -329,6 +333,8 @@ abstract class $PlanModelCopyWith<$Res> { @JsonKey(name: "total_views") int totalViews, @JsonKey(name: "total_comments") int totalComments, @JsonKey(name: "is_favorite") bool isFavorite, + @JsonKey(name: "min_price_of_options") double minPriceOfOptions, + @JsonKey(name: "max_price_of_options") double maxPriceOfOptions, bool wasLikedByMe}); $PlanTypeCopyWith<$Res> get type; @@ -370,6 +376,8 @@ class _$PlanModelCopyWithImpl<$Res, $Val extends PlanModel> Object? totalViews = null, Object? totalComments = null, Object? isFavorite = null, + Object? minPriceOfOptions = null, + Object? maxPriceOfOptions = null, Object? wasLikedByMe = null, }) { return _then(_value.copyWith( @@ -441,6 +449,14 @@ class _$PlanModelCopyWithImpl<$Res, $Val extends PlanModel> ? _value.isFavorite : isFavorite // ignore: cast_nullable_to_non_nullable as bool, + minPriceOfOptions: null == minPriceOfOptions + ? _value.minPriceOfOptions + : minPriceOfOptions // ignore: cast_nullable_to_non_nullable + as double, + maxPriceOfOptions: null == maxPriceOfOptions + ? _value.maxPriceOfOptions + : maxPriceOfOptions // ignore: cast_nullable_to_non_nullable + as double, wasLikedByMe: null == wasLikedByMe ? _value.wasLikedByMe : wasLikedByMe // ignore: cast_nullable_to_non_nullable @@ -526,7 +542,7 @@ abstract class _$$PlanModelImplCopyWith<$Res> @JsonKey(name: "image") String image, @JsonKey(name: "duration") int duration, @JsonKey(name: "state") String state, - @JsonKey(name: "type") PlanType type, + @JsonKey(name: "type", fromJson: planTypeFromJson) PlanType type, @JsonKey(name: "details") String details, @JsonKey(name: "packets_count") int packetsCount, @JsonKey(name: "packet1_options") PacketOptions? packet1Options, @@ -537,6 +553,8 @@ abstract class _$$PlanModelImplCopyWith<$Res> @JsonKey(name: "total_views") int totalViews, @JsonKey(name: "total_comments") int totalComments, @JsonKey(name: "is_favorite") bool isFavorite, + @JsonKey(name: "min_price_of_options") double minPriceOfOptions, + @JsonKey(name: "max_price_of_options") double maxPriceOfOptions, bool wasLikedByMe}); @override @@ -581,6 +599,8 @@ class __$$PlanModelImplCopyWithImpl<$Res> Object? totalViews = null, Object? totalComments = null, Object? isFavorite = null, + Object? minPriceOfOptions = null, + Object? maxPriceOfOptions = null, Object? wasLikedByMe = null, }) { return _then(_$PlanModelImpl( @@ -652,6 +672,14 @@ class __$$PlanModelImplCopyWithImpl<$Res> ? _value.isFavorite : isFavorite // ignore: cast_nullable_to_non_nullable as bool, + minPriceOfOptions: null == minPriceOfOptions + ? _value.minPriceOfOptions + : minPriceOfOptions // ignore: cast_nullable_to_non_nullable + as double, + maxPriceOfOptions: null == maxPriceOfOptions + ? _value.maxPriceOfOptions + : maxPriceOfOptions // ignore: cast_nullable_to_non_nullable + as double, wasLikedByMe: null == wasLikedByMe ? _value.wasLikedByMe : wasLikedByMe // ignore: cast_nullable_to_non_nullable @@ -670,7 +698,7 @@ class _$PlanModelImpl implements _PlanModel { @JsonKey(name: "image") required this.image, @JsonKey(name: "duration") required this.duration, @JsonKey(name: "state") required this.state, - @JsonKey(name: "type") required this.type, + @JsonKey(name: "type", fromJson: planTypeFromJson) required this.type, @JsonKey(name: "details") required this.details, @JsonKey(name: "packets_count") required this.packetsCount, @JsonKey(name: "packet1_options") this.packet1Options, @@ -681,6 +709,8 @@ class _$PlanModelImpl implements _PlanModel { @JsonKey(name: "total_views") required this.totalViews, @JsonKey(name: "total_comments") required this.totalComments, @JsonKey(name: "is_favorite") required this.isFavorite, + @JsonKey(name: "min_price_of_options") required this.minPriceOfOptions, + @JsonKey(name: "max_price_of_options") required this.maxPriceOfOptions, this.wasLikedByMe = false}); factory _$PlanModelImpl.fromJson(Map json) => @@ -705,7 +735,7 @@ class _$PlanModelImpl implements _PlanModel { @JsonKey(name: "state") final String state; @override - @JsonKey(name: "type") + @JsonKey(name: "type", fromJson: planTypeFromJson) final PlanType type; @override @JsonKey(name: "details") @@ -738,12 +768,18 @@ class _$PlanModelImpl implements _PlanModel { @JsonKey(name: "is_favorite") final bool isFavorite; @override + @JsonKey(name: "min_price_of_options") + final double minPriceOfOptions; + @override + @JsonKey(name: "max_price_of_options") + final double maxPriceOfOptions; + @override @JsonKey() final bool wasLikedByMe; @override String toString() { - return 'PlanModel(id: $id, slug: $slug, name: $name, image: $image, duration: $duration, state: $state, type: $type, details: $details, packetsCount: $packetsCount, packet1Options: $packet1Options, packet2Options: $packet2Options, packet3Options: $packet3Options, author: $author, totalLikes: $totalLikes, totalViews: $totalViews, totalComments: $totalComments, isFavorite: $isFavorite, wasLikedByMe: $wasLikedByMe)'; + return 'PlanModel(id: $id, slug: $slug, name: $name, image: $image, duration: $duration, state: $state, type: $type, details: $details, packetsCount: $packetsCount, packet1Options: $packet1Options, packet2Options: $packet2Options, packet3Options: $packet3Options, author: $author, totalLikes: $totalLikes, totalViews: $totalViews, totalComments: $totalComments, isFavorite: $isFavorite, minPriceOfOptions: $minPriceOfOptions, maxPriceOfOptions: $maxPriceOfOptions, wasLikedByMe: $wasLikedByMe)'; } @override @@ -777,32 +813,39 @@ class _$PlanModelImpl implements _PlanModel { other.totalComments == totalComments) && (identical(other.isFavorite, isFavorite) || other.isFavorite == isFavorite) && + (identical(other.minPriceOfOptions, minPriceOfOptions) || + other.minPriceOfOptions == minPriceOfOptions) && + (identical(other.maxPriceOfOptions, maxPriceOfOptions) || + other.maxPriceOfOptions == maxPriceOfOptions) && (identical(other.wasLikedByMe, wasLikedByMe) || other.wasLikedByMe == wasLikedByMe)); } @JsonKey(includeFromJson: false, includeToJson: false) @override - int get hashCode => Object.hash( - runtimeType, - id, - slug, - name, - image, - duration, - state, - type, - details, - packetsCount, - packet1Options, - packet2Options, - packet3Options, - author, - totalLikes, - totalViews, - totalComments, - isFavorite, - wasLikedByMe); + int get hashCode => Object.hashAll([ + runtimeType, + id, + slug, + name, + image, + duration, + state, + type, + details, + packetsCount, + packet1Options, + packet2Options, + packet3Options, + author, + totalLikes, + totalViews, + totalComments, + isFavorite, + minPriceOfOptions, + maxPriceOfOptions, + wasLikedByMe + ]); /// Create a copy of PlanModel /// with the given fields replaced by the non-null parameter values. @@ -828,7 +871,8 @@ abstract class _PlanModel implements PlanModel { @JsonKey(name: "image") required final String image, @JsonKey(name: "duration") required final int duration, @JsonKey(name: "state") required final String state, - @JsonKey(name: "type") required final PlanType type, + @JsonKey(name: "type", fromJson: planTypeFromJson) + required final PlanType type, @JsonKey(name: "details") required final String details, @JsonKey(name: "packets_count") required final int packetsCount, @JsonKey(name: "packet1_options") final PacketOptions? packet1Options, @@ -839,6 +883,10 @@ abstract class _PlanModel implements PlanModel { @JsonKey(name: "total_views") required final int totalViews, @JsonKey(name: "total_comments") required final int totalComments, @JsonKey(name: "is_favorite") required final bool isFavorite, + @JsonKey(name: "min_price_of_options") + required final double minPriceOfOptions, + @JsonKey(name: "max_price_of_options") + required final double maxPriceOfOptions, final bool wasLikedByMe}) = _$PlanModelImpl; factory _PlanModel.fromJson(Map json) = @@ -863,7 +911,7 @@ abstract class _PlanModel implements PlanModel { @JsonKey(name: "state") String get state; @override - @JsonKey(name: "type") + @JsonKey(name: "type", fromJson: planTypeFromJson) PlanType get type; @override @JsonKey(name: "details") @@ -896,6 +944,12 @@ abstract class _PlanModel implements PlanModel { @JsonKey(name: "is_favorite") bool get isFavorite; @override + @JsonKey(name: "min_price_of_options") + double get minPriceOfOptions; + @override + @JsonKey(name: "max_price_of_options") + double get maxPriceOfOptions; + @override bool get wasLikedByMe; /// Create a copy of PlanModel diff --git a/lib/features/feed/models/plan_pagination_model.g.dart b/lib/features/feed/models/plan_pagination_model.g.dart index 9f321958..abfe3930 100644 --- a/lib/features/feed/models/plan_pagination_model.g.dart +++ b/lib/features/feed/models/plan_pagination_model.g.dart @@ -32,7 +32,7 @@ _$PlanModelImpl _$$PlanModelImplFromJson(Map json) => image: json['image'] as String, duration: (json['duration'] as num).toInt(), state: json['state'] as String, - type: PlanType.fromJson(json['type'] as Map), + type: planTypeFromJson(json['type']), details: json['details'] as String, packetsCount: (json['packets_count'] as num).toInt(), packet1Options: json['packet1_options'] == null @@ -52,6 +52,8 @@ _$PlanModelImpl _$$PlanModelImplFromJson(Map json) => totalViews: (json['total_views'] as num).toInt(), totalComments: (json['total_comments'] as num).toInt(), isFavorite: json['is_favorite'] as bool, + minPriceOfOptions: (json['min_price_of_options'] as num).toDouble(), + maxPriceOfOptions: (json['max_price_of_options'] as num).toDouble(), wasLikedByMe: json['wasLikedByMe'] as bool? ?? false, ); @@ -74,6 +76,8 @@ Map _$$PlanModelImplToJson(_$PlanModelImpl instance) => 'total_views': instance.totalViews, 'total_comments': instance.totalComments, 'is_favorite': instance.isFavorite, + 'min_price_of_options': instance.minPriceOfOptions, + 'max_price_of_options': instance.maxPriceOfOptions, 'wasLikedByMe': instance.wasLikedByMe, }; 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 04c01bbd..87cdd95d 100644 --- a/lib/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart +++ b/lib/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart @@ -9,7 +9,7 @@ import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_sta import 'package:eatvio_app/features/feed/repositories/feed_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -class DietsCubit extends Cubit { +class DietsCubit extends Cubit{ final FeedRepository feedRepository; final DietRepository dietsRepository; int currentPage = 1; @@ -52,6 +52,7 @@ class DietsCubit extends Cubit { super.close(); } + void changeTypeFilter(int index) { emit(state.copyWith(filterState: state.filterState.copyWith(typeIndex: index), errorMessage: null)); EasyDebounce.debounce( @@ -63,6 +64,7 @@ class DietsCubit extends Cubit { ); } + void changeSortingType(int index) { emit(state.copyWith(filterState: state.filterState.copyWith(sortingIndex: index), errorMessage: null)); EasyDebounce.debounce( @@ -74,6 +76,7 @@ class DietsCubit extends Cubit { ); } + void changeDurationFilter(int index) { emit(state.copyWith(filterState: state.filterState.copyWith(durationIndex: index), errorMessage: null)); EasyDebounce.debounce( @@ -101,7 +104,11 @@ class DietsCubit extends Cubit { ); lastPage = dietsPagination.meta.lastPage; emit( - state.copyWith(dietsList: dietsPagination.data, isLoading: isLoading, errorMessage: null,), + state.copyWith( + dietsList: dietsPagination.data, + isLoading: isLoading, + errorMessage: null, + ), ); } catch (e) { emit( diff --git a/lib/features/feed/presentation/pages/feed_screen.dart b/lib/features/feed/presentation/pages/feed_screen.dart index 989e36a2..c5852416 100644 --- a/lib/features/feed/presentation/pages/feed_screen.dart +++ b/lib/features/feed/presentation/pages/feed_screen.dart @@ -1,4 +1,6 @@ import 'package:easy_debounce/easy_debounce.dart'; +import 'package:eatvio_app/core/widgets/add_item_button.dart'; +import 'package:eatvio_app/core/widgets/custom_search_bar.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'; @@ -163,16 +165,63 @@ class _FeedScreenState extends State { bottom: _chipGroup(), title: Row( children: [ - _searchBar(), + CustomSearchBar( + searchController: searchController, + onChanged: (query) { + if (query.length > 2) { + EasyDebounce.debounce( + 'searchFeed', + Duration(milliseconds: 500), + () { + switch (_selectedChipIndex) { + case 0: + feedCubit.loadFeedBlogPostList(query: query); + break; + case 1: + recipesCubit.loadRecipesList(query: query); + break; + case 2: + dietsCubit.loadDiets(query: query); + break; + case 3: + plansCubit.loadPlans(query: query); + break; + } + }, + ); + } else { + EasyDebounce.debounce( + 'searchFeed', + Duration(milliseconds: 500), + () { + switch (_selectedChipIndex) { + case 0: + feedCubit.loadFeedBlogPostList(); + break; + case 1: + recipesCubit.loadRecipesList(); + break; + case 2: + dietsCubit.loadDiets(); + break; + case 3: + plansCubit.loadPlans(); + break; + } + }, + ); + } + }, + ), SizedBox(width: 12), switch (_selectedChipIndex) { - 2 => _addItemButton( + 2 => AddItemButton( onTap: state.profile?.role == 'consultant' ? () => GoRouter.of(context).pushNamed(CreateDietScreen.name) : null, ), - 3 => _addItemButton( + 3 => AddItemButton( onTap: state.profile?.role == 'consultant' ? () => GoRouter.of(context).pushNamed(CreatePlanScreen.name) : null, ), - _ => _addItemButton() + _ => AddItemButton() }, ], ), @@ -181,21 +230,13 @@ class _FeedScreenState extends State { ), ]; }, - body: Builder( - builder: (BuildContext context) { - if (_selectedChipIndex == 0) { - return _buildAllBlogPosts(); - } else if (_selectedChipIndex == 1) { - return _buildAllRecipes(); - } else if (_selectedChipIndex == 2) { - return _buildDiets(); - } else if (_selectedChipIndex == 3) { - return _buildPlans(); - } else { - return Center(child: Text(chipsLabelList[_selectedChipIndex])); - } - }, - ), + body: switch (_selectedChipIndex) { + 0 => _buildAllBlogPosts(), + 1 => _buildAllRecipes(), + 2 => _buildDiets(), + 3 => _buildPlans(), + _ => Center(child: Text(chipsLabelList[_selectedChipIndex])), + }, ), ), ); @@ -473,111 +514,4 @@ class _FeedScreenState extends State { ), ); } - - _addItemButton({void Function()? onTap}) { - return InkWell( - onTap: onTap ?? - () { - GoRouter.of(context).pushNamed('blogPostCreateScreen'); - }, - borderRadius: BorderRadius.circular(64), - child: Ink( - decoration: BoxDecoration( - color: appTheme.blueA400, - shape: BoxShape.circle, - ), - child: Icon( - Icons.add_rounded, - color: appTheme.gray100, - ), - width: 40, - height: 40, - ), - ); - } - - _searchBar() { - return SizedBox( - height: 40, - width: MediaQuery.of(context).size.width - 84, - child: TextFormField( - controller: searchController, - autofocus: false, - style: TextStyle( - fontSize: 12, - ), - onChanged: (query) { - if (query.length > 2) { - EasyDebounce.debounce( - 'searchFeed', - Duration(milliseconds: 500), - () { - switch (_selectedChipIndex) { - case 0: - feedCubit.loadFeedBlogPostList(query: query); - break; - case 1: - recipesCubit.loadRecipesList(query: query); - break; - case 2: - dietsCubit.loadDiets(query: query); - break; - case 3: - plansCubit.loadPlans(query: query); - break; - } - }, - ); - } else { - EasyDebounce.debounce( - 'searchFeed', - Duration(milliseconds: 500), - () { - switch (_selectedChipIndex) { - case 0: - feedCubit.loadFeedBlogPostList(); - break; - case 1: - recipesCubit.loadRecipesList(); - break; - case 2: - dietsCubit.loadDiets(query: query == '' ? null : query); - break; - } - }, - ); - } - }, - decoration: InputDecoration( - hintText: 'Поиск', - hintStyle: TextStyle( - fontSize: 12, - color: appTheme.gray600, - fontWeight: FontWeight.w400, - ), - prefixIcon: Icon( - Icons.search, - color: appTheme.gray600, - size: 16, - ), - isDense: false, - contentPadding: EdgeInsets.all(8), - fillColor: appTheme.gray100, - filled: true, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(16), - borderSide: BorderSide.none, - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(16), - borderSide: BorderSide.none, - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(16), - borderSide: BorderSide.none, - ), - ), - ), - ); - } } 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 ab15c100..e055ea4f 100644 --- a/lib/features/feed/presentation/widgets/diet_filter_bottom_sheet.dart +++ b/lib/features/feed/presentation/widgets/diet_filter_bottom_sheet.dart @@ -4,14 +4,16 @@ import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_cub 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/features/my_diets/presentation/cubit/my_diets_cubit.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'; class DietFilterBottomSheet extends StatelessWidget { - const DietFilterBottomSheet({super.key, required this.dietsCubit}); - final DietsCubit dietsCubit; + const DietFilterBottomSheet({super.key, this.dietsCubit, this.myDietsCubit}); + final DietsCubit? dietsCubit; + final MyDietsCubit? myDietsCubit; @override Widget build(BuildContext context) { final s = S.of(context); @@ -32,7 +34,8 @@ class DietFilterBottomSheet extends StatelessWidget { ), SizedBox(height: 12), SortingDropDownFilter( - cubit: dietsCubit, + dietsCubit: dietsCubit, + myDietsCubit: myDietsCubit, ), SizedBox( height: 16, @@ -50,6 +53,7 @@ class DietFilterBottomSheet extends StatelessWidget { ), TypeChoiceFilterWidget( dietsCubit: dietsCubit, + myDietsCubit: myDietsCubit, ), SizedBox( height: 16, @@ -67,6 +71,7 @@ class DietFilterBottomSheet extends StatelessWidget { ), DurationChoiceFilterWidget( dietCubit: dietsCubit, + myDietsCubit: myDietsCubit, ), SizedBox( height: 24, diff --git a/lib/features/feed/presentation/widgets/diets_widget.dart b/lib/features/feed/presentation/widgets/diets_widget.dart index f18c61f7..6597f72e 100644 --- a/lib/features/feed/presentation/widgets/diets_widget.dart +++ b/lib/features/feed/presentation/widgets/diets_widget.dart @@ -1,7 +1,7 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/widgets/bookmark_button.dart'; -import 'package:eatvio_app/core/widgets/diet_info_row.dart'; +import 'package:eatvio_app/core/widgets/info_row.dart'; import 'package:eatvio_app/core/widgets/feed_icon_button.dart'; import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart'; 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 8f6e68a1..45c966d8 100644 --- a/lib/features/feed/presentation/widgets/duration_choice_filter_widget.dart +++ b/lib/features/feed/presentation/widgets/duration_choice_filter_widget.dart @@ -5,14 +5,20 @@ import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_sta import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_state.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/choice_chip_widget.dart'; +import 'package:eatvio_app/features/my_diets/presentation/cubit/my_diets_cubit.dart'; +import 'package:eatvio_app/features/my_diets/presentation/cubit/my_diets_state.dart'; +import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_cubit.dart'; +import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_state.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, this.dietCubit, this.plansCubit}); + const DurationChoiceFilterWidget({super.key, this.dietCubit, this.plansCubit, this.myDietsCubit, this.myPlansCubit}); final DietsCubit? dietCubit; final PlansCubit? plansCubit; + final MyDietsCubit? myDietsCubit; + final MyPlansCubit? myPlansCubit; @override State createState() => _DurationChoiceFilterWidgetState(); } @@ -39,102 +45,102 @@ class _DurationChoiceFilterWidgetState extends State builder: (context, state) { return Wrap( children: [ - ChoiceChipWidget( - chipsLabelList: chipsLabelList, - index: 0, - selectedChipIndex: state.filterState.durationIndex, - onSelected: (selectedValue) { - widget.dietCubit?.changeDurationFilter(0); - }, - ), - SizedBox( - width: 8, - ), - ChoiceChipWidget( - chipsLabelList: chipsLabelList, - index: 1, - selectedChipIndex: state.filterState.durationIndex, - onSelected: (selectedValue) { - widget.dietCubit?.changeDurationFilter(1); - }, - ), - SizedBox( - width: 8, - ), - ChoiceChipWidget( - chipsLabelList: chipsLabelList, - index: 2, - selectedChipIndex: state.filterState.durationIndex, - onSelected: (selectedValue) { - widget.dietCubit?.changeDurationFilter(2); - }, - ), - SizedBox( - width: 8, - ), - ChoiceChipWidget( - chipsLabelList: chipsLabelList, - index: 3, - selectedChipIndex: state.filterState.durationIndex, - onSelected: (selectedValue) { - widget.dietCubit?.changeDurationFilter(3); - }, + ...List.generate( + chipsLabelList.length, + (index) => Padding( + padding: EdgeInsets.only(right: index != chipsLabelList.length - 1 ? 8 : 0), + child: ChoiceChipWidget( + chipsLabelList: chipsLabelList, + index: index, + selectedChipIndex: state.filterState.durationIndex, + onSelected: (selectedValue) { + widget.dietCubit?.changeDurationFilter(index); + }, + ), + ), ), ], ); }, ); - } else { + } else if (widget.plansCubit != null) { return BlocBuilder( - bloc: widget.plansCubit!, + bloc: widget.plansCubit, builder: (context, state) { return Wrap( children: [ - ChoiceChipWidget( - chipsLabelList: chipsLabelList, - index: 0, - selectedChipIndex: state.filterState.durationIndex, - onSelected: (selectedValue) { - widget.plansCubit?.changeDurationFilter(0); - }, + ...List.generate( + chipsLabelList.length, + (index) => Padding( + padding: EdgeInsets.only(right: index != chipsLabelList.length - 1 ? 8 : 0), + child: ChoiceChipWidget( + chipsLabelList: chipsLabelList, + index: index, + selectedChipIndex: state.filterState.durationIndex, + onSelected: (selectedValue) { + widget.plansCubit?.changeDurationFilter(index); + }, + ), + ), ), - SizedBox( - width: 8, - ), - ChoiceChipWidget( - chipsLabelList: chipsLabelList, - index: 1, - selectedChipIndex: state.filterState.durationIndex, - onSelected: (selectedValue) { - widget.plansCubit?.changeDurationFilter(1); - }, - ), - SizedBox( - width: 8, - ), - ChoiceChipWidget( - chipsLabelList: chipsLabelList, - index: 2, - selectedChipIndex: state.filterState.durationIndex, - onSelected: (selectedValue) { - widget.plansCubit?.changeDurationFilter(2); - }, - ), - SizedBox( - width: 8, - ), - ChoiceChipWidget( - chipsLabelList: chipsLabelList, - index: 3, - selectedChipIndex: state.filterState.durationIndex, - onSelected: (selectedValue) { - widget.plansCubit?.changeDurationFilter(3); - }, + ], + ); + }, + ); + } else if (widget.myPlansCubit != null) { + return BlocBuilder( + bloc: widget.myPlansCubit, + builder: (context, state) { + return Wrap( + children: [ + ...List.generate( + chipsLabelList.length, + (index) => Padding( + padding: EdgeInsets.only(right: index != chipsLabelList.length - 1 ? 8 : 0), + child: ChoiceChipWidget( + chipsLabelList: chipsLabelList, + index: index, + selectedChipIndex: switch (state.selectedStateIndex) { + 1 => state.draftFilterState.durationIndex, + _ => state.activeFilterState.durationIndex + }, + onSelected: (selectedValue) { + widget.myPlansCubit?.changeDurationFilter(index); + }, + ), + ), ), ], ); }, ); } + return BlocBuilder( + bloc: widget.myDietsCubit, + builder: (context, state) { + return Wrap( + children: [ + ...List.generate( + chipsLabelList.length, + (index) => Padding( + padding: EdgeInsets.only(right: index != chipsLabelList.length - 1 ? 8 : 0), + child: ChoiceChipWidget( + chipsLabelList: chipsLabelList, + index: index, + selectedChipIndex: switch (state.selectedStateIndex) { + 1 => state.draftFilterState.durationIndex, + 2 => state.personalFilterState.durationIndex, + _ => state.activeFilterState.durationIndex + }, + onSelected: (selectedValue) { + widget.myDietsCubit?.changeDurationFilter(index); + }, + ), + ), + ), + ], + ); + }, + ); } } diff --git a/lib/features/feed/presentation/widgets/plan_widget.dart b/lib/features/feed/presentation/widgets/plan_widget.dart index e62eea64..f51dac71 100644 --- a/lib/features/feed/presentation/widgets/plan_widget.dart +++ b/lib/features/feed/presentation/widgets/plan_widget.dart @@ -1,13 +1,15 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/widgets/bookmark_button.dart'; -import 'package:eatvio_app/core/widgets/diet_info_row.dart'; +import 'package:eatvio_app/core/widgets/info_row.dart'; import 'package:eatvio_app/core/widgets/feed_icon_button.dart'; +import 'package:eatvio_app/features/diets/presentation/widgets/info_bouble.dart'; import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_cubit.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/feed/presentation/bloc/plans_cubit/plans_cubit.dart'; +import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_html/flutter_html.dart'; @@ -31,6 +33,7 @@ class PlanWidget extends StatefulWidget { class _PlanWidgetState extends State { @override Widget build(BuildContext context) { + final s = S.of(context); return DecoratedBox( decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), @@ -117,8 +120,61 @@ class _PlanWidgetState extends State { ], ], ), + SizedBox(height: 16), Padding( - padding: const EdgeInsets.only(left: 16, top: 12), + padding: const EdgeInsets.only(left: 16.0), + child: ListTile( + contentPadding: EdgeInsets.zero, + leading: Container( + width: 46, + height: 46, + decoration: BoxDecoration( + shape: BoxShape.circle, + image: DecorationImage( + image: NetworkImage(widget.plan.author.authorProfileIcon), + fit: BoxFit.cover, + ), + ), + ), + subtitle: Row( + children: [ + InfoBouble( + radius: 12, + backgroundColor: appTheme.gray50, + padding: EdgeInsets.symmetric(vertical: 4, horizontal: 10), + children: [ + Text( + '${widget.plan.author.authorPlaceInRating}-й в рейтинге', + style: TextStyles.textXSSemibold.copyWith( + color: appTheme.blueA400, + ), + ), + ], + ), + Spacer(), + Text( + '${widget.plan.packetsCount == 1 ? widget.plan.packet1Options?.price : '${s.from} ${widget.plan.minPriceOfOptions.toStringAsFixed(0)}'}₽', + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.greenA700), + ), + SizedBox(width: 16), + ], + ), + title: Padding( + padding: const EdgeInsets.only(bottom: 4.0), + child: Text( + widget.plan.author.authorName, + style: TextStyles.textSSemibold.copyWith( + color: appTheme.black, + ), + ), + ), + ), + ), + SizedBox(height: 16), + Padding( + padding: const EdgeInsets.only( + left: 16, + ), child: Row( children: [ FeedIconButton( diff --git a/lib/features/feed/presentation/widgets/plans_filter_bottom_sheet.dart b/lib/features/feed/presentation/widgets/plans_filter_bottom_sheet.dart index 7c77dc86..9bc794ee 100644 --- a/lib/features/feed/presentation/widgets/plans_filter_bottom_sheet.dart +++ b/lib/features/feed/presentation/widgets/plans_filter_bottom_sheet.dart @@ -4,14 +4,16 @@ import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_cub import 'package:eatvio_app/features/feed/presentation/widgets/duration_choice_filter_widget.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/price_choice_filter_widget.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/type_choice_filter_widget.dart'; +import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_cubit.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'; class PlansFilterBottomSheet extends StatelessWidget { - const PlansFilterBottomSheet({super.key, required this.plansCubit}); - final PlansCubit plansCubit; + const PlansFilterBottomSheet({super.key, this.plansCubit, this.myPlansCubit}); + final PlansCubit? plansCubit; + final MyPlansCubit? myPlansCubit; @override Widget build(BuildContext context) { final s = S.of(context); @@ -30,7 +32,7 @@ class PlansFilterBottomSheet extends StatelessWidget { style: TextStyles.textMSemibold.copyWith(color: appTheme.black), ), SizedBox(height: 12), - PriceChoiceFilterWidget(plansCubit: plansCubit), + PriceChoiceFilterWidget(plansCubit: plansCubit, myPlansCubit: myPlansCubit,), SizedBox(height: 16), CustomDivider(), SizedBox( @@ -45,6 +47,7 @@ class PlansFilterBottomSheet extends StatelessWidget { ), DurationChoiceFilterWidget( plansCubit: plansCubit, + myPlansCubit: myPlansCubit, ), SizedBox( height: 16, @@ -62,6 +65,7 @@ class PlansFilterBottomSheet extends StatelessWidget { ), TypeChoiceFilterWidget( plansCubit: plansCubit, + myPlansCubit: myPlansCubit, ), SizedBox( height: 24, diff --git a/lib/features/feed/presentation/widgets/price_choice_filter_widget.dart b/lib/features/feed/presentation/widgets/price_choice_filter_widget.dart index 015dc4cf..b01a5bbd 100644 --- a/lib/features/feed/presentation/widgets/price_choice_filter_widget.dart +++ b/lib/features/feed/presentation/widgets/price_choice_filter_widget.dart @@ -3,12 +3,19 @@ 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/feed/presentation/bloc/plans_cubit/plans_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_state.dart'; +import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_cubit.dart'; +import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_state.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class PriceChoiceFilterWidget extends StatefulWidget { - PriceChoiceFilterWidget({super.key, required this.plansCubit}); - final PlansCubit plansCubit; + PriceChoiceFilterWidget({ + super.key, + required this.plansCubit, + this.myPlansCubit, + }); + final PlansCubit? plansCubit; + final MyPlansCubit? myPlansCubit; @override State createState() => _PriceChoiceFilterWidgetState(); @@ -44,6 +51,164 @@ class _PriceChoiceFilterWidgetState extends State { @override Widget build(BuildContext context) { + if (widget.myPlansCubit != null) { + return BlocBuilder( + bloc: widget.myPlansCubit, + builder: (context, state) { + final minPlanPrice = switch (state.selectedStateIndex) { + 1 => state.minDraftPlanPrice, + _ => state.minActivePlanPrice, + }; + + final maxPlanPrice = switch (state.selectedStateIndex) { + 1 => state.maxDraftPlanPrice, + _ => state.maxActivePlanPrice, + }; + + final minPriceFilter = switch (state.selectedStateIndex) { + 1 => state.draftFilterState.priceFilter.$1, + _ => state.activeFilterState.priceFilter.$1, + }; + + final maxPriceFilter = switch (state.selectedStateIndex) { + 1 => state.draftFilterState.priceFilter.$2, + _ => state.activeFilterState.priceFilter.$2, + }; + + prevMinPrice ??= minPlanPrice; + prevMaxPrice ??= maxPlanPrice; + + minPriceController.text = minPriceFilter?.toStringAsFixed(0) ?? '₽'; + maxPriceController.text = maxPriceFilter?.toStringAsFixed(0) ?? '₽'; + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + Expanded( + child: CustomTextFormField( + keyboardType: TextInputType.number, + controller: minPriceController, + onChanged: (value) { + final price = int.tryParse(value.replaceAll('₽', '')); + if (price == null) { + EasyDebounce.cancel('changeMinPriceFilter'); + minPriceController.value = TextEditingValue( + text: '₽', + selection: TextSelection.collapsed(offset: 0), + ); + return; + } + if (price < minPlanPrice) { + minPriceController.value = TextEditingValue( + text: '${prevMinPrice ?? ''}₽', + selection: TextSelection.collapsed(offset: prevMinPrice.toString().length), + ); + return; + } + if (price > (maxPriceFilter ?? maxPlanPrice)) { + minPriceController.value = TextEditingValue( + text: '${prevMinPrice ?? ''}₽', + selection: TextSelection.collapsed(offset: prevMinPrice.toString().length), + ); + EasyDebounce.cancel('changeMinPriceFilter'); + widget.myPlansCubit?.changePriceFilter( + prevMinPrice ?? minPlanPrice, + maxPriceFilter ?? maxPlanPrice, + ); + return; + } + prevMinPrice = price; + EasyDebounce.debounce( + 'changeMinPriceFilter', + Duration(seconds: 2), + () => widget.myPlansCubit?.changePriceFilter( + price, + maxPriceFilter ?? maxPlanPrice, + disableDebounce: true, + ), + ); + }, + ), + ), + SizedBox(width: 8), + Expanded( + child: CustomTextFormField( + onChanged: (value) { + final price = int.tryParse(value.replaceAll('₽', '')); + if (price == null) { + EasyDebounce.cancel('changeMaxPriceFilter'); + maxPriceController.value = TextEditingValue( + text: '₽', + selection: TextSelection.collapsed(offset: 0), + ); + return; + } + if (price > maxPlanPrice) { + maxPriceController.value = TextEditingValue( + text: '${prevMaxPrice ?? ''}₽', + selection: TextSelection.collapsed(offset: prevMaxPrice.toString().length), + ); + return; + } + if (price < (minPriceFilter ?? minPlanPrice)) { + maxPriceController.value = TextEditingValue( + text: '${prevMaxPrice ?? ''}₽', + selection: TextSelection.collapsed(offset: prevMaxPrice.toString().length), + ); + EasyDebounce.cancel('changeMaxPriceFilter'); + widget.myPlansCubit?.changePriceFilter( + minPriceFilter ?? minPlanPrice, + prevMaxPrice ?? maxPlanPrice, + ); + return; + } + prevMaxPrice = price; + EasyDebounce.debounce( + 'changeMaxPriceFilter', + Duration(seconds: 2), + () => widget.myPlansCubit?.changePriceFilter( + minPriceFilter ?? minPlanPrice, + price, + disableDebounce: true, + ), + ); + }, + keyboardType: TextInputType.number, + controller: maxPriceController, + ), + ), + ], + ), + SizedBox(height: 12), + Theme( + data: Theme.of(context).copyWith( + sliderTheme: SliderThemeData( + overlayShape: SliderComponentShape.noOverlay, + ), + ), + child: RangeSlider( + activeColor: appTheme.blueA400, + inactiveColor: appTheme.blueA400, + min: minPlanPrice.toDouble(), + max: maxPlanPrice.toDouble(), + onChanged: (values) { + if (values.start <= values.end) { + widget.myPlansCubit?.changePriceFilter(values.start.toInt(), values.end.toInt()); + } + }, + values: RangeValues( + minPriceFilter?.toDouble() ?? minPlanPrice.toDouble(), + maxPriceFilter?.toDouble() ?? maxPlanPrice.toDouble(), + ), + ), + ), + ], + ); + }, + ); + } return BlocBuilder( bloc: widget.plansCubit, builder: (context, state) { @@ -84,7 +249,7 @@ class _PriceChoiceFilterWidgetState extends State { selection: TextSelection.collapsed(offset: prevMinPrice.toString().length), ); EasyDebounce.cancel('changeMinPriceFilter'); - widget.plansCubit.changePriceFilter( + widget.plansCubit?.changePriceFilter( prevMinPrice ?? state.minPlanPrice, state.filterState.priceFilter.$2 ?? state.maxPlanPrice, ); @@ -94,7 +259,7 @@ class _PriceChoiceFilterWidgetState extends State { EasyDebounce.debounce( 'changeMinPriceFilter', Duration(seconds: 2), - () => widget.plansCubit.changePriceFilter( + () => widget.plansCubit?.changePriceFilter( price, state.filterState.priceFilter.$2 ?? state.maxPlanPrice, disableDebounce: true, @@ -129,7 +294,7 @@ class _PriceChoiceFilterWidgetState extends State { selection: TextSelection.collapsed(offset: prevMaxPrice.toString().length), ); EasyDebounce.cancel('changeMaxPriceFilter'); - widget.plansCubit.changePriceFilter( + widget.plansCubit?.changePriceFilter( state.filterState.priceFilter.$1 ?? state.minPlanPrice, prevMaxPrice ?? state.maxPlanPrice, ); @@ -139,7 +304,7 @@ class _PriceChoiceFilterWidgetState extends State { EasyDebounce.debounce( 'changeMaxPriceFilter', Duration(seconds: 2), - () => widget.plansCubit.changePriceFilter( + () => widget.plansCubit?.changePriceFilter( state.filterState.priceFilter.$1 ?? state.minPlanPrice, price, disableDebounce: true, @@ -166,7 +331,7 @@ class _PriceChoiceFilterWidgetState extends State { max: state.maxPlanPrice.toDouble(), onChanged: (values) { if (values.start <= values.end) { - widget.plansCubit.changePriceFilter(values.start.toInt(), values.end.toInt()); + widget.plansCubit?.changePriceFilter(values.start.toInt(), values.end.toInt()); } }, values: RangeValues( 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 ec85409d..f03605ef 100644 --- a/lib/features/feed/presentation/widgets/sorting_drop_down_filter.dart +++ b/lib/features/feed/presentation/widgets/sorting_drop_down_filter.dart @@ -2,6 +2,8 @@ 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/features/my_diets/presentation/cubit/my_diets_cubit.dart'; +import 'package:eatvio_app/features/my_diets/presentation/cubit/my_diets_state.dart'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:flutter/material.dart'; @@ -9,8 +11,9 @@ import 'package:collection/collection.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class SortingDropDownFilter extends StatefulWidget { - const SortingDropDownFilter({super.key, required this.cubit}); - final DietsCubit cubit; + const SortingDropDownFilter({super.key, this.dietsCubit, this.myDietsCubit}); + final DietsCubit? dietsCubit; + final MyDietsCubit? myDietsCubit; @override State createState() => _SortingDropDownFilterState(); } @@ -33,9 +36,40 @@ class _SortingDropDownFilterState extends State { @override Widget build(BuildContext context) { - return BlocBuilder( - bloc: widget.cubit, + if (widget.dietsCubit != null) { + return BlocBuilder( + bloc: widget.dietsCubit!, + 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.dietsCubit!.changeSortingType(sortingTypes.indexOf(value)); + } + }, + ); + }, + ); + } + return BlocBuilder( + bloc: widget.myDietsCubit!, builder: (context, state) { + final sortingIndex = switch (state.selectedStateIndex) { + 1 => state.draftFilterState.sortingIndex, + 2 => state.personalFilterState.sortingIndex, + _ => state.activeFilterState.sortingIndex + }; return CustomDropDownButton( items: sortingTypes .mapIndexed( @@ -48,10 +82,10 @@ class _SortingDropDownFilterState extends State { ), ) .toList(), - value: sortingTypes[state.filterState.sortingIndex], + value: sortingTypes[sortingIndex], onChanged: (value) { if (value != null) { - widget.cubit.changeSortingType(sortingTypes.indexOf(value)); + widget.myDietsCubit!.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 47766db0..d6009470 100644 --- a/lib/features/feed/presentation/widgets/type_choice_filter_widget.dart +++ b/lib/features/feed/presentation/widgets/type_choice_filter_widget.dart @@ -3,6 +3,10 @@ import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_sta import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_state.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/choice_chip_widget.dart'; +import 'package:eatvio_app/features/my_diets/presentation/cubit/my_diets_cubit.dart'; +import 'package:eatvio_app/features/my_diets/presentation/cubit/my_diets_state.dart'; +import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_cubit.dart'; +import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_state.dart'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -12,9 +16,13 @@ class TypeChoiceFilterWidget extends StatefulWidget { super.key, this.dietsCubit, this.plansCubit, + this.myDietsCubit, + this.myPlansCubit, }); final DietsCubit? dietsCubit; final PlansCubit? plansCubit; + final MyDietsCubit? myDietsCubit; + final MyPlansCubit? myPlansCubit; @override State createState() => _TypeChoiceFilterWidgetState(); } @@ -25,7 +33,7 @@ class _TypeChoiceFilterWidgetState extends State { void didChangeDependencies() { final s = S.of(context); super.didChangeDependencies(); - if (widget.dietsCubit != null) { + if (widget.dietsCubit != null || widget.myDietsCubit != null) { if (chipsLabelList.isEmpty) { chipsLabelList.add(s.all); chipsLabelList.add(s.vegan_type); @@ -52,124 +60,102 @@ class _TypeChoiceFilterWidgetState extends State { builder: (context, state) { return Wrap( children: [ - ChoiceChipWidget( - chipsLabelList: chipsLabelList, - index: 0, - selectedChipIndex: state.filterState.typeIndex, - onSelected: (selectedValue) { - widget.plansCubit?.changeTypeFilter(0); - }, - ), - SizedBox( - width: 8, - ), - ChoiceChipWidget( - chipsLabelList: chipsLabelList, - index: 1, - selectedChipIndex: state.filterState.typeIndex, - onSelected: (selectedValue) { - widget.plansCubit?.changeTypeFilter(1); - }, - ), - SizedBox( - width: 8, - ), - ChoiceChipWidget( - chipsLabelList: chipsLabelList, - index: 2, - selectedChipIndex: state.filterState.typeIndex, - onSelected: (selectedValue) { - widget.plansCubit?.changeTypeFilter(2); - }, - ), - SizedBox( - width: 8, - ), - ChoiceChipWidget( - chipsLabelList: chipsLabelList, - index: 3, - selectedChipIndex: state.filterState.typeIndex, - onSelected: (selectedValue) { - widget.plansCubit?.changeTypeFilter(3); - }, - ), - SizedBox( - width: 8, - ), - ChoiceChipWidget( - chipsLabelList: chipsLabelList, - index: 4, - selectedChipIndex: state.filterState.typeIndex, - onSelected: (selectedValue) { - widget.plansCubit?.changeTypeFilter(4); - }, + ...List.generate( + chipsLabelList.length, + (index) => Padding( + padding: EdgeInsets.only(right: index != chipsLabelList.length - 1 ? 8.0 : 0), + child: ChoiceChipWidget( + chipsLabelList: chipsLabelList, + index: index, + selectedChipIndex: state.filterState.typeIndex, + onSelected: (selectedValue) { + widget.plansCubit?.changeTypeFilter(index); + }, + ), + ), ), ], ); }, ); - } else { + } else if (widget.dietsCubit != null) { return BlocBuilder( bloc: widget.dietsCubit, builder: (context, state) { return Wrap( children: [ - ChoiceChipWidget( - chipsLabelList: chipsLabelList, - index: 0, - selectedChipIndex: state.filterState.typeIndex, - onSelected: (selectedValue) { - widget.dietsCubit?.changeTypeFilter(0); - }, + ...List.generate( + chipsLabelList.length, + (index) => Padding( + padding: EdgeInsets.only(right: index != chipsLabelList.length - 1 ? 8.0 : 0), + child: ChoiceChipWidget( + chipsLabelList: chipsLabelList, + index: index, + selectedChipIndex: state.filterState.typeIndex, + onSelected: (selectedValue) { + widget.dietsCubit?.changeTypeFilter(index); + }, + ), + ), ), - SizedBox( - width: 8, - ), - ChoiceChipWidget( - chipsLabelList: chipsLabelList, - index: 1, - selectedChipIndex: state.filterState.typeIndex, - onSelected: (selectedValue) { - widget.dietsCubit?.changeTypeFilter(1); - }, - ), - SizedBox( - width: 8, - ), - ChoiceChipWidget( - chipsLabelList: chipsLabelList, - index: 2, - selectedChipIndex: state.filterState.typeIndex, - onSelected: (selectedValue) { - widget.dietsCubit?.changeTypeFilter(2); - }, - ), - SizedBox( - width: 8, - ), - ChoiceChipWidget( - chipsLabelList: chipsLabelList, - index: 3, - selectedChipIndex: state.filterState.typeIndex, - onSelected: (selectedValue) { - widget.dietsCubit?.changeTypeFilter(3); - }, - ), - SizedBox( - width: 8, - ), - ChoiceChipWidget( - chipsLabelList: chipsLabelList, - index: 4, - selectedChipIndex: state.filterState.typeIndex, - onSelected: (selectedValue) { - widget.dietsCubit?.changeTypeFilter(4); - }, + ], + ); + }, + ); + } else if (widget.myPlansCubit != null) { + return BlocBuilder( + bloc: widget.myPlansCubit, + builder: (context, state) { + return Wrap( + children: [ + ...List.generate( + chipsLabelList.length, + (index) => Padding( + padding: EdgeInsets.only(right: index != chipsLabelList.length - 1 ? 8.0 : 0), + child: ChoiceChipWidget( + chipsLabelList: chipsLabelList, + index: index, + selectedChipIndex: switch (state.selectedStateIndex) { + 1 => state.draftFilterState.typeIndex, + _ => state.activeFilterState.typeIndex + }, + onSelected: (selectedValue) { + widget.myPlansCubit?.changeTypeFilter(index); + }, + ), + ), ), ], ); }, ); } + return BlocBuilder( + bloc: widget.myDietsCubit, + builder: (context, state) { + return Wrap( + children: [ + ...List.generate( + chipsLabelList.length, + (index) => Padding( + padding: EdgeInsets.only(right: index != chipsLabelList.length - 1 ? 8.0 : 0), + child: ChoiceChipWidget( + chipsLabelList: chipsLabelList, + index: index, + selectedChipIndex: switch (state.selectedStateIndex) { + 1 => state.draftFilterState.typeIndex, + 2 => state.personalFilterState.typeIndex, + _ => state.activeFilterState.typeIndex + }, + onSelected: (selectedValue) { + widget.myDietsCubit?.changeTypeFilter(index); + }, + ), + ), + ), + ], + ); + }, + ); } } diff --git a/lib/features/menu/presentation/pages/menu_screen.dart b/lib/features/menu/presentation/pages/menu_screen.dart index 58f0fea3..50c3d308 100644 --- a/lib/features/menu/presentation/pages/menu_screen.dart +++ b/lib/features/menu/presentation/pages/menu_screen.dart @@ -4,6 +4,8 @@ import 'package:eatvio_app/core/token_storage.dart'; import 'package:eatvio_app/core/utils/image_constant.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/features/my_diets/presentation/my_diets_screen.dart'; +import 'package:eatvio_app/features/my_plans/presentation/my_plans_screen.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:go_router/go_router.dart'; @@ -36,13 +38,13 @@ class MenuScreen extends StatelessWidget { _tile( leading: 'assets/icons/diets.svg', title: 'Мои диеты', - onTap: () => GoRouter.of(context).push('/diet-select-screen'), + onTap: () => GoRouter.of(context).push(MyDietsScreen.path), ), _divider(context), _tile( leading: ImageConstant.icnPlans, title: 'Мои планы', - onTap: () => GoRouter.of(context).push('/plan-select-screen'), + onTap: () => GoRouter.of(context).push(MyPlansScreen.path), ), _divider(context), _tile( diff --git a/lib/features/my_diets/presentation/cubit/my_diets_cubit.dart b/lib/features/my_diets/presentation/cubit/my_diets_cubit.dart new file mode 100644 index 00000000..7a998163 --- /dev/null +++ b/lib/features/my_diets/presentation/cubit/my_diets_cubit.dart @@ -0,0 +1,250 @@ +import 'package:easy_debounce/easy_debounce.dart'; +import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/features/my_diets/presentation/cubit/my_diets_state.dart'; +import 'package:eatvio_app/features/my_diets/repositories/my_diets_repository.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class MyDietsCubit extends Cubit { + final MyDietsRepository myDietsRepository; + + int currentDraftPage = 1; + int currentPersonalPage = 1; + int currentActivePage = 1; + late int lastDraftPage; + late int lastPersonalPage; + late int lastActivePage; + bool isLoadingDraft = false; + bool isLoadingPersonal = false; + bool isLoadingActive = false; + String? _activeQuery; + String? _personalQuery; + String? _draftQuery; + + 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, ...dietTypeValues]; + + MyDietsCubit({required this.myDietsRepository}) : super(MyDietsState()); + + void changeTypeFilter(int index) { + if (state.selectedStateIndex == 0) { + emit(state.copyWith(activeFilterState: state.activeFilterState.copyWith(typeIndex: index), error: null)); + EasyDebounce.debounce( + 'loadDiets', + Duration(seconds: 1), + () => loadDiets( + query: _activeQuery, + ), + ); + } else if (state.selectedStateIndex == 1) { + emit(state.copyWith(draftFilterState: state.draftFilterState.copyWith(typeIndex: index), error: null)); + EasyDebounce.debounce( + 'loadDiets', + Duration(seconds: 1), + () => loadDiets( + query: _draftQuery, + ), + ); + } else if (state.selectedStateIndex == 2) { + emit(state.copyWith(personalFilterState: state.personalFilterState.copyWith(typeIndex: index), error: null)); + EasyDebounce.debounce( + 'loadDiets', + Duration(seconds: 1), + () => loadDiets( + query: _personalQuery, + ), + ); + } + } + + void changeSortingType(int index) { + if (state.selectedStateIndex == 0) { + emit(state.copyWith(activeFilterState: state.activeFilterState.copyWith(sortingIndex: index), error: null)); + EasyDebounce.debounce( + 'loadDiets', + Duration(seconds: 1), + () => loadDiets( + query: _activeQuery, + ), + ); + } else if (state.selectedStateIndex == 1) { + emit(state.copyWith(draftFilterState: state.draftFilterState.copyWith(sortingIndex: index), error: null)); + EasyDebounce.debounce( + 'loadDiets', + Duration(seconds: 1), + () => loadDiets( + query: _draftQuery, + ), + ); + } else if (state.selectedStateIndex == 2) { + emit(state.copyWith(personalFilterState: state.personalFilterState.copyWith(sortingIndex: index), error: null)); + EasyDebounce.debounce( + 'loadDiets', + Duration(seconds: 1), + () => loadDiets( + query: _personalQuery, + ), + ); + } + } + + void changeDurationFilter(int index) { + if (state.selectedStateIndex == 0) { + emit(state.copyWith(activeFilterState: state.activeFilterState.copyWith(durationIndex: index), error: null)); + EasyDebounce.debounce( + 'loadDiets', + Duration(seconds: 1), + () => loadDiets( + query: _activeQuery, + ), + ); + } else if (state.selectedStateIndex == 1) { + emit(state.copyWith(draftFilterState: state.draftFilterState.copyWith(durationIndex: index), error: null)); + EasyDebounce.debounce( + 'loadDiets', + Duration(seconds: 1), + () => loadDiets( + query: _draftQuery, + ), + ); + } else if (state.selectedStateIndex == 2) { + emit(state.copyWith(personalFilterState: state.personalFilterState.copyWith(durationIndex: index), error: null)); + EasyDebounce.debounce( + 'loadDiets', + Duration(seconds: 1), + () => loadDiets( + query: _personalQuery, + ), + ); + } + } + + void changeSelectedStateIndex(int index) { + emit(state.copyWith(selectedStateIndex: index)); + } + + Future loadDiets({ + String? query, + }) async { + switch (state.selectedStateIndex) { + case 0: + await _loadActiveDiets(query: query); + break; + case 1: + await _loadDraftDiets(query: query); + break; + case 2: + await _loadPersonalDiets(query: query); + break; + } + } + + Future loadAllDiets() async { + emit(state.copyWith(isLoading: true)); + await _loadActiveDiets(); + await _loadDraftDiets(); + await _loadPersonalDiets(); + emit(state.copyWith(isLoading: false)); + } + + Future _loadActiveDiets({String? query}) async { + try { + _activeQuery = query; + final type = _types[state.activeFilterState.typeIndex]; + final duration = _durationValues[state.activeFilterState.durationIndex]; + final sorting = _sortingValues[state.activeFilterState.sortingIndex]; + final dietsPagination = await myDietsRepository.getDiets( + query: query, + types: type, + duration: duration, + sorting: sorting, + ); + lastActivePage = dietsPagination.meta.lastPage; + emit( + state.copyWith( + activeDiets: dietsPagination.data, + isLoading: isLoadingActive, + error: null, + ), + ); + } catch (e) { + emit( + state.copyWith(error: e.toString()), + ); + } + } + + Future _loadDraftDiets({String? query}) async { + try { + _draftQuery = query; + final type = _types[state.draftFilterState.typeIndex]; + final duration = _durationValues[state.draftFilterState.durationIndex]; + final sorting = _sortingValues[state.draftFilterState.sortingIndex]; + final dietsPagination = await myDietsRepository.getDiets( + query: query, + types: type, + duration: duration, + sorting: sorting, + ); + lastActivePage = dietsPagination.meta.lastPage; + emit( + state.copyWith( + draftDiets: dietsPagination.data, + isLoading: isLoadingDraft, + error: null, + ), + ); + } catch (e) { + emit( + state.copyWith(error: e.toString()), + ); + } + } + + Future _loadPersonalDiets({String? query}) async { + try { + _personalQuery = query; + final type = _types[state.personalFilterState.typeIndex]; + final duration = _durationValues[state.personalFilterState.durationIndex]; + final sorting = _sortingValues[state.personalFilterState.sortingIndex]; + final dietsPagination = await myDietsRepository.getDiets( + query: query, + types: type, + duration: duration, + sorting: sorting, + ); + lastActivePage = dietsPagination.meta.lastPage; + emit( + state.copyWith( + personalDiets: dietsPagination.data, + isLoading: isLoadingPersonal, + error: null, + ), + ); + } catch (e) { + emit( + state.copyWith(error: e.toString()), + ); + } + } + + Future deleteDiet(int dietId) async { + try { + await myDietsRepository.deleteDiet(dietId); + await loadAllDiets(); + return true; + } catch (e) { + emit( + state.copyWith(error: e.toString()), + ); + return false; + } + } +} diff --git a/lib/features/my_diets/presentation/cubit/my_diets_state.dart b/lib/features/my_diets/presentation/cubit/my_diets_state.dart new file mode 100644 index 00000000..36b0ec38 --- /dev/null +++ b/lib/features/my_diets/presentation/cubit/my_diets_state.dart @@ -0,0 +1,41 @@ +import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; +import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_state.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'my_diets_state.freezed.dart'; + +@freezed +class MyDietsState with _$MyDietsState { + factory MyDietsState({ + @Default([]) List activeDiets, + @Default([]) List draftDiets, + @Default([]) List personalDiets, + @Default(false) bool isLoading, + @Default(null) String? error, + @Default(0) int selectedStateIndex, + @Default( + const DietsFilterState( + durationIndex: 0, + sortingIndex: 0, + typeIndex: 0, + ), + ) + DietsFilterState activeFilterState, + @Default( + const DietsFilterState( + durationIndex: 0, + sortingIndex: 0, + typeIndex: 0, + ), + ) + DietsFilterState draftFilterState, + @Default( + const DietsFilterState( + durationIndex: 0, + sortingIndex: 0, + typeIndex: 0, + ), + ) + DietsFilterState personalFilterState, + }) = _MyDietsState; +} diff --git a/lib/features/my_diets/presentation/cubit/my_diets_state.freezed.dart b/lib/features/my_diets/presentation/cubit/my_diets_state.freezed.dart new file mode 100644 index 00000000..b15a73ae --- /dev/null +++ b/lib/features/my_diets/presentation/cubit/my_diets_state.freezed.dart @@ -0,0 +1,400 @@ +// 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 'my_diets_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 _$MyDietsState { + List get activeDiets => throw _privateConstructorUsedError; + List get draftDiets => throw _privateConstructorUsedError; + List get personalDiets => throw _privateConstructorUsedError; + bool get isLoading => throw _privateConstructorUsedError; + String? get error => throw _privateConstructorUsedError; + int get selectedStateIndex => throw _privateConstructorUsedError; + DietsFilterState get activeFilterState => throw _privateConstructorUsedError; + DietsFilterState get draftFilterState => throw _privateConstructorUsedError; + DietsFilterState get personalFilterState => + throw _privateConstructorUsedError; + + /// Create a copy of MyDietsState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $MyDietsStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $MyDietsStateCopyWith<$Res> { + factory $MyDietsStateCopyWith( + MyDietsState value, $Res Function(MyDietsState) then) = + _$MyDietsStateCopyWithImpl<$Res, MyDietsState>; + @useResult + $Res call( + {List activeDiets, + List draftDiets, + List personalDiets, + bool isLoading, + String? error, + int selectedStateIndex, + DietsFilterState activeFilterState, + DietsFilterState draftFilterState, + DietsFilterState personalFilterState}); + + $DietsFilterStateCopyWith<$Res> get activeFilterState; + $DietsFilterStateCopyWith<$Res> get draftFilterState; + $DietsFilterStateCopyWith<$Res> get personalFilterState; +} + +/// @nodoc +class _$MyDietsStateCopyWithImpl<$Res, $Val extends MyDietsState> + implements $MyDietsStateCopyWith<$Res> { + _$MyDietsStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of MyDietsState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? activeDiets = null, + Object? draftDiets = null, + Object? personalDiets = null, + Object? isLoading = null, + Object? error = freezed, + Object? selectedStateIndex = null, + Object? activeFilterState = null, + Object? draftFilterState = null, + Object? personalFilterState = null, + }) { + return _then(_value.copyWith( + activeDiets: null == activeDiets + ? _value.activeDiets + : activeDiets // ignore: cast_nullable_to_non_nullable + as List, + draftDiets: null == draftDiets + ? _value.draftDiets + : draftDiets // ignore: cast_nullable_to_non_nullable + as List, + personalDiets: null == personalDiets + ? _value.personalDiets + : personalDiets // ignore: cast_nullable_to_non_nullable + as List, + isLoading: null == isLoading + ? _value.isLoading + : isLoading // ignore: cast_nullable_to_non_nullable + as bool, + error: freezed == error + ? _value.error + : error // ignore: cast_nullable_to_non_nullable + as String?, + selectedStateIndex: null == selectedStateIndex + ? _value.selectedStateIndex + : selectedStateIndex // ignore: cast_nullable_to_non_nullable + as int, + activeFilterState: null == activeFilterState + ? _value.activeFilterState + : activeFilterState // ignore: cast_nullable_to_non_nullable + as DietsFilterState, + draftFilterState: null == draftFilterState + ? _value.draftFilterState + : draftFilterState // ignore: cast_nullable_to_non_nullable + as DietsFilterState, + personalFilterState: null == personalFilterState + ? _value.personalFilterState + : personalFilterState // ignore: cast_nullable_to_non_nullable + as DietsFilterState, + ) as $Val); + } + + /// Create a copy of MyDietsState + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $DietsFilterStateCopyWith<$Res> get activeFilterState { + return $DietsFilterStateCopyWith<$Res>(_value.activeFilterState, (value) { + return _then(_value.copyWith(activeFilterState: value) as $Val); + }); + } + + /// Create a copy of MyDietsState + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $DietsFilterStateCopyWith<$Res> get draftFilterState { + return $DietsFilterStateCopyWith<$Res>(_value.draftFilterState, (value) { + return _then(_value.copyWith(draftFilterState: value) as $Val); + }); + } + + /// Create a copy of MyDietsState + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $DietsFilterStateCopyWith<$Res> get personalFilterState { + return $DietsFilterStateCopyWith<$Res>(_value.personalFilterState, (value) { + return _then(_value.copyWith(personalFilterState: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$MyDietsStateImplCopyWith<$Res> + implements $MyDietsStateCopyWith<$Res> { + factory _$$MyDietsStateImplCopyWith( + _$MyDietsStateImpl value, $Res Function(_$MyDietsStateImpl) then) = + __$$MyDietsStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {List activeDiets, + List draftDiets, + List personalDiets, + bool isLoading, + String? error, + int selectedStateIndex, + DietsFilterState activeFilterState, + DietsFilterState draftFilterState, + DietsFilterState personalFilterState}); + + @override + $DietsFilterStateCopyWith<$Res> get activeFilterState; + @override + $DietsFilterStateCopyWith<$Res> get draftFilterState; + @override + $DietsFilterStateCopyWith<$Res> get personalFilterState; +} + +/// @nodoc +class __$$MyDietsStateImplCopyWithImpl<$Res> + extends _$MyDietsStateCopyWithImpl<$Res, _$MyDietsStateImpl> + implements _$$MyDietsStateImplCopyWith<$Res> { + __$$MyDietsStateImplCopyWithImpl( + _$MyDietsStateImpl _value, $Res Function(_$MyDietsStateImpl) _then) + : super(_value, _then); + + /// Create a copy of MyDietsState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? activeDiets = null, + Object? draftDiets = null, + Object? personalDiets = null, + Object? isLoading = null, + Object? error = freezed, + Object? selectedStateIndex = null, + Object? activeFilterState = null, + Object? draftFilterState = null, + Object? personalFilterState = null, + }) { + return _then(_$MyDietsStateImpl( + activeDiets: null == activeDiets + ? _value._activeDiets + : activeDiets // ignore: cast_nullable_to_non_nullable + as List, + draftDiets: null == draftDiets + ? _value._draftDiets + : draftDiets // ignore: cast_nullable_to_non_nullable + as List, + personalDiets: null == personalDiets + ? _value._personalDiets + : personalDiets // ignore: cast_nullable_to_non_nullable + as List, + isLoading: null == isLoading + ? _value.isLoading + : isLoading // ignore: cast_nullable_to_non_nullable + as bool, + error: freezed == error + ? _value.error + : error // ignore: cast_nullable_to_non_nullable + as String?, + selectedStateIndex: null == selectedStateIndex + ? _value.selectedStateIndex + : selectedStateIndex // ignore: cast_nullable_to_non_nullable + as int, + activeFilterState: null == activeFilterState + ? _value.activeFilterState + : activeFilterState // ignore: cast_nullable_to_non_nullable + as DietsFilterState, + draftFilterState: null == draftFilterState + ? _value.draftFilterState + : draftFilterState // ignore: cast_nullable_to_non_nullable + as DietsFilterState, + personalFilterState: null == personalFilterState + ? _value.personalFilterState + : personalFilterState // ignore: cast_nullable_to_non_nullable + as DietsFilterState, + )); + } +} + +/// @nodoc + +class _$MyDietsStateImpl implements _MyDietsState { + _$MyDietsStateImpl( + {final List activeDiets = const [], + final List draftDiets = const [], + final List personalDiets = const [], + this.isLoading = false, + this.error = null, + this.selectedStateIndex = 0, + this.activeFilterState = const DietsFilterState( + durationIndex: 0, sortingIndex: 0, typeIndex: 0), + this.draftFilterState = const DietsFilterState( + durationIndex: 0, sortingIndex: 0, typeIndex: 0), + this.personalFilterState = const DietsFilterState( + durationIndex: 0, sortingIndex: 0, typeIndex: 0)}) + : _activeDiets = activeDiets, + _draftDiets = draftDiets, + _personalDiets = personalDiets; + + final List _activeDiets; + @override + @JsonKey() + List get activeDiets { + if (_activeDiets is EqualUnmodifiableListView) return _activeDiets; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_activeDiets); + } + + final List _draftDiets; + @override + @JsonKey() + List get draftDiets { + if (_draftDiets is EqualUnmodifiableListView) return _draftDiets; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_draftDiets); + } + + final List _personalDiets; + @override + @JsonKey() + List get personalDiets { + if (_personalDiets is EqualUnmodifiableListView) return _personalDiets; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_personalDiets); + } + + @override + @JsonKey() + final bool isLoading; + @override + @JsonKey() + final String? error; + @override + @JsonKey() + final int selectedStateIndex; + @override + @JsonKey() + final DietsFilterState activeFilterState; + @override + @JsonKey() + final DietsFilterState draftFilterState; + @override + @JsonKey() + final DietsFilterState personalFilterState; + + @override + String toString() { + return 'MyDietsState(activeDiets: $activeDiets, draftDiets: $draftDiets, personalDiets: $personalDiets, isLoading: $isLoading, error: $error, selectedStateIndex: $selectedStateIndex, activeFilterState: $activeFilterState, draftFilterState: $draftFilterState, personalFilterState: $personalFilterState)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$MyDietsStateImpl && + const DeepCollectionEquality() + .equals(other._activeDiets, _activeDiets) && + const DeepCollectionEquality() + .equals(other._draftDiets, _draftDiets) && + const DeepCollectionEquality() + .equals(other._personalDiets, _personalDiets) && + (identical(other.isLoading, isLoading) || + other.isLoading == isLoading) && + (identical(other.error, error) || other.error == error) && + (identical(other.selectedStateIndex, selectedStateIndex) || + other.selectedStateIndex == selectedStateIndex) && + (identical(other.activeFilterState, activeFilterState) || + other.activeFilterState == activeFilterState) && + (identical(other.draftFilterState, draftFilterState) || + other.draftFilterState == draftFilterState) && + (identical(other.personalFilterState, personalFilterState) || + other.personalFilterState == personalFilterState)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + const DeepCollectionEquality().hash(_activeDiets), + const DeepCollectionEquality().hash(_draftDiets), + const DeepCollectionEquality().hash(_personalDiets), + isLoading, + error, + selectedStateIndex, + activeFilterState, + draftFilterState, + personalFilterState); + + /// Create a copy of MyDietsState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$MyDietsStateImplCopyWith<_$MyDietsStateImpl> get copyWith => + __$$MyDietsStateImplCopyWithImpl<_$MyDietsStateImpl>(this, _$identity); +} + +abstract class _MyDietsState implements MyDietsState { + factory _MyDietsState( + {final List activeDiets, + final List draftDiets, + final List personalDiets, + final bool isLoading, + final String? error, + final int selectedStateIndex, + final DietsFilterState activeFilterState, + final DietsFilterState draftFilterState, + final DietsFilterState personalFilterState}) = _$MyDietsStateImpl; + + @override + List get activeDiets; + @override + List get draftDiets; + @override + List get personalDiets; + @override + bool get isLoading; + @override + String? get error; + @override + int get selectedStateIndex; + @override + DietsFilterState get activeFilterState; + @override + DietsFilterState get draftFilterState; + @override + DietsFilterState get personalFilterState; + + /// Create a copy of MyDietsState + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$MyDietsStateImplCopyWith<_$MyDietsStateImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/features/my_diets/presentation/my_diets_screen.dart b/lib/features/my_diets/presentation/my_diets_screen.dart new file mode 100644 index 00000000..65ac8441 --- /dev/null +++ b/lib/features/my_diets/presentation/my_diets_screen.dart @@ -0,0 +1,194 @@ +import 'package:easy_debounce/easy_debounce.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/widgets/add_item_button.dart'; +import 'package:eatvio_app/core/widgets/custom_search_bar.dart'; +import 'package:eatvio_app/core/widgets/loader.dart'; +import 'package:eatvio_app/features/create_diet/presentation/create_diet_screen.dart'; +import 'package:eatvio_app/features/feed/presentation/widgets/diet_filter_bottom_sheet.dart'; +import 'package:eatvio_app/features/feed/presentation/widgets/filter_badge.dart'; +import 'package:eatvio_app/features/my_diets/presentation/cubit/my_diets_cubit.dart'; +import 'package:eatvio_app/features/my_diets/presentation/cubit/my_diets_state.dart'; +import 'package:eatvio_app/features/my_diets/presentation/widgets/my_diet_widget.dart'; +import 'package:eatvio_app/features/my_diets/presentation/widgets/my_diets_tab.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 MyDietsScreen extends StatelessWidget { + const MyDietsScreen({super.key}); + static const name = 'myDietsScreen'; + static const path = '/my-diets-screen'; + @override + Widget build(BuildContext context) { + return BlocProvider(create: (context) => sl()..loadAllDiets(), child: MyDietsScreenContent()); + } +} + +class MyDietsScreenContent extends StatelessWidget { + MyDietsScreenContent({super.key}); + + final activeDietsScrollController = ScrollController(); + final draftDietsScrollController = ScrollController(); + final personalDietsScrollController = ScrollController(); + + final activeDietsSearchController = SearchController(); + final draftDietsSearchController = SearchController(); + final personalDietsSearchController = SearchController(); + + @override + Widget build(BuildContext context) { + final s = S.of(context); + return Scaffold( + backgroundColor: appTheme.gray100, + appBar: AppBar( + centerTitle: true, + title: Text( + s.my_diets, + maxLines: 2, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + softWrap: true, + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), + ), + ), + body: BlocBuilder( + buildWhen: (previous, current) => previous.selectedStateIndex != current.selectedStateIndex, + builder: (context, state) { + return NestedScrollView( + controller: switch (state.selectedStateIndex) { + 1 => draftDietsScrollController, + 2 => personalDietsScrollController, + _ => activeDietsScrollController + }, + headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { + return [ + SliverAppBar( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(bottom: Radius.circular(12)), + ), + backgroundColor: appTheme.gray100, + foregroundColor: appTheme.gray100, + automaticallyImplyLeading: false, + stretchTriggerOffset: 1, + floating: true, + primary: false, + snap: true, + scrolledUnderElevation: 0, + bottom: PreferredSize( + preferredSize: Size( + MediaQuery.sizeOf(context).width, + 64, + ), + child: Padding( + padding: const EdgeInsets.only(bottom: 0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(height: 8), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: MyDietsTab(), + ), + SizedBox(height: 8), + Row( + children: [ + SizedBox(width: 4), + Expanded( + child: CustomSearchBar( + enableIcon: false, + backgroundColor: appTheme.whiteA700, + borderRadius: 100, + searchController: switch (state.selectedStateIndex) { + 1 => draftDietsSearchController, + 2 => personalDietsSearchController, + _ => activeDietsSearchController + }, + onChanged: (query) { + EasyDebounce.debounce( + 'searchDiets', + Duration(milliseconds: 500), + () { + context.read().loadDiets(query: query == '' ? null : query); + }, + ); + }, + ), + ), + SizedBox(width: 12), + AddItemButton( + onTap: () => GoRouter.of(context).pushNamed(CreateDietScreen.name), + ), + SizedBox(width: 4), + ], + ), + SizedBox(height: 16), + ], + ), + ), + ), + ), + ]; + }, + body: RefreshIndicator( + onRefresh: () => context.read().loadDiets(), + child: BlocBuilder( + builder: (BuildContext context, MyDietsState state) { + if (state.isLoading) { + return Scaffold( + backgroundColor: Colors.grey.shade200, + body: const Loader(), + ); + } else if (state.error != null) { + return Scaffold( + backgroundColor: Colors.grey.shade200, + body: Padding( + padding: const EdgeInsets.all(16), + child: Center( + child: Text(state.error ?? ''), + ), + ), + ); + } else { + final diets = switch (state.selectedStateIndex) { + 1 => state.draftDiets, + 2 => state.personalDiets, + _ => state.activeDiets, + }; + return Stack( + children: [ + SizedBox.expand(), + ListView.separated( + shrinkWrap: true, + physics: BouncingScrollPhysics(), + itemBuilder: (context, index) { + return MyDietWidget(diet: diets[index]); + }, + separatorBuilder: (context, index) { + return SizedBox(height: 20); + }, + itemCount: diets.length, + ), + Positioned( + right: 0, + bottom: 74, + child: FilterBadge( + filterBottomSheet: DietFilterBottomSheet( + myDietsCubit: context.read(), + ), + ), + ), + ], + ); + } + }, + ), + ), + ); + }, + ), + ); + } +} diff --git a/lib/features/my_diets/presentation/widgets/my_diet_widget.dart b/lib/features/my_diets/presentation/widgets/my_diet_widget.dart new file mode 100644 index 00000000..41655991 --- /dev/null +++ b/lib/features/my_diets/presentation/widgets/my_diet_widget.dart @@ -0,0 +1,252 @@ +import 'package:cached_network_image/cached_network_image.dart'; +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/widgets/bottom_sheet_handler_bar.dart'; +import 'package:eatvio_app/core/widgets/bottom_sheet_with_blur.dart'; +import 'package:eatvio_app/core/widgets/custom_outlined_button.dart'; +import 'package:eatvio_app/core/widgets/info_row.dart'; +import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; +import 'package:eatvio_app/features/my_diets/presentation/cubit/my_diets_cubit.dart'; +import 'package:eatvio_app/features/my_diets/presentation/cubit/my_diets_state.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_html/flutter_html.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:go_router/go_router.dart'; +import 'package:share_plus/share_plus.dart'; + +class MyDietWidget extends StatefulWidget { + final Diet diet; + const MyDietWidget({ + super.key, + required this.diet, + }); + + @override + State createState() => _MyDietWidgetState(); +} + +class _MyDietWidgetState extends State { + @override + Widget build(BuildContext context) { + final s = S.of(context); + return DecoratedBox( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: Colors.white, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Stack( + children: [ + InkWell( + onTap: () => GoRouter.of(context).pushNamed('aboutDietScreen', extra: {'diet': widget.diet, 'navigateToComments': false}), + child: ClipRRect( + borderRadius: BorderRadius.vertical(top: Radius.circular(12)), + child: CachedNetworkImage( + imageUrl: baseUrl + '/storage/' + widget.diet.image, + fit: BoxFit.fitWidth, + width: MediaQuery.of(context).size.width, + ), + ), + ), + Positioned( + bottom: 16, + child: SizedBox( + width: MediaQuery.sizeOf(context).width, + child: InfoRow( + diet: widget.diet, + itemsBackgroundColor: appTheme.whiteA700, + disableDescriptionOfTypes: true, + ), + ), + ), + ], + ), + SizedBox(height: 12), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Text( + widget.diet.name, + style: TextStyles.textMSemibold.copyWith(color: appTheme.black), + ), + ), + if (widget.diet.details.isNotEmpty) ...[ + SizedBox( + height: 12, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Html( + data: widget.diet.details.length > 200 ? widget.diet.details.substring(0, 200) + '...' : widget.diet.details, + style: { + "*": Style( + margin: Margins.zero, + color: appTheme.black, + fontSize: FontSize(TextStyles.textSMedium.fontSize ?? 14), + fontWeight: TextStyles.textSMedium.fontWeight ?? FontWeight.w400, + ), + }, + ), + ), + ], + ], + ), + SizedBox(height: 20), + BlocBuilder( + builder: (context, state) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + children: [ + Row( + children: [ + Expanded( + child: CustomOutlinedButton( + text: switch (state.selectedStateIndex) { + 1 => s.publish, + 2 => s.toOffer, + _ => s.edit, + }, + height: 52, + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), + buttonStyle: ButtonStyle( + shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))), + backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), + side: WidgetStatePropertyAll(BorderSide.none), + ), + ), + ), + if (state.selectedStateIndex == 0) ...[ + SizedBox(width: 6), + CustomOutlinedButton( + onPressed: () async { + await Share.shareUri( + Uri.parse( + baseUrl + '/ru/diet/' + widget.diet.id.toString(), + ), + ); + }, + height: 52, + width: 52, + leftIcon: SvgPicture.asset( + 'assets/icons/share2.svg', + width: 20, + height: 20, + colorFilter: ColorFilter.mode(appTheme.whiteA700, BlendMode.srcIn), + ), + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.red), + buttonStyle: ButtonStyle( + shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))), + backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), + side: WidgetStatePropertyAll(BorderSide.none), + ), + text: '', + ), + ], + ], + ), + SizedBox(height: 8), + Row( + children: [ + Expanded( + child: CustomOutlinedButton( + text: switch (state.selectedStateIndex) { + 0 => s.removeFromActive, + _ => s.edit, + }, + height: 52, + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.black), + buttonStyle: ButtonStyle( + shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))), + backgroundColor: WidgetStatePropertyAll(appTheme.lightGray0), + side: WidgetStatePropertyAll(BorderSide.none), + ), + ), + ), + SizedBox(width: 6), + CustomOutlinedButton( + onPressed: () async { + final result = await context.read().deleteDiet(widget.diet.id); + if (result) + Future.delayed(Duration(seconds: 3), () { + showBottomSheetWithBlur( + context: context, + child: Column( + children: [ + SizedBox( + height: 20, + ), + BottomSheetHandlerBar(), + SizedBox( + height: 20, + ), + Text( + '${widget.diet.name}', + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), + ), + Text( + s.deleted, + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), + ), + SizedBox( + height: 20, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: CustomOutlinedButton( + onPressed: () { + context.pop(); + }, + text: 'Хорошо', + height: 52, + buttonTextStyle: TextStyles.textSSemibold.copyWith(color: appTheme.whiteA700), + buttonStyle: ButtonStyle( + side: WidgetStatePropertyAll(BorderSide.none), + backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), + ), + ), + ), + SizedBox( + height: 20, + ), + ], + ), + ); + }); + }, + height: 52, + width: 52, + leftIcon: SvgPicture.asset( + 'assets/icons/trash_can.svg', + width: 20, + height: 20, + ), + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.red), + buttonStyle: ButtonStyle( + shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))), + backgroundColor: WidgetStatePropertyAll(appTheme.lightGray0), + side: WidgetStatePropertyAll(BorderSide.none), + ), + text: '', + ), + ], + ), + SizedBox(height: 8), + ], + ), + ); + }, + ), + ], + ), + ); + } +} diff --git a/lib/features/my_diets/presentation/widgets/my_diets_tab.dart b/lib/features/my_diets/presentation/widgets/my_diets_tab.dart new file mode 100644 index 00000000..a653441c --- /dev/null +++ b/lib/features/my_diets/presentation/widgets/my_diets_tab.dart @@ -0,0 +1,74 @@ +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/animated_option_button.dart'; +import 'package:eatvio_app/features/my_diets/presentation/cubit/my_diets_cubit.dart'; +import 'package:eatvio_app/features/my_diets/presentation/cubit/my_diets_state.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class MyDietsTab extends StatefulWidget { + const MyDietsTab({super.key, this.additionalOnTap}); + final void Function()? additionalOnTap; + @override + State createState() => _MyDietsTabState(); +} + +class _MyDietsTabState extends State { + final List tabTitles = []; + @override + void didChangeDependencies() { + final s = S.of(context); + if (tabTitles.isEmpty) { + tabTitles.add(s.published); + tabTitles.add(s.drafted); + tabTitles.add(s.personal); + } + super.didChangeDependencies(); + } + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return Align( + alignment: Alignment.centerLeft, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + Container( + width: 484, + height: 48, + decoration: BoxDecoration( + color: appTheme.whiteA700, + borderRadius: BorderRadius.circular(100), + ), + padding: const EdgeInsets.all(4), + child: Row( + children: [ + ...List.generate( + 3, + (index) => Expanded( + child: AnimatedOptionButton( + textStyle: TextStyles.textSMedium, + isSelected: state.selectedStateIndex == index, + text: tabTitles[index], + onTap: () { + widget.additionalOnTap?.call(); + context.read().changeSelectedStateIndex(index); + }, + ), + ), + ), + ], + ), + ), + ], + ), + ), + ); + }, + ); + } +} diff --git a/lib/features/my_diets/repositories/my_diets_repository.dart b/lib/features/my_diets/repositories/my_diets_repository.dart new file mode 100644 index 00000000..8c15a546 --- /dev/null +++ b/lib/features/my_diets/repositories/my_diets_repository.dart @@ -0,0 +1,36 @@ +import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; +import 'package:eatvio_app/features/my_diets/services/my_diets_service.dart'; + +class MyDietsRepository { + final MyDietsService myDietsService; + + MyDietsRepository({required this.myDietsService}); + + Future deleteDiet(int dietId) async { + try { + await myDietsService.deleteDiet(dietId: dietId); + } catch (e) { + rethrow; + } + } + + Future getDiets({ + int page = 1, + String? query, + String? types, + int? duration, + String? sorting, + }) async { + try { + return (await myDietsService.getDiets( + page: page, + query: query, + types: types, + duration: duration, + sorting: sorting, + )); + } catch (e) { + rethrow; + } + } +} diff --git a/lib/features/my_diets/services/my_diets_service.dart b/lib/features/my_diets/services/my_diets_service.dart new file mode 100644 index 00000000..e2bba8a0 --- /dev/null +++ b/lib/features/my_diets/services/my_diets_service.dart @@ -0,0 +1,27 @@ +import 'package:dio/dio.dart'; +import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; +import 'package:retrofit/retrofit.dart'; + +part 'my_diets_service.g.dart'; + +@RestApi() +abstract class MyDietsService { + + factory MyDietsService(Dio dio, {String baseUrl}) = _MyDietsService; + + @GET('/api/diets/index') + Future getDiets({ + @Query('page') required int page, + @Query('filters[name][\$startsWith]') String? query, + @Query('filters[type][\$contains]') String? types, + @Query('filters[duration][\$eq]') int? duration, + @Query('sort') String? sorting, + }); + + @POST('/api/diets/delete/{dietId}') + Future deleteDiet({ + @Path() required int dietId, + }); + + +} \ No newline at end of file diff --git a/lib/features/my_diets/services/my_diets_service.g.dart b/lib/features/my_diets/services/my_diets_service.g.dart new file mode 100644 index 00000000..fec66099 --- /dev/null +++ b/lib/features/my_diets/services/my_diets_service.g.dart @@ -0,0 +1,124 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'my_diets_service.dart'; + +// ************************************************************************** +// RetrofitGenerator +// ************************************************************************** + +// ignore_for_file: unnecessary_brace_in_string_interps,no_leading_underscores_for_local_identifiers,unused_element,unnecessary_string_interpolations + +class _MyDietsService implements MyDietsService { + _MyDietsService( + this._dio, { + this.baseUrl, + this.errorLogger, + }); + + final Dio _dio; + + String? baseUrl; + + final ParseErrorLogger? errorLogger; + + @override + Future getDiets({ + required int page, + String? query, + String? types, + int? duration, + String? sorting, + }) async { + final _extra = {}; + final queryParameters = { + r'page': page, + r'filters[name][$startsWith]': query, + r'filters[type][$contains]': types, + r'filters[duration][$eq]': duration, + r'sort': sorting, + }; + queryParameters.removeWhere((k, v) => v == null); + final _headers = {}; + const Map? _data = null; + final _options = _setStreamType(Options( + method: 'GET', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/api/diets/index', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + ))); + final _result = await _dio.fetch>(_options); + late DietsPagination _value; + try { + _value = DietsPagination.fromJson(_result.data!); + } on Object catch (e, s) { + errorLogger?.logError(e, s, _options); + rethrow; + } + return _value; + } + + @override + Future deleteDiet({required int dietId}) async { + final _extra = {}; + final queryParameters = {}; + final _headers = {}; + const Map? _data = null; + final _options = _setStreamType(Options( + method: 'POST', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/api/diets/delete/${dietId}', + 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/my_plans/presentation/cubit/my_plans_cubit.dart b/lib/features/my_plans/presentation/cubit/my_plans_cubit.dart new file mode 100644 index 00000000..eb61d560 --- /dev/null +++ b/lib/features/my_plans/presentation/cubit/my_plans_cubit.dart @@ -0,0 +1,207 @@ +import 'package:easy_debounce/easy_debounce.dart'; +import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_state.dart'; +import 'package:eatvio_app/features/my_plans/repositories/my_diets_repository.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class MyPlansCubit extends Cubit { + final MyPlansRepository myPlansRepository; + + int currentDraftPage = 1; + int currentActivePage = 1; + late int lastDraftPage; + late int lastPersonalPage; + late int lastActivePage; + bool isLoadingDraft = false; + bool isLoadingActive = false; + String? _activeQuery; + String? _draftQuery; + + static const List _durationValues = [null, ...durationValues]; + static const List _types = [null, ...planTypeValues]; + + MyPlansCubit({required this.myPlansRepository}) : super(MyPlansState()); + + void changePriceFilter(int minPrice, int maxPrice, {bool disableDebounce = false}) { + if (state.selectedStateIndex == 0) { + emit(state.copyWith(activeFilterState: state.activeFilterState.copyWith(priceFilter: (minPrice, maxPrice)), error: null)); + EasyDebounce.debounce( + 'loadPlans', + Duration(seconds: disableDebounce ? 0 : 1), + () => loadPlans( + query: _activeQuery, + ), + ); + } else if (state.selectedStateIndex == 1) { + emit(state.copyWith(draftFilterState: state.draftFilterState.copyWith(priceFilter: (minPrice, maxPrice)), error: null)); + EasyDebounce.debounce( + 'loadPlans', + Duration(seconds: disableDebounce ? 0 : 1), + () => loadPlans( + query: _draftQuery, + ), + ); + } + } + + void changeTypeFilter(int index) { + if (state.selectedStateIndex == 0) { + emit(state.copyWith(activeFilterState: state.activeFilterState.copyWith(typeIndex: index), error: null)); + EasyDebounce.debounce( + 'loadPlans', + Duration(seconds: 1), + () => loadPlans( + query: _activeQuery, + ), + ); + } else if (state.selectedStateIndex == 1) { + emit(state.copyWith(draftFilterState: state.draftFilterState.copyWith(typeIndex: index), error: null)); + EasyDebounce.debounce( + 'loadPlans', + Duration(seconds: 1), + () => loadPlans( + query: _draftQuery, + ), + ); + } + } + + void changeDurationFilter(int index) { + if (state.selectedStateIndex == 0) { + emit(state.copyWith(activeFilterState: state.activeFilterState.copyWith(durationIndex: index), error: null)); + EasyDebounce.debounce( + 'loadPlans', + Duration(seconds: 1), + () => loadPlans( + query: _activeQuery, + ), + ); + } else if (state.selectedStateIndex == 1) { + emit(state.copyWith(draftFilterState: state.draftFilterState.copyWith(durationIndex: index), error: null)); + EasyDebounce.debounce( + 'loadPlans', + Duration(seconds: 1), + () => loadPlans( + query: _draftQuery, + ), + ); + } + } + + void changeSelectedStateIndex(int index) { + emit(state.copyWith(selectedStateIndex: index)); + } + + Future loadPlans({ + String? query, + }) async { + switch (state.selectedStateIndex) { + case 0: + await _loadActivePlans(query: query); + break; + case 1: + await _loadDraftPlans(query: query); + break; + } + } + + Future loadAllPlans() async { + emit(state.copyWith(isLoading: true)); + await _loadActivePlans(); + await _loadDraftPlans(); + emit(state.copyWith(isLoading: false)); + } + + Future _loadActivePlans({String? query}) async { + try { + _activeQuery = query; + final type = _types[state.activeFilterState.typeIndex]; + final duration = _durationValues[state.activeFilterState.durationIndex]; + final minPrice = state.activeFilterState.priceFilter.$1; + final maxPrice = state.activeFilterState.priceFilter.$2; + final plansPagination = await myPlansRepository.getPlans( + query: query, + duration: duration, + maxPrice: maxPrice, + minPrice: minPrice, + types: type, + ); + lastActivePage = plansPagination.meta.lastPage; + final minPlanPrice = plansPagination.meta.totalMinPrice ?? 0; + final maxPlanPrice = plansPagination.meta.totalMaxPrice ?? 0; + if (state.activeFilterState.priceFilter.$1 == null || state.activeFilterState.priceFilter.$2 == null) { + emit( + state.copyWith( + activeFilterState: state.activeFilterState.copyWith(priceFilter: (minPlanPrice, maxPlanPrice)), + ), + ); + } + emit( + state.copyWith( + activePlans: plansPagination.data, + isLoading: isLoadingActive, + error: null, + minActivePlanPrice: minPlanPrice, + maxActivePlanPrice: maxPlanPrice, + ), + ); + } catch (e) { + emit( + state.copyWith(error: e.toString()), + ); + } + } + + Future _loadDraftPlans({String? query}) async { + try { + _draftQuery = query; + final type = _types[state.draftFilterState.typeIndex]; + final duration = _durationValues[state.draftFilterState.durationIndex]; + final minPrice = state.draftFilterState.priceFilter.$1; + final maxPrice = state.draftFilterState.priceFilter.$2; + final plansPagination = await myPlansRepository.getPlans( + query: query, + duration: duration, + maxPrice: maxPrice, + minPrice: minPrice, + types: type, + ); + lastDraftPage = plansPagination.meta.lastPage; + final minPlanPrice = plansPagination.meta.totalMinPrice ?? 0; + final maxPlanPrice = plansPagination.meta.totalMaxPrice ?? 0; + if (state.draftFilterState.priceFilter.$1 == null || state.draftFilterState.priceFilter.$2 == null) { + emit( + state.copyWith( + draftFilterState: state.draftFilterState.copyWith(priceFilter: (minPlanPrice, maxPlanPrice)), + ), + ); + } + emit( + state.copyWith( + draftPlans: plansPagination.data, + isLoading: isLoadingDraft, + error: null, + minDraftPlanPrice: minPlanPrice, + maxDraftPlanPrice: maxPlanPrice, + ), + ); + } catch (e) { + emit( + state.copyWith(error: e.toString()), + ); + } + } + + Future deletePlan(String slug) async { + try { + await myPlansRepository.deletePlan(slug); + await loadAllPlans(); + return true; + } catch (e) { + emit( + state.copyWith(error: e.toString()), + ); + return false; + } + } +} diff --git a/lib/features/my_plans/presentation/cubit/my_plans_state.dart b/lib/features/my_plans/presentation/cubit/my_plans_state.dart new file mode 100644 index 00000000..7434a8a9 --- /dev/null +++ b/lib/features/my_plans/presentation/cubit/my_plans_state.dart @@ -0,0 +1,36 @@ +import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart'; +import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_state.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'my_plans_state.freezed.dart'; + +@freezed +class MyPlansState with _$MyPlansState { + factory MyPlansState({ + @Default([]) List activePlans, + @Default([]) List draftPlans, + @Default(false) bool isLoading, + @Default(null) String? error, + @Default(0) int selectedStateIndex, + @Default(0) int minActivePlanPrice, + @Default(0) int maxActivePlanPrice, + @Default(0) int minDraftPlanPrice, + @Default(0) int maxDraftPlanPrice, + @Default( + const PlansFilterState( + priceFilter: (null, null), + durationIndex: 0, + typeIndex: 0, + ), + ) + PlansFilterState activeFilterState, + @Default( + const PlansFilterState( + priceFilter: (null, null), + durationIndex: 0, + typeIndex: 0, + ), + ) + PlansFilterState draftFilterState, + }) = _MyPlansState; +} diff --git a/lib/features/my_plans/presentation/cubit/my_plans_state.freezed.dart b/lib/features/my_plans/presentation/cubit/my_plans_state.freezed.dart new file mode 100644 index 00000000..ea5d960b --- /dev/null +++ b/lib/features/my_plans/presentation/cubit/my_plans_state.freezed.dart @@ -0,0 +1,424 @@ +// 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 'my_plans_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 _$MyPlansState { + List get activePlans => throw _privateConstructorUsedError; + List get draftPlans => throw _privateConstructorUsedError; + bool get isLoading => throw _privateConstructorUsedError; + String? get error => throw _privateConstructorUsedError; + int get selectedStateIndex => throw _privateConstructorUsedError; + int get minActivePlanPrice => throw _privateConstructorUsedError; + int get maxActivePlanPrice => throw _privateConstructorUsedError; + int get minDraftPlanPrice => throw _privateConstructorUsedError; + int get maxDraftPlanPrice => throw _privateConstructorUsedError; + PlansFilterState get activeFilterState => throw _privateConstructorUsedError; + PlansFilterState get draftFilterState => throw _privateConstructorUsedError; + + /// Create a copy of MyPlansState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $MyPlansStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $MyPlansStateCopyWith<$Res> { + factory $MyPlansStateCopyWith( + MyPlansState value, $Res Function(MyPlansState) then) = + _$MyPlansStateCopyWithImpl<$Res, MyPlansState>; + @useResult + $Res call( + {List activePlans, + List draftPlans, + bool isLoading, + String? error, + int selectedStateIndex, + int minActivePlanPrice, + int maxActivePlanPrice, + int minDraftPlanPrice, + int maxDraftPlanPrice, + PlansFilterState activeFilterState, + PlansFilterState draftFilterState}); + + $PlansFilterStateCopyWith<$Res> get activeFilterState; + $PlansFilterStateCopyWith<$Res> get draftFilterState; +} + +/// @nodoc +class _$MyPlansStateCopyWithImpl<$Res, $Val extends MyPlansState> + implements $MyPlansStateCopyWith<$Res> { + _$MyPlansStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of MyPlansState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? activePlans = null, + Object? draftPlans = null, + Object? isLoading = null, + Object? error = freezed, + Object? selectedStateIndex = null, + Object? minActivePlanPrice = null, + Object? maxActivePlanPrice = null, + Object? minDraftPlanPrice = null, + Object? maxDraftPlanPrice = null, + Object? activeFilterState = null, + Object? draftFilterState = null, + }) { + return _then(_value.copyWith( + activePlans: null == activePlans + ? _value.activePlans + : activePlans // ignore: cast_nullable_to_non_nullable + as List, + draftPlans: null == draftPlans + ? _value.draftPlans + : draftPlans // ignore: cast_nullable_to_non_nullable + as List, + isLoading: null == isLoading + ? _value.isLoading + : isLoading // ignore: cast_nullable_to_non_nullable + as bool, + error: freezed == error + ? _value.error + : error // ignore: cast_nullable_to_non_nullable + as String?, + selectedStateIndex: null == selectedStateIndex + ? _value.selectedStateIndex + : selectedStateIndex // ignore: cast_nullable_to_non_nullable + as int, + minActivePlanPrice: null == minActivePlanPrice + ? _value.minActivePlanPrice + : minActivePlanPrice // ignore: cast_nullable_to_non_nullable + as int, + maxActivePlanPrice: null == maxActivePlanPrice + ? _value.maxActivePlanPrice + : maxActivePlanPrice // ignore: cast_nullable_to_non_nullable + as int, + minDraftPlanPrice: null == minDraftPlanPrice + ? _value.minDraftPlanPrice + : minDraftPlanPrice // ignore: cast_nullable_to_non_nullable + as int, + maxDraftPlanPrice: null == maxDraftPlanPrice + ? _value.maxDraftPlanPrice + : maxDraftPlanPrice // ignore: cast_nullable_to_non_nullable + as int, + activeFilterState: null == activeFilterState + ? _value.activeFilterState + : activeFilterState // ignore: cast_nullable_to_non_nullable + as PlansFilterState, + draftFilterState: null == draftFilterState + ? _value.draftFilterState + : draftFilterState // ignore: cast_nullable_to_non_nullable + as PlansFilterState, + ) as $Val); + } + + /// Create a copy of MyPlansState + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $PlansFilterStateCopyWith<$Res> get activeFilterState { + return $PlansFilterStateCopyWith<$Res>(_value.activeFilterState, (value) { + return _then(_value.copyWith(activeFilterState: value) as $Val); + }); + } + + /// Create a copy of MyPlansState + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $PlansFilterStateCopyWith<$Res> get draftFilterState { + return $PlansFilterStateCopyWith<$Res>(_value.draftFilterState, (value) { + return _then(_value.copyWith(draftFilterState: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$MyPlansStateImplCopyWith<$Res> + implements $MyPlansStateCopyWith<$Res> { + factory _$$MyPlansStateImplCopyWith( + _$MyPlansStateImpl value, $Res Function(_$MyPlansStateImpl) then) = + __$$MyPlansStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {List activePlans, + List draftPlans, + bool isLoading, + String? error, + int selectedStateIndex, + int minActivePlanPrice, + int maxActivePlanPrice, + int minDraftPlanPrice, + int maxDraftPlanPrice, + PlansFilterState activeFilterState, + PlansFilterState draftFilterState}); + + @override + $PlansFilterStateCopyWith<$Res> get activeFilterState; + @override + $PlansFilterStateCopyWith<$Res> get draftFilterState; +} + +/// @nodoc +class __$$MyPlansStateImplCopyWithImpl<$Res> + extends _$MyPlansStateCopyWithImpl<$Res, _$MyPlansStateImpl> + implements _$$MyPlansStateImplCopyWith<$Res> { + __$$MyPlansStateImplCopyWithImpl( + _$MyPlansStateImpl _value, $Res Function(_$MyPlansStateImpl) _then) + : super(_value, _then); + + /// Create a copy of MyPlansState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? activePlans = null, + Object? draftPlans = null, + Object? isLoading = null, + Object? error = freezed, + Object? selectedStateIndex = null, + Object? minActivePlanPrice = null, + Object? maxActivePlanPrice = null, + Object? minDraftPlanPrice = null, + Object? maxDraftPlanPrice = null, + Object? activeFilterState = null, + Object? draftFilterState = null, + }) { + return _then(_$MyPlansStateImpl( + activePlans: null == activePlans + ? _value._activePlans + : activePlans // ignore: cast_nullable_to_non_nullable + as List, + draftPlans: null == draftPlans + ? _value._draftPlans + : draftPlans // ignore: cast_nullable_to_non_nullable + as List, + isLoading: null == isLoading + ? _value.isLoading + : isLoading // ignore: cast_nullable_to_non_nullable + as bool, + error: freezed == error + ? _value.error + : error // ignore: cast_nullable_to_non_nullable + as String?, + selectedStateIndex: null == selectedStateIndex + ? _value.selectedStateIndex + : selectedStateIndex // ignore: cast_nullable_to_non_nullable + as int, + minActivePlanPrice: null == minActivePlanPrice + ? _value.minActivePlanPrice + : minActivePlanPrice // ignore: cast_nullable_to_non_nullable + as int, + maxActivePlanPrice: null == maxActivePlanPrice + ? _value.maxActivePlanPrice + : maxActivePlanPrice // ignore: cast_nullable_to_non_nullable + as int, + minDraftPlanPrice: null == minDraftPlanPrice + ? _value.minDraftPlanPrice + : minDraftPlanPrice // ignore: cast_nullable_to_non_nullable + as int, + maxDraftPlanPrice: null == maxDraftPlanPrice + ? _value.maxDraftPlanPrice + : maxDraftPlanPrice // ignore: cast_nullable_to_non_nullable + as int, + activeFilterState: null == activeFilterState + ? _value.activeFilterState + : activeFilterState // ignore: cast_nullable_to_non_nullable + as PlansFilterState, + draftFilterState: null == draftFilterState + ? _value.draftFilterState + : draftFilterState // ignore: cast_nullable_to_non_nullable + as PlansFilterState, + )); + } +} + +/// @nodoc + +class _$MyPlansStateImpl implements _MyPlansState { + _$MyPlansStateImpl( + {final List activePlans = const [], + final List draftPlans = const [], + this.isLoading = false, + this.error = null, + this.selectedStateIndex = 0, + this.minActivePlanPrice = 0, + this.maxActivePlanPrice = 0, + this.minDraftPlanPrice = 0, + this.maxDraftPlanPrice = 0, + this.activeFilterState = const PlansFilterState( + priceFilter: (null, null), durationIndex: 0, typeIndex: 0), + this.draftFilterState = const PlansFilterState( + priceFilter: (null, null), durationIndex: 0, typeIndex: 0)}) + : _activePlans = activePlans, + _draftPlans = draftPlans; + + final List _activePlans; + @override + @JsonKey() + List get activePlans { + if (_activePlans is EqualUnmodifiableListView) return _activePlans; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_activePlans); + } + + final List _draftPlans; + @override + @JsonKey() + List get draftPlans { + if (_draftPlans is EqualUnmodifiableListView) return _draftPlans; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_draftPlans); + } + + @override + @JsonKey() + final bool isLoading; + @override + @JsonKey() + final String? error; + @override + @JsonKey() + final int selectedStateIndex; + @override + @JsonKey() + final int minActivePlanPrice; + @override + @JsonKey() + final int maxActivePlanPrice; + @override + @JsonKey() + final int minDraftPlanPrice; + @override + @JsonKey() + final int maxDraftPlanPrice; + @override + @JsonKey() + final PlansFilterState activeFilterState; + @override + @JsonKey() + final PlansFilterState draftFilterState; + + @override + String toString() { + return 'MyPlansState(activePlans: $activePlans, draftPlans: $draftPlans, isLoading: $isLoading, error: $error, selectedStateIndex: $selectedStateIndex, minActivePlanPrice: $minActivePlanPrice, maxActivePlanPrice: $maxActivePlanPrice, minDraftPlanPrice: $minDraftPlanPrice, maxDraftPlanPrice: $maxDraftPlanPrice, activeFilterState: $activeFilterState, draftFilterState: $draftFilterState)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$MyPlansStateImpl && + const DeepCollectionEquality() + .equals(other._activePlans, _activePlans) && + const DeepCollectionEquality() + .equals(other._draftPlans, _draftPlans) && + (identical(other.isLoading, isLoading) || + other.isLoading == isLoading) && + (identical(other.error, error) || other.error == error) && + (identical(other.selectedStateIndex, selectedStateIndex) || + other.selectedStateIndex == selectedStateIndex) && + (identical(other.minActivePlanPrice, minActivePlanPrice) || + other.minActivePlanPrice == minActivePlanPrice) && + (identical(other.maxActivePlanPrice, maxActivePlanPrice) || + other.maxActivePlanPrice == maxActivePlanPrice) && + (identical(other.minDraftPlanPrice, minDraftPlanPrice) || + other.minDraftPlanPrice == minDraftPlanPrice) && + (identical(other.maxDraftPlanPrice, maxDraftPlanPrice) || + other.maxDraftPlanPrice == maxDraftPlanPrice) && + (identical(other.activeFilterState, activeFilterState) || + other.activeFilterState == activeFilterState) && + (identical(other.draftFilterState, draftFilterState) || + other.draftFilterState == draftFilterState)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + const DeepCollectionEquality().hash(_activePlans), + const DeepCollectionEquality().hash(_draftPlans), + isLoading, + error, + selectedStateIndex, + minActivePlanPrice, + maxActivePlanPrice, + minDraftPlanPrice, + maxDraftPlanPrice, + activeFilterState, + draftFilterState); + + /// Create a copy of MyPlansState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$MyPlansStateImplCopyWith<_$MyPlansStateImpl> get copyWith => + __$$MyPlansStateImplCopyWithImpl<_$MyPlansStateImpl>(this, _$identity); +} + +abstract class _MyPlansState implements MyPlansState { + factory _MyPlansState( + {final List activePlans, + final List draftPlans, + final bool isLoading, + final String? error, + final int selectedStateIndex, + final int minActivePlanPrice, + final int maxActivePlanPrice, + final int minDraftPlanPrice, + final int maxDraftPlanPrice, + final PlansFilterState activeFilterState, + final PlansFilterState draftFilterState}) = _$MyPlansStateImpl; + + @override + List get activePlans; + @override + List get draftPlans; + @override + bool get isLoading; + @override + String? get error; + @override + int get selectedStateIndex; + @override + int get minActivePlanPrice; + @override + int get maxActivePlanPrice; + @override + int get minDraftPlanPrice; + @override + int get maxDraftPlanPrice; + @override + PlansFilterState get activeFilterState; + @override + PlansFilterState get draftFilterState; + + /// Create a copy of MyPlansState + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$MyPlansStateImplCopyWith<_$MyPlansStateImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/features/my_plans/presentation/my_plans_screen.dart b/lib/features/my_plans/presentation/my_plans_screen.dart new file mode 100644 index 00000000..efa423e0 --- /dev/null +++ b/lib/features/my_plans/presentation/my_plans_screen.dart @@ -0,0 +1,189 @@ +import 'package:easy_debounce/easy_debounce.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/widgets/add_item_button.dart'; +import 'package:eatvio_app/core/widgets/custom_search_bar.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/widgets/filter_badge.dart'; +import 'package:eatvio_app/features/feed/presentation/widgets/plans_filter_bottom_sheet.dart'; +import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_cubit.dart'; +import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_state.dart'; +import 'package:eatvio_app/features/my_plans/presentation/widgets/my_plan_widget.dart'; +import 'package:eatvio_app/features/my_plans/presentation/widgets/my_plans_tab.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 MyPlansScreen extends StatelessWidget { + const MyPlansScreen({super.key}); + static const name = 'myPlansScreen'; + static const path = '/my-plans-screen'; + @override + Widget build(BuildContext context) { + return BlocProvider(create: (context) => sl()..loadAllPlans(), child: MyPlansScreenContent()); + } +} + +class MyPlansScreenContent extends StatelessWidget { + MyPlansScreenContent({super.key}); + + final activePlansScrollController = ScrollController(); + final draftPlansScrollController = ScrollController(); + + final activePlansSearchController = SearchController(); + final draftPlansSearchController = SearchController(); + + @override + Widget build(BuildContext context) { + final s = S.of(context); + return Scaffold( + backgroundColor: appTheme.gray100, + appBar: AppBar( + centerTitle: true, + title: Text( + s.my_plans, + maxLines: 2, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + softWrap: true, + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), + ), + ), + body: BlocBuilder( + buildWhen: (previous, current) => previous.selectedStateIndex != current.selectedStateIndex, + builder: (context, state) { + return NestedScrollView( + controller: switch (state.selectedStateIndex) { + 1 => draftPlansScrollController, + _ => activePlansScrollController, + }, + headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { + return [ + SliverAppBar( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(bottom: Radius.circular(12)), + ), + backgroundColor: appTheme.gray100, + foregroundColor: appTheme.gray100, + automaticallyImplyLeading: false, + stretchTriggerOffset: 1, + floating: true, + primary: false, + snap: true, + scrolledUnderElevation: 0, + bottom: PreferredSize( + preferredSize: Size( + MediaQuery.sizeOf(context).width, + 64, + ), + child: Padding( + padding: const EdgeInsets.only(bottom: 0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(height: 8), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: MyPlansTab(), + ), + SizedBox(height: 8), + Row( + children: [ + SizedBox(width: 4), + Expanded( + child: CustomSearchBar( + enableIcon: false, + backgroundColor: appTheme.whiteA700, + borderRadius: 100, + searchController: switch (state.selectedStateIndex) { + 1 => draftPlansSearchController, + _ => activePlansSearchController, + }, + onChanged: (query) { + EasyDebounce.debounce( + 'searchPlans', + Duration(milliseconds: 500), + () { + context.read().loadPlans(query: query == '' ? null : query); + }, + ); + }, + ), + ), + SizedBox(width: 12), + AddItemButton( + onTap: () => GoRouter.of(context).pushNamed(CreatePlanScreen.name), + ), + SizedBox(width: 4), + ], + ), + SizedBox(height: 16), + ], + ), + ), + ), + ), + ]; + }, + body: RefreshIndicator( + onRefresh: () => context.read().loadPlans(), + child: BlocBuilder( + builder: (BuildContext context, MyPlansState state) { + if (state.isLoading) { + return Scaffold( + backgroundColor: Colors.grey.shade200, + body: const Loader(), + ); + } else if (state.error != null) { + return Scaffold( + backgroundColor: Colors.grey.shade200, + body: Padding( + padding: const EdgeInsets.all(16), + child: Center( + child: Text(state.error ?? ''), + ), + ), + ); + } else { + final plans = switch (state.selectedStateIndex) { + 1 => state.draftPlans, + _ => state.activePlans, + }; + return Stack( + children: [ + SizedBox.expand(), + ListView.separated( + shrinkWrap: true, + physics: BouncingScrollPhysics(), + itemBuilder: (context, index) { + return MyPlanWidget(plan: plans[index]); + }, + separatorBuilder: (context, index) { + return SizedBox(height: 20); + }, + itemCount: plans.length, + ), + Positioned( + right: 0, + bottom: 74, + child: FilterBadge( + filterBottomSheet: PlansFilterBottomSheet( + myPlansCubit: context.read(), + ), + ), + ), + ], + ); + } + }, + ), + ), + ); + }, + ), + ); + } +} diff --git a/lib/features/my_plans/presentation/widgets/my_plan_widget.dart b/lib/features/my_plans/presentation/widgets/my_plan_widget.dart new file mode 100644 index 00000000..1040a207 --- /dev/null +++ b/lib/features/my_plans/presentation/widgets/my_plan_widget.dart @@ -0,0 +1,259 @@ +import 'package:cached_network_image/cached_network_image.dart'; +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/widgets/bottom_sheet_handler_bar.dart'; +import 'package:eatvio_app/core/widgets/bottom_sheet_with_blur.dart'; +import 'package:eatvio_app/core/widgets/custom_outlined_button.dart'; +import 'package:eatvio_app/core/widgets/info_row.dart'; +import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart'; +import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_cubit.dart'; +import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_state.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_html/flutter_html.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:go_router/go_router.dart'; +import 'package:share_plus/share_plus.dart'; + +class MyPlanWidget extends StatefulWidget { + final PlanModel plan; + const MyPlanWidget({ + super.key, + required this.plan, + }); + + @override + State createState() => _MyPlanWidgetState(); +} + +class _MyPlanWidgetState extends State { + @override + Widget build(BuildContext context) { + final s = S.of(context); + return DecoratedBox( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: Colors.white, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Stack( + children: [ + InkWell( + onTap: () => GoRouter.of(context).pushNamed('aboutPlanScreen', extra: {'plan': widget.plan, 'navigateToComments': false}), + child: ClipRRect( + borderRadius: BorderRadius.vertical(top: Radius.circular(12)), + child: CachedNetworkImage( + imageUrl: baseUrl + '/storage/' + widget.plan.image, + fit: BoxFit.fitWidth, + width: MediaQuery.of(context).size.width, + ), + ), + ), + Positioned( + bottom: 16, + child: SizedBox( + width: MediaQuery.sizeOf(context).width, + child: InfoRow( + plan: widget.plan, + itemsBackgroundColor: appTheme.whiteA700, + disableDescriptionOfTypes: true, + ), + ), + ), + ], + ), + SizedBox(height: 12), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Text( + widget.plan.name, + style: TextStyles.textMSemibold.copyWith(color: appTheme.black), + ), + ), + if (widget.plan.details.isNotEmpty) ...[ + SizedBox( + height: 12, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Html( + data: widget.plan.details.length > 200 ? widget.plan.details.substring(0, 200) + '...' : widget.plan.details, + style: { + "*": Style( + margin: Margins.zero, + color: appTheme.black, + fontSize: FontSize(TextStyles.textSMedium.fontSize ?? 14), + fontWeight: TextStyles.textSMedium.fontWeight ?? FontWeight.w400, + ), + }, + ), + ), + ], + ], + ), + SizedBox(height: 16), + Padding( + padding: EdgeInsets.only(left: 16), + child: Text( + '${widget.plan.packetsCount == 1 ? widget.plan.packet1Options?.price : '${s.from} ${widget.plan.minPriceOfOptions.toStringAsFixed(0)}'}₽', + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.greenA700), + ), + ), + SizedBox(height: 20), + BlocBuilder( + builder: (context, state) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + children: [ + Row( + children: [ + Expanded( + child: CustomOutlinedButton( + text: switch (state.selectedStateIndex) { + 1 => s.publish, + _ => s.edit, + }, + height: 52, + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), + buttonStyle: ButtonStyle( + shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))), + backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), + side: WidgetStatePropertyAll(BorderSide.none), + ), + ), + ), + if (state.selectedStateIndex == 0) ...[ + SizedBox(width: 6), + CustomOutlinedButton( + onPressed: () async { + await Share.shareUri( + Uri.parse( + baseUrl + '/ru/plan/' + widget.plan.id.toString(), + ), + ); + }, + height: 52, + width: 52, + leftIcon: SvgPicture.asset( + 'assets/icons/share2.svg', + width: 20, + height: 20, + colorFilter: ColorFilter.mode(appTheme.whiteA700, BlendMode.srcIn), + ), + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.red), + buttonStyle: ButtonStyle( + shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))), + backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), + side: WidgetStatePropertyAll(BorderSide.none), + ), + text: '', + ), + ], + ], + ), + SizedBox(height: 8), + Row( + children: [ + Expanded( + child: CustomOutlinedButton( + text: switch (state.selectedStateIndex) { + 0 => s.removeFromActive, + _ => s.edit, + }, + height: 52, + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.black), + buttonStyle: ButtonStyle( + shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))), + backgroundColor: WidgetStatePropertyAll(appTheme.lightGray0), + side: WidgetStatePropertyAll(BorderSide.none), + ), + ), + ), + SizedBox(width: 6), + CustomOutlinedButton( + onPressed: () async { + final result = await context.read().deletePlan(widget.plan.slug); + if (result) + Future.delayed(Duration(seconds: 3), () { + showBottomSheetWithBlur( + context: context, + child: Column( + children: [ + SizedBox( + height: 20, + ), + BottomSheetHandlerBar(), + SizedBox( + height: 20, + ), + Text( + '${widget.plan.name}', + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), + ), + Text( + s.deleted, + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), + ), + SizedBox( + height: 20, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: CustomOutlinedButton( + onPressed: () { + context.pop(); + }, + text: 'Хорошо', + height: 52, + buttonTextStyle: TextStyles.textSSemibold.copyWith(color: appTheme.whiteA700), + buttonStyle: ButtonStyle( + side: WidgetStatePropertyAll(BorderSide.none), + backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), + ), + ), + ), + SizedBox( + height: 20, + ), + ], + ), + ); + }); + }, + height: 52, + width: 52, + leftIcon: SvgPicture.asset( + 'assets/icons/trash_can.svg', + width: 20, + height: 20, + ), + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.red), + buttonStyle: ButtonStyle( + shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))), + backgroundColor: WidgetStatePropertyAll(appTheme.lightGray0), + side: WidgetStatePropertyAll(BorderSide.none), + ), + text: '', + ), + ], + ), + SizedBox(height: 8), + ], + ), + ); + }, + ), + ], + ), + ); + } +} diff --git a/lib/features/my_plans/presentation/widgets/my_plans_tab.dart b/lib/features/my_plans/presentation/widgets/my_plans_tab.dart new file mode 100644 index 00000000..c681efd1 --- /dev/null +++ b/lib/features/my_plans/presentation/widgets/my_plans_tab.dart @@ -0,0 +1,71 @@ +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/animated_option_button.dart'; +import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_cubit.dart'; +import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_state.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class MyPlansTab extends StatefulWidget { + const MyPlansTab({super.key, this.additionalOnTap}); + final void Function()? additionalOnTap; + @override + State createState() => _MyPlansTabState(); +} + +class _MyPlansTabState extends State { + final List tabTitles = []; + @override + void didChangeDependencies() { + final s = S.of(context); + if (tabTitles.isEmpty) { + tabTitles.add(s.published); + tabTitles.add(s.drafted); + } + super.didChangeDependencies(); + } + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return Align( + alignment: Alignment.centerLeft, + child: Row( + children: [ + Expanded( + child: Container( + height: 48, + decoration: BoxDecoration( + color: appTheme.whiteA700, + borderRadius: BorderRadius.circular(100), + ), + padding: const EdgeInsets.all(4), + child: Row( + children: [ + ...List.generate( + 2, + (index) => Expanded( + child: AnimatedOptionButton( + textStyle: TextStyles.textSMedium, + isSelected: state.selectedStateIndex == index, + text: tabTitles[index], + onTap: () { + widget.additionalOnTap?.call(); + context.read().changeSelectedStateIndex(index); + }, + ), + ), + ), + ], + ), + ), + ), + ], + ), + ); + }, + ); + } +} diff --git a/lib/features/my_plans/repositories/my_diets_repository.dart b/lib/features/my_plans/repositories/my_diets_repository.dart new file mode 100644 index 00000000..a28c88cd --- /dev/null +++ b/lib/features/my_plans/repositories/my_diets_repository.dart @@ -0,0 +1,39 @@ +import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart'; +import 'package:eatvio_app/features/my_plans/services/my_plans_service.dart'; + +class MyPlansRepository { + final MyPlansService myPlansService; + + MyPlansRepository({required this.myPlansService}); + + Future deletePlan(String slug) async { + try { + await myPlansService.deletePlan(slug: slug); + } catch (e) { + rethrow; + } + } + + Future getPlans({ + int page = 1, + String? query, + String? types, + int? duration, + int? minPrice, + int? maxPrice, + }) async { + try { + return (await myPlansService.getPlans( + page: page, + query: query, + types: types, + duration: duration, + minPrice: minPrice, + maxPrice: maxPrice, + )); + } catch (e) { + rethrow; + } + } + +} diff --git a/lib/features/my_plans/services/my_plans_service.dart b/lib/features/my_plans/services/my_plans_service.dart new file mode 100644 index 00000000..ee81e55e --- /dev/null +++ b/lib/features/my_plans/services/my_plans_service.dart @@ -0,0 +1,28 @@ +import 'package:dio/dio.dart'; +import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart'; +import 'package:retrofit/retrofit.dart'; + +part 'my_plans_service.g.dart'; + +@RestApi() +abstract class MyPlansService { + + factory MyPlansService(Dio dio, {String baseUrl}) = _MyPlansService; + + @GET('/api/plans/index') + Future getPlans({ + @Query('page') required int page, + @Query('filters[name][\$startsWith]') String? query, + @Query('filters[type][\$contains]') String? types, + @Query('filters[duration][\$eq]') int? duration, + @Query('filters[min_price_of_options][\$between][0]') int? minPrice, + @Query('filters[min_price_of_options][\$between][1]') int? maxPrice, + }); + + @POST('/api/plans/delete/{slug}') + Future deletePlan({ + @Path() required String slug, + }); + + +} \ No newline at end of file diff --git a/lib/features/my_plans/services/my_plans_service.g.dart b/lib/features/my_plans/services/my_plans_service.g.dart new file mode 100644 index 00000000..dd70281f --- /dev/null +++ b/lib/features/my_plans/services/my_plans_service.g.dart @@ -0,0 +1,126 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'my_plans_service.dart'; + +// ************************************************************************** +// RetrofitGenerator +// ************************************************************************** + +// ignore_for_file: unnecessary_brace_in_string_interps,no_leading_underscores_for_local_identifiers,unused_element,unnecessary_string_interpolations + +class _MyPlansService implements MyPlansService { + _MyPlansService( + this._dio, { + this.baseUrl, + this.errorLogger, + }); + + final Dio _dio; + + String? baseUrl; + + final ParseErrorLogger? errorLogger; + + @override + Future getPlans({ + required int page, + String? query, + String? types, + int? duration, + int? minPrice, + int? maxPrice, + }) async { + final _extra = {}; + final queryParameters = { + r'page': page, + r'filters[name][$startsWith]': query, + r'filters[type][$contains]': types, + r'filters[duration][$eq]': duration, + r'filters[min_price_of_options][$between][0]': minPrice, + r'filters[min_price_of_options][$between][1]': maxPrice, + }; + queryParameters.removeWhere((k, v) => v == null); + final _headers = {}; + const Map? _data = null; + final _options = _setStreamType(Options( + method: 'GET', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/api/plans/index', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + ))); + final _result = await _dio.fetch>(_options); + late PlanPaginationModel _value; + try { + _value = PlanPaginationModel.fromJson(_result.data!); + } on Object catch (e, s) { + errorLogger?.logError(e, s, _options); + rethrow; + } + return _value; + } + + @override + Future deletePlan({required String slug}) async { + final _extra = {}; + final queryParameters = {}; + final _headers = {}; + const Map? _data = null; + final _options = _setStreamType(Options( + method: 'POST', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/api/plans/delete/${slug}', + 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/plans/presentation/cubit/about_plan_cubit.dart b/lib/features/plans/presentation/cubit/about_plan_cubit.dart index b22bcfaf..ad60723a 100644 --- a/lib/features/plans/presentation/cubit/about_plan_cubit.dart +++ b/lib/features/plans/presentation/cubit/about_plan_cubit.dart @@ -42,15 +42,19 @@ class AboutPlanCubit extends Cubit { emit(state.copyWith(chosenOptionIndex: index)); } - Future likePlan({required int index, required String slug}) async { + Future likePlan({required int? index, required String slug}) async { final currentPlan = state.planModel; if (currentPlan == null) return; if (currentPlan.wasLikedByMe) { emit(state.copyWith(planModel: currentPlan.copyWith(wasLikedByMe: false, totalLikes: currentPlan.totalLikes - 1), error: null)); - planRepository.emitLikesStateToFeed(LikesState(wasLikedByMe: false, totalLikes: currentPlan.totalLikes - 1, index: index)); + if (index != null) { + planRepository.emitLikesStateToFeed(LikesState(wasLikedByMe: false, totalLikes: currentPlan.totalLikes - 1, index: index)); + } } else { emit(state.copyWith(planModel: currentPlan.copyWith(wasLikedByMe: true, totalLikes: currentPlan.totalLikes + 1), error: null)); - planRepository.emitLikesStateToFeed(LikesState(wasLikedByMe: true, totalLikes: currentPlan.totalLikes + 1, index: index)); + if (index != null) { + planRepository.emitLikesStateToFeed(LikesState(wasLikedByMe: true, totalLikes: currentPlan.totalLikes + 1, index: index)); + } } EasyDebounce.debounce( @@ -61,19 +65,23 @@ class AboutPlanCubit extends Cubit { await planRepository.likePlan(slug); } catch (e) { log(e.toString()); - planRepository.emitLikesStateToFeed(LikesState(wasLikedByMe: currentPlan.wasLikedByMe, totalLikes: currentPlan.totalLikes, index: index)); + if (index != null) { + planRepository.emitLikesStateToFeed(LikesState(wasLikedByMe: currentPlan.wasLikedByMe, totalLikes: currentPlan.totalLikes, index: index)); + } emit(state.copyWith(planModel: currentPlan, error: null)); } }, ); } - Future addToFavorite({required String slug, required int index}) async { + Future addToFavorite({required String slug, required int? index}) async { final currentPlan = state.planModel; if (currentPlan == null) return; emit(state.copyWith(planModel: currentPlan.copyWith(isFavorite: true))); - planRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: true, index: index)); + if (index != null) { + planRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: true, index: index)); + } EasyDebounce.debounce( 'add_or_remove_plan_to_favorites', Duration(seconds: 1), @@ -83,19 +91,23 @@ class AboutPlanCubit extends Cubit { } catch (e) { log(e.toString()); showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время'); - planRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: currentPlan.isFavorite, index: index)); + if (index != null) { + planRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: currentPlan.isFavorite, index: index)); + } emit(state.copyWith(planModel: currentPlan, error: null)); } }, ); } - Future removeFromFavorite({required String slug, required int index}) async { + Future removeFromFavorite({required String slug, required int? index}) async { final currentPlan = state.planModel; if (currentPlan == null) return; emit(state.copyWith(planModel: currentPlan.copyWith(isFavorite: false))); - planRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: false, index: index)); + if (index != null) { + planRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: false, index: index)); + } EasyDebounce.debounce( 'add_or_remove_plan_to_favorites', Duration(seconds: 1), @@ -105,7 +117,9 @@ class AboutPlanCubit extends Cubit { } catch (e) { log(e.toString()); showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время'); - planRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: currentPlan.isFavorite, index: currentPlan.id)); + if (index != null) { + planRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: currentPlan.isFavorite, index: index)); + } emit(state.copyWith(planModel: currentPlan, error: null)); } }, diff --git a/lib/features/plans/presentation/pages/about_plan_screen.dart b/lib/features/plans/presentation/pages/about_plan_screen.dart index beed1eee..b76cce48 100644 --- a/lib/features/plans/presentation/pages/about_plan_screen.dart +++ b/lib/features/plans/presentation/pages/about_plan_screen.dart @@ -5,7 +5,7 @@ import 'package:eatvio_app/core/const.dart'; 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/diet_info_row.dart'; +import 'package:eatvio_app/core/widgets/info_row.dart'; import 'package:eatvio_app/core/widgets/loader.dart'; import 'package:eatvio_app/features/diets/presentation/widgets/author_card.dart'; import 'package:eatvio_app/features/diets/presentation/widgets/expandable_text.dart'; @@ -34,7 +34,7 @@ class AboutPlanScreen extends StatefulWidget { AboutPlanScreen({super.key, required this.plan, required this.navigateToComments, required this.index}); final PlanModel plan; final bool navigateToComments; - final int index; + final int? index; @override State createState() => _AboutPlanScreenState(); } diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart index 64dc4d01..11c0d855 100644 --- a/lib/generated/intl/messages_en.dart +++ b/lib/generated/intl/messages_en.dart @@ -88,7 +88,9 @@ class MessageLookup extends MessageLookupByLibrary { "date_of_birth": MessageLookupByLibrary.simpleMessage("Дата рождения"), "day": MessageLookupByLibrary.simpleMessage("День"), "days": MessageLookupByLibrary.simpleMessage("дней"), + "deleted": MessageLookupByLibrary.simpleMessage("Удалена"), "diary": MessageLookupByLibrary.simpleMessage("Дневник"), + "diet": MessageLookupByLibrary.simpleMessage("Диета"), "diet_author": MessageLookupByLibrary.simpleMessage("Автор диеты"), "diet_plan": MessageLookupByLibrary.simpleMessage("План диеты"), "diet_state": MessageLookupByLibrary.simpleMessage("Статус диеты"), @@ -102,6 +104,7 @@ class MessageLookup extends MessageLookupByLibrary { "down_pressure": MessageLookupByLibrary.simpleMessage("Нижние показатели давления:"), "draft": MessageLookupByLibrary.simpleMessage("Черновик"), + "drafted": MessageLookupByLibrary.simpleMessage("Черновики"), "eating_behavior": MessageLookupByLibrary.simpleMessage("Пищевое поведение"), "eating_behavior_type": MessageLookupByLibrary.simpleMessage( @@ -109,6 +112,7 @@ class MessageLookup extends MessageLookupByLibrary { "eating_behavior_type_desc": MessageLookupByLibrary.simpleMessage( "Формирование здорового пищевого поведения — это процесс, который включает в себя изменение привычек и отношения к еде. Он направлен на то, чтобы помочь людям научиться правильно питаться и поддерживать здоровый образ жизни.Основные принципы формирования здорового пищевого поведения: \n● Разнообразие продуктов; \n● Сбалансированный рацион; \n● Питьевой режим; \n● Ограничение вредных продуктов; \n● Регулярные приёмы пищи; \n● Умеренность;Осознанное питание; \n● Физическая активность;"), "economy": MessageLookupByLibrary.simpleMessage("Эконом"), + "edit": MessageLookupByLibrary.simpleMessage("Редактировать"), "email": MessageLookupByLibrary.simpleMessage("Элетронная почта"), "email_empty": MessageLookupByLibrary.simpleMessage( "Необходимо ввести адрес электронной почты"), @@ -142,6 +146,7 @@ class MessageLookup extends MessageLookupByLibrary { "fit_type_desc": MessageLookupByLibrary.simpleMessage( "План питания для поддержания формы сбалансированный и разнообразный, чтобы обеспечить организм всеми необходимыми питательными веществами. Общие принципы: \n● Дробное питание; \n● Разнообразие продуктов; \n● Контроль порций; \n● Питьевой режим; \n● Ограничение вредных продуктов;"), "fitness": MessageLookupByLibrary.simpleMessage("Поддержка формы"), + "from": MessageLookupByLibrary.simpleMessage("от"), "from_a_to_z": MessageLookupByLibrary.simpleMessage("Название: А-Я"), "from_z_to_a": MessageLookupByLibrary.simpleMessage("Название: Я-А"), "gallery": MessageLookupByLibrary.simpleMessage("Галерея"), @@ -187,6 +192,8 @@ class MessageLookup extends MessageLookupByLibrary { "msg_something_went_wrong": MessageLookupByLibrary.simpleMessage("Что то пошло не так!"), "my_blog": MessageLookupByLibrary.simpleMessage("Мой блог"), + "my_diets": MessageLookupByLibrary.simpleMessage("Мои диеты"), + "my_plans": MessageLookupByLibrary.simpleMessage("Мои планы"), "my_settings": MessageLookupByLibrary.simpleMessage("Мои настройки"), "name_empty": MessageLookupByLibrary.simpleMessage( "Имя пользователя обязательно для заполнения"), @@ -220,6 +227,7 @@ class MessageLookup extends MessageLookupByLibrary { "password_short": MessageLookupByLibrary.simpleMessage( "В пароле должно быть минимум 6 символов"), "pcs": MessageLookupByLibrary.simpleMessage("шт"), + "personal": MessageLookupByLibrary.simpleMessage("Персональные"), "personal_diet_state": MessageLookupByLibrary.simpleMessage("Персональная"), "photo": MessageLookupByLibrary.simpleMessage("Фото"), @@ -249,6 +257,9 @@ class MessageLookup extends MessageLookupByLibrary { "profile": MessageLookupByLibrary.simpleMessage("Профиль"), "proteins": MessageLookupByLibrary.simpleMessage("Белки"), "publish": MessageLookupByLibrary.simpleMessage("Опубликовать"), + "published": MessageLookupByLibrary.simpleMessage("Опубликованные"), + "removeFromActive": + MessageLookupByLibrary.simpleMessage("Снять с публикации"), "save": MessageLookupByLibrary.simpleMessage("Сохранить"), "services": MessageLookupByLibrary.simpleMessage("Оказание услуг"), "settings": MessageLookupByLibrary.simpleMessage("Настройки"), @@ -264,6 +275,7 @@ class MessageLookup extends MessageLookupByLibrary { "Расскажите в свободной форме о том как просыпаетесь, когда едите, когда ложитесь спать:"), "three_options": MessageLookupByLibrary.simpleMessage("3 опции"), "time": MessageLookupByLibrary.simpleMessage("Время прохождения"), + "toOffer": MessageLookupByLibrary.simpleMessage("Предложить"), "to_draft": MessageLookupByLibrary.simpleMessage("В черновик"), "unfavorite_foods": MessageLookupByLibrary.simpleMessage("Нелюбимые продукты"), diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index 363081a6..b8f0195c 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -2059,6 +2059,116 @@ class S { args: [], ); } + + /// `Мои диеты` + String get my_diets { + return Intl.message( + 'Мои диеты', + name: 'my_diets', + desc: '', + args: [], + ); + } + + /// `Опубликованные` + String get published { + return Intl.message( + 'Опубликованные', + name: 'published', + desc: '', + args: [], + ); + } + + /// `Черновики` + String get drafted { + return Intl.message( + 'Черновики', + name: 'drafted', + desc: '', + args: [], + ); + } + + /// `Персональные` + String get personal { + return Intl.message( + 'Персональные', + name: 'personal', + desc: '', + args: [], + ); + } + + /// `Редактировать` + String get edit { + return Intl.message( + 'Редактировать', + name: 'edit', + desc: '', + args: [], + ); + } + + /// `Предложить` + String get toOffer { + return Intl.message( + 'Предложить', + name: 'toOffer', + desc: '', + args: [], + ); + } + + /// `Снять с публикации` + String get removeFromActive { + return Intl.message( + 'Снять с публикации', + name: 'removeFromActive', + desc: '', + args: [], + ); + } + + /// `Диета` + String get diet { + return Intl.message( + 'Диета', + name: 'diet', + desc: '', + args: [], + ); + } + + /// `Удалена` + String get deleted { + return Intl.message( + 'Удалена', + name: 'deleted', + desc: '', + args: [], + ); + } + + /// `Мои планы` + String get my_plans { + return Intl.message( + 'Мои планы', + name: 'my_plans', + desc: '', + args: [], + ); + } + + /// `от` + String get from { + return Intl.message( + 'от', + name: 'from', + desc: '', + args: [], + ); + } } class AppLocalizationDelegate extends LocalizationsDelegate { diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 4d8be716..16d0d7ee 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -199,7 +199,16 @@ "wellness_type_desc": "Если вы любите мучные продукты, но хотите придерживаться диеты, вам подходит диета с добавленим мучных блюд на основе цельнозерновых вариантов. В них больше сложных углеводов и клетчатки, что способствует длительному чувству сытости и поддерживает здоровье пищеварительной системы.", "eating_behavior_type": "Формирование пищевого поведения", "eating_behavior_type_desc": "Формирование здорового пищевого поведения — это процесс, который включает в себя изменение привычек и отношения к еде. Он направлен на то, чтобы помочь людям научиться правильно питаться и поддерживать здоровый образ жизни.Основные принципы формирования здорового пищевого поведения: \n● Разнообразие продуктов; \n● Сбалансированный рацион; \n● Питьевой режим; \n● Ограничение вредных продуктов; \n● Регулярные приёмы пищи; \n● Умеренность;Осознанное питание; \n● Физическая активность;", - "price": "Цена" - - + "price": "Цена", + "my_diets": "Мои диеты", + "published": "Опубликованные", + "drafted": "Черновики", + "personal": "Персональные", + "edit": "Редактировать", + "toOffer": "Предложить", + "removeFromActive": "Снять с публикации", + "diet": "Диета", + "deleted": "Удалена", + "my_plans": "Мои планы", + "from": "от" }