diff --git a/assets/icons/cloud.svg b/assets/icons/cloud.svg new file mode 100644 index 00000000..422941f0 --- /dev/null +++ b/assets/icons/cloud.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/no_internet.svg b/assets/icons/no_internet.svg new file mode 100644 index 00000000..e2b89a16 --- /dev/null +++ b/assets/icons/no_internet.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/play_icon.svg b/assets/icons/play_icon.svg new file mode 100644 index 00000000..d12618a2 --- /dev/null +++ b/assets/icons/play_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/right_arrow.svg b/assets/icons/right_arrow.svg new file mode 100644 index 00000000..06f74177 --- /dev/null +++ b/assets/icons/right_arrow.svg @@ -0,0 +1,3 @@ + + + diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index b335af4e..e7e549aa 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -361,6 +361,8 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 16; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; @@ -382,6 +384,7 @@ MARKETING_VERSION = 2.0.0; PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; @@ -507,6 +510,8 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 16; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; @@ -528,6 +533,7 @@ MARKETING_VERSION = 2.0.0; PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -541,6 +547,8 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 16; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; @@ -562,6 +570,7 @@ MARKETING_VERSION = 2.0.0; PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; diff --git a/lib/core/app_router.dart b/lib/core/app_router.dart index 94450f4f..135b465e 100644 --- a/lib/core/app_router.dart +++ b/lib/core/app_router.dart @@ -1,4 +1,4 @@ -import 'package:eatvio_app/core/initialization_cubit/ws_manager.dart'; +import 'package:eatvio_app/core/websockets/ws_manager.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/token_storage.dart'; import 'package:eatvio_app/features/auth/presentation/pages/auth_screen.dart'; @@ -8,16 +8,17 @@ import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/prof import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_state.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart'; import 'package:eatvio_app/features/calendar/presentation/pages/calendar_screen.dart'; +import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart'; import 'package:eatvio_app/features/chat/presentation/pages/chat_screen.dart'; -import 'package:eatvio_app/features/chat/presentation/pages/chat_select_screen.dart'; +import 'package:eatvio_app/features/chat/presentation/pages/chats_screen.dart'; import 'package:eatvio_app/features/chat/presentation/pages/photo_view_screen.dart'; -import 'package:eatvio_app/features/complaining/presentation/complaining_screen.dart'; import 'package:eatvio_app/features/consultant/become_consultant/presentation/pages/become_consultant_screen.dart'; import 'package:eatvio_app/features/consultant/become_consultant/presentation/pages/stepper_become_consultant_screen.dart'; import 'package:eatvio_app/features/consultations/presentation/pages/consultations_screen.dart'; import 'package:eatvio_app/features/create_diet/presentation/create_diet_screen.dart'; import 'package:eatvio_app/features/create_diet/presentation/diet_preview_screen.dart'; import 'package:eatvio_app/features/create_plan/presentation/create_plan_screen.dart'; +import 'package:eatvio_app/features/consultant_rating/presentation/consultant_rating_screen.dart'; import 'package:eatvio_app/features/dev_tools/presentation/dev_tools_screen.dart'; import 'package:eatvio_app/features/diary/presentation/pages/diary_screen.dart'; import 'package:eatvio_app/features/diets/presentation/pages/about_diet_screen.dart'; @@ -34,8 +35,8 @@ import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart'; import 'package:eatvio_app/features/plans/presentation/pages/about_plan_screen.dart'; import 'package:eatvio_app/features/profile/presentation/pages/profile_screen.dart'; import 'package:eatvio_app/features/profile/presentation/pages/subs_screen.dart'; -import 'package:eatvio_app/features/recipes/presentation/pages/about_recipes_screen.dart'; -import 'package:eatvio_app/features/recipes/presentation/pages/recipe_create_screen.dart'; +import 'package:eatvio_app/features/recipe/presentation/about_recipe_screen.dart'; +import 'package:eatvio_app/features/create_recipe/presentation/pages/recipe_create_screen.dart'; import 'package:eatvio_app/features/registration/presentation/pages/registration_screen.dart'; import 'package:eatvio_app/features/registration/presentation/pages/webview_registration_screen.dart'; import 'package:eatvio_app/features/settings/entities/settings_entity.dart'; @@ -64,6 +65,8 @@ class AppRouter { path: '/auth-screen', name: 'authScreen', redirect: (context, state) async { + // TokenStorage.removeTokenInStorage(); + // await TokenStorage.saveTokenInStorage('Bearer 3|laravel_sanctum_zoubsO9NPXAqgLZEdHV9R3XbuDUHJJMJyXcd77mhbb649f04'); if (await TokenStorage.haveTokenInStorage()) { return '/my-profile-screen'; } @@ -125,7 +128,7 @@ class AppRouter { GoRoute( path: '/chat-select-screen', name: 'chatSelectScreen', - pageBuilder: (context, state) => NoTransitionPage(child: ChatSelectScreen()), + pageBuilder: (context, state) => NoTransitionPage(child: ChatsScreen()), ), GoRoute( path: '/notifications-screen', @@ -159,9 +162,14 @@ class AppRouter { GoRoute( path: '/about-recipes-screen', name: 'aboutRecipesScreen', - builder: (context, state) => AboutRecipesScreen( - recipes: state.extra as Recipes, - ), + builder: (context, state) { + final data = state.extra as Map; + return AboutRecipeScreen( + recipe: data['recipe'] as Recipe, + index: data['index'] as int?, + navigateToComments: data['navigateToComments'] as bool, + ); + }, ), GoRoute( path: RecipeCreateScreen.path, @@ -212,7 +220,15 @@ class AppRouter { GoRoute( path: '/chat-screen', name: 'chatScreen', - builder: (context, state) => ChatScreen(chatId: state.extra as String), + builder: (context, state) { + final data = state.extra as Map; + final chatsCubit = data['chatsCubit'] as ChatsCubit; + final chatId = data['chatId'] as String; + return ChatScreen( + chatId: chatId, + chatsCubit: chatsCubit, + ); + }, ), GoRoute( path: '/settings-screen', @@ -298,11 +314,6 @@ class AppRouter { index: int.parse(state.pathParameters['index']!), ), ), - GoRoute( - path: '/complaining-screen', - name: 'complainingScreen', - builder: (context, state) => ComplainingScreen(), - ), GoRoute( path: CreateDietScreen.path, name: CreateDietScreen.name, @@ -348,6 +359,11 @@ class AppRouter { name: DevToolsScreen.name, builder: (context, state) => DevToolsScreen(), ), + GoRoute( + path: ConsultantRatingScreen.path, + name: ConsultantRatingScreen.name, + builder: (context, state) => ConsultantRatingScreen(), + ), ], ); } diff --git a/lib/core/const.dart b/lib/core/const.dart index dfeae1fc..4e8b3650 100644 --- a/lib/core/const.dart +++ b/lib/core/const.dart @@ -1,5 +1,5 @@ -String baseUrl = 'https://eatvio.ru'; -// String baseUrl = 'https://dev2.eatvio.com'; +// String baseUrl = 'https://eatvio.ru'; +String baseUrl = 'https://dev2.eatvio.com'; const List durationValues = [7, 14, 21]; diff --git a/lib/core/enums/base_state.dart b/lib/core/enums/base_state.dart new file mode 100644 index 00000000..9a65b9e5 --- /dev/null +++ b/lib/core/enums/base_state.dart @@ -0,0 +1,6 @@ +enum BaseState { + loading, + error, + initial, + success +} \ No newline at end of file diff --git a/lib/core/enums/complained_content.dart b/lib/core/enums/complained_content.dart new file mode 100644 index 00000000..66fe8ee1 --- /dev/null +++ b/lib/core/enums/complained_content.dart @@ -0,0 +1,7 @@ +enum ComplainedContent { + diet, + plan, + recipe, + profile, + posts +} \ No newline at end of file diff --git a/lib/core/enums/product_type.dart b/lib/core/enums/product_type.dart new file mode 100644 index 00000000..415487f3 --- /dev/null +++ b/lib/core/enums/product_type.dart @@ -0,0 +1,5 @@ +enum ProductType { + diet, + plan, + recipe +} \ No newline at end of file diff --git a/lib/core/initialization_cubit/initialization_cubit.dart b/lib/core/initialization_cubit/initialization_cubit.dart deleted file mode 100644 index e69de29b..00000000 diff --git a/lib/core/locator_service.dart b/lib/core/locator_service.dart index 4dbfd90e..f5caadfb 100644 --- a/lib/core/locator_service.dart +++ b/lib/core/locator_service.dart @@ -1,10 +1,13 @@ import 'package:dio/dio.dart'; import 'package:eatvio_app/core/const.dart'; -import 'package:eatvio_app/core/initialization_cubit/ws_manager.dart'; -import 'package:eatvio_app/core/initialization_cubit/ws_service.dart'; +import 'package:eatvio_app/core/websockets/ws_manager.dart'; +import 'package:eatvio_app/core/websockets/ws_service.dart'; import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit.dart'; import 'package:eatvio_app/features/auth/repositories/auth_repository.dart'; import 'package:eatvio_app/features/auth/services/auth_service.dart'; +import 'package:eatvio_app/features/complaining/presentation/cubit/complaining_cubit.dart'; +import 'package:eatvio_app/features/complaining/repositories/complaining_repository.dart'; +import 'package:eatvio_app/features/complaining/services/complaining_service.dart'; import 'package:eatvio_app/features/create_diet/presentation/cubit/create_diet_cubit.dart'; import 'package:eatvio_app/features/create_diet/repositories/create_diet_repository.dart'; import 'package:eatvio_app/features/create_diet/services/create_diet_service.dart'; @@ -21,7 +24,7 @@ import 'package:eatvio_app/features/calendar/presentation/bloc/calendar_cubit.da import 'package:eatvio_app/features/calendar/repositories/calendar_repository.dart'; import 'package:eatvio_app/features/calendar/services/calendar_service.dart'; import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart'; -import 'package:eatvio_app/features/chat/presentation/bloc/chat_select_cubit/chat_select_cubit.dart'; +import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart'; import 'package:eatvio_app/features/chat/repositories/chat_repository.dart'; import 'package:eatvio_app/features/chat/services/chat_service.dart'; import 'package:eatvio_app/features/consultant/become_consultant/presentation/bloc/register_consultant_cubit/register_consultant_cubit.dart'; @@ -57,9 +60,12 @@ import 'package:eatvio_app/features/profile/presentation/bloc/profile_cubit/prof import 'package:eatvio_app/features/profile/presentation/bloc/sub_cubit/sub_cubit.dart'; import 'package:eatvio_app/features/profile/repositories/profile_repository.dart'; import 'package:eatvio_app/features/profile/services/profile_service.dart'; -import 'package:eatvio_app/features/recipes/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart'; -import 'package:eatvio_app/features/recipes/reporitories/recipe_repository.dart'; -import 'package:eatvio_app/features/recipes/services/recipe_service.dart'; +import 'package:eatvio_app/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart'; +import 'package:eatvio_app/features/create_recipe/reporitories/recipe_repository.dart'; +import 'package:eatvio_app/features/create_recipe/services/recipe_service.dart'; +import 'package:eatvio_app/features/recipe/presentation/cubit/about_recipe_cubit.dart'; +import 'package:eatvio_app/features/recipe/repositories/about_recipe_repository.dart'; +import 'package:eatvio_app/features/recipe/services/about_recipe_service.dart'; import 'package:eatvio_app/features/registration/presentation/bloc/registration_cubit/registration_cubit.dart'; import 'package:eatvio_app/features/registration/repositories/registration_repository.dart'; import 'package:eatvio_app/features/registration/services/registration_service.dart'; @@ -84,13 +90,13 @@ Future init() async { // Cubit factories sl.registerFactory(() => RegistrationCubit(registrationRepository: sl())); sl.registerFactory(() => ChatRoomCubit(chatRepository: sl(), wsManager: sl())); - sl.registerFactory(() => RecipesCubit(feedRepository: sl(), recipeRepository: sl())); + sl.registerFactory(() => RecipesCubit(feedRepository: sl(), recipeRepository: sl(), aboutRecipeRepository: sl())); sl.registerFactory(() => CalendarCubit(calendarRepository: sl())); sl.registerFactory(() => ProfileCubit(profileRepository: sl(), chatRepository: sl())); sl.registerFactory(() => profilePlans.PlansCubit(plansRepository: sl())); sl.registerFactory(() => DietsCubit(feedRepository: sl(), dietsRepository: sl())); sl.registerFactory(() => CommentsCubit(feedRepository: sl())); - sl.registerFactory(() => ChatSelectCubit(chatRepository: sl())); + sl.registerFactory(() => ChatsCubit(chatRepository: sl(), wsManager: sl(), profileRepository: sl())); sl.registerFactory(() => PlansCubit(feedRepository: sl(), planRepository: sl())); sl.registerFactory(() => SettingsCubit(profileRepository: sl(), settingsRepository: sl())); sl.registerFactory(() => RegisterConsultantCubit(consultantRepository: sl())); @@ -103,11 +109,13 @@ Future init() async { sl.registerFactory(() => FeedProfileCubit(profileRepository: sl())); sl.registerFactory(() => AboutPlanCubit(planRepository: sl())); sl.registerFactory(() => CreateRecipeCubit(recipeRepository: sl())); - sl.registerFactory(() => MyDietsCubit(myDietsRepository: sl())); + sl.registerFactory(() => MyDietsCubit(myDietsRepository: sl(), createDietRepository: sl())); sl.registerFactory(() => MyPlansCubit(myPlansRepository: sl())); sl.registerFactory(() => UserDetailsCubit(profileRepository: sl())); sl.registerFactory(() => ConsultantsCubit(feedRepository: sl())); + sl.registerFactory(() => AboutRecipeCubit(recipeRepository: sl(), profileRepository: sl())); sl.registerFactory(() => AuthCubit(authRepository: sl())); + sl.registerFactory(() => ComplainingCubit(repository: sl())); // Repository sl.registerLazySingleton(() => ChatRepository(chatService: sl())); @@ -127,6 +135,8 @@ Future init() async { sl.registerLazySingleton(() => PlanRepository(planService: sl())); sl.registerLazySingleton(() => MyDietsRepository(myDietsService: sl())); sl.registerLazySingleton(() => MyPlansRepository(myPlansService: sl())); + sl.registerLazySingleton(() => AboutRecipeRepository(recipeService: sl())); + sl.registerLazySingleton(() => ComplainingRepository(service: sl())); sl.registerLazySingleton(() => AuthRepository(authService: sl())); // Services @@ -135,7 +145,7 @@ Future init() async { sl.registerLazySingleton(() => RegistrationService(sl())); sl.registerLazySingleton(() => CalendarService(sl())); sl.registerLazySingleton(() => ProfileService(sl())); - sl.registerLazySingleton(() => RecipeService(sl())); + sl.registerLazySingleton(() => CreateRecipeService(sl())); sl.registerLazySingleton(() => SettingsService(sl())); sl.registerLazySingleton(() => ConsultantService(sl())); sl.registerLazySingleton(() => DiaryService(sl())); @@ -148,6 +158,8 @@ Future init() async { sl.registerLazySingleton(() => MyDietsService(sl())); sl.registerLazySingleton(() => MyPlansService(sl())); sl.registerLazySingleton(() => WsService(sl())); + sl.registerLazySingleton(() => AboutRecipeService(sl())); + sl.registerLazySingleton(() => ComplainingService(sl())); sl.registerLazySingleton(() => AuthService(sl())); // External diff --git a/lib/core/theme/custom_text_style.dart b/lib/core/theme/custom_text_style.dart index 6837e186..2e39baf8 100644 --- a/lib/core/theme/custom_text_style.dart +++ b/lib/core/theme/custom_text_style.dart @@ -184,6 +184,12 @@ abstract class TextStyles { height: 1.255, ); + static const headingFS40FW600 = TextStyle( + fontSize: 40, + fontWeight: FontWeight.w600, + height: 1.255, + ); + static const textLSemibold = TextStyle( fontSize: 18, fontWeight: FontWeight.w600, @@ -230,10 +236,10 @@ abstract class TextStyles { fontSize: 12, fontWeight: FontWeight.w500, height: 1.255, - ); + ); static const textFS10FW500 = TextStyle( - fontSize: 12, + fontSize: 10, fontWeight: FontWeight.w500, height: 1.255, ); diff --git a/lib/core/theme/theme_helper.dart b/lib/core/theme/theme_helper.dart index 31fbd60b..2c319dd8 100644 --- a/lib/core/theme/theme_helper.dart +++ b/lib/core/theme/theme_helper.dart @@ -190,6 +190,10 @@ class PrimaryColors { Color get transparentBlack => Color(0xFF1313264).withValues(alpha: 0.3); Color get gray60004 => Color(0xFFD4DAE2).withValues(alpha: 0.2); + + Color get blueGray => Color(0xFF809CB9); + + Color get dialogShadow => Color.fromARGB(125, 0, 0, 0); } PrimaryColors get appTheme => ThemeHelper().themeColor(); diff --git a/lib/core/utils/localization_utils.dart b/lib/core/utils/localization_utils.dart index 23d846a0..2070f626 100644 --- a/lib/core/utils/localization_utils.dart +++ b/lib/core/utils/localization_utils.dart @@ -41,3 +41,33 @@ String getCommentForm(int number, S s) { return s.comments_plural; } } + +String getMinuteForm(int number) { + if (number % 10 == 1 && number % 100 != 11) { + return S.current.minute; + } else if ([2, 3, 4].contains(number % 10) && ![12, 13, 14].contains(number % 100)) { + return S.current.minutes; + } else { + return S.current.another_form_of_minute; + } +} + +String getHourForm(int number) { + if (number % 10 == 1 && number % 100 != 11) { + return S.current.hour; + } else if ([2, 3, 4].contains(number % 10) && ![12, 13, 14].contains(number % 100)) { + return S.current.another_form_of_hour; + } else { + return S.current.hours; + } +} + +String getStepForm(int number) { + if (number % 10 == 1 && number % 100 != 11) { + return S.current.stage; + } else if ([2, 3, 4].contains(number % 10) && ![12, 13, 14].contains(number % 100)) { + return S.current.another_form_of_stage; + } else { + return S.current.stages; + } +} diff --git a/lib/core/initialization_cubit/ws_manager.dart b/lib/core/websockets/ws_manager.dart similarity index 98% rename from lib/core/initialization_cubit/ws_manager.dart rename to lib/core/websockets/ws_manager.dart index 3a72512e..e414557d 100644 --- a/lib/core/initialization_cubit/ws_manager.dart +++ b/lib/core/websockets/ws_manager.dart @@ -2,7 +2,7 @@ import 'dart:developer'; import 'package:dart_pusher_channels/dart_pusher_channels.dart'; import 'package:eatvio_app/core/const.dart'; -import 'package:eatvio_app/core/initialization_cubit/ws_service.dart'; +import 'package:eatvio_app/core/websockets/ws_service.dart'; import 'package:eatvio_app/core/token_storage.dart'; class WsManager { diff --git a/lib/core/initialization_cubit/ws_service.dart b/lib/core/websockets/ws_service.dart similarity index 100% rename from lib/core/initialization_cubit/ws_service.dart rename to lib/core/websockets/ws_service.dart diff --git a/lib/core/initialization_cubit/ws_service.g.dart b/lib/core/websockets/ws_service.g.dart similarity index 100% rename from lib/core/initialization_cubit/ws_service.g.dart rename to lib/core/websockets/ws_service.g.dart diff --git a/lib/core/widgets/data_error_widget.dart b/lib/core/widgets/data_error_widget.dart new file mode 100644 index 00000000..1ce1c110 --- /dev/null +++ b/lib/core/widgets/data_error_widget.dart @@ -0,0 +1,68 @@ +import 'package:eatvio_app/core/theme/custom_text_style.dart'; +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/core/widgets/custom_outlined_button.dart'; +import 'package:eatvio_app/core/widgets/loader.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; + +class DataErrorWidget extends StatefulWidget { + const DataErrorWidget({super.key, required this.onReload}); + + final Future Function() onReload; + + @override + State createState() => _DataErrorWidgetState(); +} + +class _DataErrorWidgetState extends State { + bool isLoading = false; + @override + Widget build(BuildContext context) { + final s = S.of(context); + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SvgPicture.asset('assets/icons/cloud.svg'), + SizedBox(height: 30), + Text( + s.something_went_wrong, + style: TextStyles.textLSemibold.copyWith(color: appTheme.black), + ), + SizedBox(height: 12), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: Text( + s.can_not_load_data_please_try_again, + style: TextStyles.textSMedium.copyWith(color: appTheme.black), + textAlign: TextAlign.center, + ), + ), + SizedBox(height: 20), + isLoading + ? Loader() + : Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: CustomOutlinedButton( + text: s.try_again, + height: 52, + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), + buttonStyle: ButtonStyle( + side: WidgetStatePropertyAll(BorderSide.none), + backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), + ), + onPressed: () async { + setState(() { + isLoading = true; + }); + await widget.onReload(); + setState(() { + isLoading = false; + }); + }, + ), + ), + ], + ); + } +} diff --git a/lib/core/widgets/info_row.dart b/lib/core/widgets/info_row.dart index 564cf015..9f535d91 100644 --- a/lib/core/widgets/info_row.dart +++ b/lib/core/widgets/info_row.dart @@ -1,9 +1,11 @@ import 'package:eatvio_app/core/theme/custom_text_style.dart'; +import 'package:eatvio_app/core/enums/product_type.dart'; import 'package:eatvio_app/core/utils/localization_utils.dart'; import 'package:eatvio_app/features/diets/presentation/widgets/info_bouble.dart'; import 'package:eatvio_app/features/diets/presentation/widgets/type_badges.dart'; import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart'; +import 'package:eatvio_app/features/feed/models/recipes_model.dart'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:flutter/material.dart'; @@ -11,21 +13,35 @@ import 'package:flutter_svg/svg.dart'; class InfoRow extends StatelessWidget { const InfoRow({ + required this.type, super.key, this.diet, this.itemsBackgroundColor, this.disableDescriptionOfTypes = false, this.plan, + this.recipe, }); + final ProductType type; final Color? itemsBackgroundColor; final Diet? diet; final PlanModel? plan; + final Recipe? recipe; final bool disableDescriptionOfTypes; @override Widget build(BuildContext context) { final s = S.of(context); + String? timeTitle; + if (recipe != null) { + final hours = recipe!.cookingTime ~/ 60; + final minutes = recipe!.cookingTime % 60; + if (hours > 0) { + timeTitle = '$hours ${getHourForm(hours)} $minutes ${getMinuteForm(minutes)}'; + } else { + timeTitle = '$minutes ${getMinuteForm(minutes)}'; + } + } return SingleChildScrollView( padding: const EdgeInsets.symmetric(horizontal: 16), scrollDirection: Axis.horizontal, @@ -41,7 +57,11 @@ class InfoRow extends StatelessWidget { ), SizedBox(width: 4), Text( - diet == null ? '${plan?.duration} ${getDayForm(plan?.duration ?? 7, s)}' : '${diet?.duration} ${getDayForm(diet?.duration ?? 7, s)}', + switch (type) { + ProductType.diet => '${diet?.duration} ${getDayForm(diet?.duration ?? 7, s)}', + ProductType.plan => '${plan?.duration} ${getDayForm(plan?.duration ?? 7, s)}', + ProductType.recipe => '${timeTitle ?? ''}', + }, style: TextStyles.textXSSemibold.copyWith(color: appTheme.black), ), ], @@ -51,18 +71,42 @@ class InfoRow extends StatelessWidget { backgroundColor: itemsBackgroundColor, children: [ SvgPicture.asset( - 'assets/icons/people.svg', + switch (type) { + ProductType.diet => 'assets/icons/people.svg', + ProductType.plan => 'assets/icons/people.svg', + ProductType.recipe => 'assets/icons/calories.svg', + }, width: 13.3, height: 13.3, ), SizedBox(width: 4), Text( - '80 человек прошли', + switch (type) { + ProductType.diet => '80 человек прошли', + ProductType.plan => '80 человек прошли', + ProductType.recipe => (recipe?.calories.toStringAsFixed(0) ?? '0') + ' ккал', + }, style: TextStyles.textXSSemibold.copyWith(color: appTheme.black), ), ], ), SizedBox(width: 12), + if (type == ProductType.recipe) + InfoBouble( + backgroundColor: itemsBackgroundColor, + children: [ + SvgPicture.asset( + 'assets/icons/steps.svg', + width: 13.3, + height: 13.3, + ), + SizedBox(width: 4), + Text( + '${recipe?.steps?.length ?? 0} ${getStepForm(recipe?.steps?.length ?? 0)}', + style: TextStyles.textXSSemibold.copyWith(color: appTheme.black), + ), + ], + ), if (plan?.type.hasAtLeastOneType() == true || diet?.type.hasAtLeastOneType() == true) InfoBouble( backgroundColor: itemsBackgroundColor, diff --git a/lib/core/widgets/show_error_dialog.dart b/lib/core/widgets/show_error_dialog.dart new file mode 100644 index 00000000..317f3321 --- /dev/null +++ b/lib/core/widgets/show_error_dialog.dart @@ -0,0 +1,59 @@ +import 'package:eatvio_app/core/app_router.dart'; +import 'package:eatvio_app/core/theme/custom_text_style.dart'; +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +void showErrorDialog(BuildContext context, {String? title, String? description}) { + showDialog( + context: context, + barrierColor: Colors.transparent, + builder: (BuildContext context) { + final s = S.of(context); + return Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Container( + decoration: BoxDecoration( + color: appTheme.whiteA700, + borderRadius: BorderRadius.all(Radius.circular(20)), + boxShadow: [BoxShadow(color: appTheme.dialogShadow, blurRadius: 32)], + ), + width: MediaQuery.sizeOf(context).width, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 20), + Text( + title ?? s.some_error_occurred, + style: TextStyles.textLSemibold.copyWith(color: appTheme.black, decoration: TextDecoration.none), + ), + SizedBox(height: 12), + Padding( + padding: const EdgeInsets.only(right: 72), + child: Text( + description ?? s.some_error_occurred_description, + style: TextStyles.textSMedium.copyWith(color: appTheme.black, decoration: TextDecoration.none), + ), + ), + Align( + alignment: Alignment.centerRight, + child: TextButton( + onPressed: () => AppRouter.navigatorKey.currentContext?.pop(), + child: Text(s.ok, style: TextStyles.textLSemibold.copyWith(color: appTheme.blueA400)), + ), + ), + SizedBox(height: 12), + ], + ), + ), + ), + ), + ); + }, + ); +} diff --git a/lib/features/feed/presentation/widgets/success_published_diet_bottom_sheet.dart b/lib/core/widgets/success_published_diet_bottom_sheet.dart similarity index 88% rename from lib/features/feed/presentation/widgets/success_published_diet_bottom_sheet.dart rename to lib/core/widgets/success_published_diet_bottom_sheet.dart index eade82c2..606aad41 100644 --- a/lib/features/feed/presentation/widgets/success_published_diet_bottom_sheet.dart +++ b/lib/core/widgets/success_published_diet_bottom_sheet.dart @@ -1,7 +1,9 @@ +import 'package:eatvio_app/core/app_router.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/custom_outlined_button.dart'; +import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; @@ -10,6 +12,7 @@ class SuccessPublishedDietBottomSheet extends StatelessWidget { final String title; @override Widget build(BuildContext context) { + final s = S.of(context); return Column( children: [ SizedBox( @@ -34,9 +37,9 @@ class SuccessPublishedDietBottomSheet extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 20), child: CustomOutlinedButton( onPressed: () { - GoRouter.of(context).pop(); + AppRouter.navigatorKey.currentContext?.pop(); }, - text: 'Хорошо', + text: s.ok, height: 52, buttonTextStyle: TextStyles.textSSemibold.copyWith(color: appTheme.whiteA700), buttonStyle: ButtonStyle( diff --git a/lib/core/widgets/success_published_recipe_bottom_sheet.dart b/lib/core/widgets/success_published_recipe_bottom_sheet.dart new file mode 100644 index 00000000..c97736ae --- /dev/null +++ b/lib/core/widgets/success_published_recipe_bottom_sheet.dart @@ -0,0 +1,57 @@ +import 'package:eatvio_app/core/app_router.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/custom_outlined_button.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +class SuccessPublishedRecipeBottomSheet extends StatelessWidget { + const SuccessPublishedRecipeBottomSheet({super.key, required this.title}); + final String title; + @override + Widget build(BuildContext context) { + final s = S.of(context); + return Column( + children: [ + SizedBox( + height: 20, + ), + BottomSheetHandlerBar(), + SizedBox( + height: 20, + ), + Text( + s.recipe_publishing, + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), + ), + Text( + '"$title" ${s.successfully_published}', + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), + ), + SizedBox( + height: 20, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: CustomOutlinedButton( + onPressed: () { + AppRouter.navigatorKey.currentContext?.pop(); + }, + text: s.ok, + 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/blog_post/presentation/pages/blog_post_screen.dart b/lib/features/blog_post/presentation/pages/blog_post_screen.dart index 1696928a..637a2a56 100644 --- a/lib/features/blog_post/presentation/pages/blog_post_screen.dart +++ b/lib/features/blog_post/presentation/pages/blog_post_screen.dart @@ -1,7 +1,10 @@ import 'dart:io'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/enums/complained_content.dart'; import 'package:eatvio_app/core/utils/date_time_utils.dart'; +import 'package:eatvio_app/core/widgets/show_error_dialog.dart'; +import 'package:eatvio_app/features/complaining/presentation/complaining_bottom_sheet.dart'; import 'package:eatvio_app/core/widgets/loader.dart'; import 'package:eatvio_app/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_cubit.dart'; import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart'; @@ -11,6 +14,7 @@ import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit import 'package:eatvio_app/features/feed/presentation/widgets/comments_widget.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/theme_helper.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'; @@ -191,9 +195,13 @@ class _BlogPostScreenState extends State { title: Text(widget.blogPost.title), actions: [ PopupMenuButton( - onSelected: (value) { + onSelected: (value) async { if (value == 'delete') { - sl().deleteBlogPost(widget.blogPost.id); + final result = await sl().deleteBlogPost(widget.blogPost.id); + if (!result) { + showErrorDialog(context, title: S.current.could_not_delete_blog_post); + return; + } context.read().loadFeedBlogPostList(); Navigator.of(context).pop(); } @@ -219,6 +227,16 @@ class _BlogPostScreenState extends State { PopupMenuItem( value: 'report', child: Text('Пожаловаться'), + onTap: () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (context) => ComplainingBottomSheet( + complainedContentType: ComplainedContent.posts, + id: widget.blogPost.id, + ), + ); + }, ), ], ), diff --git a/lib/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_cubit.dart b/lib/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_cubit.dart index 90ee1e88..8d2d1bf0 100644 --- a/lib/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_cubit.dart +++ b/lib/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_cubit.dart @@ -1,3 +1,5 @@ +import 'dart:developer'; + import 'package:eatvio_app/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_state.dart'; import 'package:eatvio_app/features/blog_post_create/repositories/blog_post_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -10,23 +12,29 @@ class BlogPostCreateCubit extends Cubit { required this.blogPostRepository, }) : super(BlogPostCreateState()); - createBlogPost({ + Future createBlogPost({ String? title, String? videoUrl, String? description, XFile? image, }) async { - emit(BlogPostCreateState.loading()); - await blogPostRepository.createBlogPost( - title, - videoUrl, - description, - image, - ); - emit(BlogPostCreateState.loaded()); + try { + emit(BlogPostCreateState.loading()); + await blogPostRepository.createBlogPost( + title, + videoUrl, + description, + image, + ); + return true; + } catch (e, st) { + log(e.toString(), stackTrace: st); + emit(BlogPostCreateState.loaded()); + return false; + } } - updateBlogPost({ + Future updateBlogPost({ required int id, String? title, String? videoUrl, @@ -34,19 +42,32 @@ class BlogPostCreateCubit extends Cubit { XFile? image, String? oldImageUrl, }) async { - emit(BlogPostCreateState.loading()); - await blogPostRepository.updateBlogPost( - id, - title, - videoUrl, - description, - image, - oldImageUrl, - ); - emit(BlogPostCreateState.loaded()); + try { + emit(BlogPostCreateState.loading()); + await blogPostRepository.updateBlogPost( + id, + title, + videoUrl, + description, + image, + oldImageUrl, + ); + emit(BlogPostCreateState.loaded()); + return true; + } catch (e, st) { + log(e.toString(), stackTrace: st); + emit(BlogPostCreateState.loaded()); + return false; + } } - deleteBlogPost(int id) async { - await blogPostRepository.deleteBlogPost(id); + Future deleteBlogPost(int id) async { + try { + await blogPostRepository.deleteBlogPost(id); + return true; + } catch (e, st) { + log(e.toString(), stackTrace: st); + return false; + } } } diff --git a/lib/features/blog_post_create/presentation/pages/blog_post_create_screen.dart b/lib/features/blog_post_create/presentation/pages/blog_post_create_screen.dart index 9e7a85d3..04102415 100644 --- a/lib/features/blog_post_create/presentation/pages/blog_post_create_screen.dart +++ b/lib/features/blog_post_create/presentation/pages/blog_post_create_screen.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:io'; import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/theme/app_decoration.dart'; +import 'package:eatvio_app/core/widgets/show_error_dialog.dart'; import 'package:eatvio_app/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_cubit.dart'; import 'package:eatvio_app/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_state.dart'; import 'package:eatvio_app/core/locator_service.dart'; @@ -9,10 +10,12 @@ 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/models/feed_blog_post_model.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart'; +import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart'; +import 'package:go_router/go_router.dart'; import 'package:image_picker/image_picker.dart'; class BlogPostCreateScreen extends StatefulWidget { @@ -65,7 +68,6 @@ class _BlogPostCreateScreenState extends State { listener: (BuildContext context, BlogPostCreateState state) { if (state is BlogPostCreateStateLoaded) { context.read().loadFeedBlogPostList(); - Navigator.of(context).pop(); } }, child: _buildBlogPostCreate(), @@ -148,8 +150,9 @@ class _BlogPostCreateScreenState extends State { setState(() { isLoading = true; }); + if (widget.feedBlogPostModel != null) { - await blogPostCubit.updateBlogPost( + final result = await blogPostCubit.updateBlogPost( id: widget.feedBlogPostModel!.id, title: titleController.text, description: descriptionController.text, @@ -157,13 +160,29 @@ class _BlogPostCreateScreenState extends State { image: image, oldImageUrl: widget.feedBlogPostModel!.image, ); + setState(() { + isLoading = false; + }); + if (!result) { + showErrorDialog(context, title: S.current.could_not_update_blog_post); + } else { + context.pop(); + } } else { - await blogPostCubit.createBlogPost( + final result = await blogPostCubit.createBlogPost( title: titleController.text, description: descriptionController.text, videoUrl: videoUrlController.text, image: image, ); + setState(() { + isLoading = false; + }); + if (!result) { + showErrorDialog(context, title: S.current.could_not_publish_blog_post); + } else { + context.pop(); + } } }, ), diff --git a/lib/features/bottom_navigation_bar/presentation/bloc/notifications_cubit/notifications_cubit.dart b/lib/features/bottom_navigation_bar/presentation/bloc/notifications_cubit/notifications_cubit.dart index 5d9a27f6..e193a23c 100644 --- a/lib/features/bottom_navigation_bar/presentation/bloc/notifications_cubit/notifications_cubit.dart +++ b/lib/features/bottom_navigation_bar/presentation/bloc/notifications_cubit/notifications_cubit.dart @@ -1,4 +1,4 @@ -import 'package:eatvio_app/core/initialization_cubit/ws_manager.dart'; +import 'package:eatvio_app/core/websockets/ws_manager.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/notifications_cubit/notifications_state.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; diff --git a/lib/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart b/lib/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart index e96b6f25..32da11b4 100644 --- a/lib/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart +++ b/lib/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart @@ -1,10 +1,10 @@ import 'package:badges/badges.dart' as badges; import 'package:eatvio_app/core/utils/image_constant.dart'; -import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/new_messages_cubit/new_messages_cubit.dart'; -import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/new_messages_cubit/new_messages_state.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/notifications_cubit/notifications_cubit.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/notifications_cubit/notifications_state.dart'; import 'package:eatvio_app/features/profile/models/profile_model.dart'; +import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart'; +import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_state.dart'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:flutter/material.dart'; @@ -160,15 +160,19 @@ class _BottomNavigationBarWidgetState extends State { ), BottomNavigationBarItem( label: s.chat, - icon: BlocBuilder( - builder: (BuildContext context, NewMessagesState state) { + icon: BlocBuilder( + builder: (BuildContext context, ChatsState state) { + final hasUnreadMessages = state.chatsWithUnreadMessagesCount > 0; return badges.Badge( position: badges.BadgePosition.topEnd(), badgeAnimation: badges.BadgeAnimation.fade(), + badgeStyle: badges.BadgeStyle( + badgeColor: hasUnreadMessages ? Colors.red : Colors.transparent, + ), badgeContent: Text( - (state is NewMessagesStateLoaded) ? state.messagesCount.toString() : '0', + hasUnreadMessages ? state.chatsWithUnreadMessagesCount.toString() : '', style: TextStyle( - color: Colors.white, + color: hasUnreadMessages ? Colors.white : Colors.transparent, fontSize: 8, ), ), diff --git a/lib/features/chat/models/chat_message_model.dart b/lib/features/chat/models/chat_message_model.dart index 23f03f9f..591e5993 100644 --- a/lib/features/chat/models/chat_message_model.dart +++ b/lib/features/chat/models/chat_message_model.dart @@ -1,3 +1,4 @@ +import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; part 'chat_message_model.freezed.dart'; @@ -10,7 +11,10 @@ class ChatMessageModel with _$ChatMessageModel { required String data, @JsonKey(name: 'media_data') required List? mediaData, @JsonKey(name: 'author_id') required int authorId, - @JsonKey(name: 'updated_at') required String updatedAt, + @JsonKey(name: 'updated_at', fromJson: parseDate) required DateTime updatedAt, + @JsonKey(name: 'read_by_to_user', defaultValue: false) required bool isRead, + @JsonKey(name: 'is_author_message_auth_user') required bool? isCurrentUserAuthor, + @JsonKey(name: 'room_id') required String? roomId, }) = _ChatMessageModel; factory ChatMessageModel.fromJson(Map json) => _$ChatMessageModelFromJson(json); diff --git a/lib/features/chat/models/chat_message_model.freezed.dart b/lib/features/chat/models/chat_message_model.freezed.dart index 8f921425..3cddc329 100644 --- a/lib/features/chat/models/chat_message_model.freezed.dart +++ b/lib/features/chat/models/chat_message_model.freezed.dart @@ -26,8 +26,14 @@ mixin _$ChatMessageModel { List? get mediaData => throw _privateConstructorUsedError; @JsonKey(name: 'author_id') int get authorId => throw _privateConstructorUsedError; - @JsonKey(name: 'updated_at') - String get updatedAt => throw _privateConstructorUsedError; + @JsonKey(name: 'updated_at', fromJson: parseDate) + DateTime get updatedAt => throw _privateConstructorUsedError; + @JsonKey(name: 'read_by_to_user', defaultValue: false) + bool get isRead => throw _privateConstructorUsedError; + @JsonKey(name: 'is_author_message_auth_user') + bool? get isCurrentUserAuthor => throw _privateConstructorUsedError; + @JsonKey(name: 'room_id') + String? get roomId => throw _privateConstructorUsedError; /// Serializes this ChatMessageModel to a JSON map. Map toJson() => throw _privateConstructorUsedError; @@ -50,7 +56,10 @@ abstract class $ChatMessageModelCopyWith<$Res> { String data, @JsonKey(name: 'media_data') List? mediaData, @JsonKey(name: 'author_id') int authorId, - @JsonKey(name: 'updated_at') String updatedAt}); + @JsonKey(name: 'updated_at', fromJson: parseDate) DateTime updatedAt, + @JsonKey(name: 'read_by_to_user', defaultValue: false) bool isRead, + @JsonKey(name: 'is_author_message_auth_user') bool? isCurrentUserAuthor, + @JsonKey(name: 'room_id') String? roomId}); } /// @nodoc @@ -73,6 +82,9 @@ class _$ChatMessageModelCopyWithImpl<$Res, $Val extends ChatMessageModel> Object? mediaData = freezed, Object? authorId = null, Object? updatedAt = null, + Object? isRead = null, + Object? isCurrentUserAuthor = freezed, + Object? roomId = freezed, }) { return _then(_value.copyWith( id: null == id @@ -94,7 +106,19 @@ class _$ChatMessageModelCopyWithImpl<$Res, $Val extends ChatMessageModel> updatedAt: null == updatedAt ? _value.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable - as String, + as DateTime, + isRead: null == isRead + ? _value.isRead + : isRead // ignore: cast_nullable_to_non_nullable + as bool, + isCurrentUserAuthor: freezed == isCurrentUserAuthor + ? _value.isCurrentUserAuthor + : isCurrentUserAuthor // ignore: cast_nullable_to_non_nullable + as bool?, + roomId: freezed == roomId + ? _value.roomId + : roomId // ignore: cast_nullable_to_non_nullable + as String?, ) as $Val); } } @@ -112,7 +136,10 @@ abstract class _$$ChatMessageModelImplCopyWith<$Res> String data, @JsonKey(name: 'media_data') List? mediaData, @JsonKey(name: 'author_id') int authorId, - @JsonKey(name: 'updated_at') String updatedAt}); + @JsonKey(name: 'updated_at', fromJson: parseDate) DateTime updatedAt, + @JsonKey(name: 'read_by_to_user', defaultValue: false) bool isRead, + @JsonKey(name: 'is_author_message_auth_user') bool? isCurrentUserAuthor, + @JsonKey(name: 'room_id') String? roomId}); } /// @nodoc @@ -133,6 +160,9 @@ class __$$ChatMessageModelImplCopyWithImpl<$Res> Object? mediaData = freezed, Object? authorId = null, Object? updatedAt = null, + Object? isRead = null, + Object? isCurrentUserAuthor = freezed, + Object? roomId = freezed, }) { return _then(_$ChatMessageModelImpl( id: null == id @@ -154,7 +184,19 @@ class __$$ChatMessageModelImplCopyWithImpl<$Res> updatedAt: null == updatedAt ? _value.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable - as String, + as DateTime, + isRead: null == isRead + ? _value.isRead + : isRead // ignore: cast_nullable_to_non_nullable + as bool, + isCurrentUserAuthor: freezed == isCurrentUserAuthor + ? _value.isCurrentUserAuthor + : isCurrentUserAuthor // ignore: cast_nullable_to_non_nullable + as bool?, + roomId: freezed == roomId + ? _value.roomId + : roomId // ignore: cast_nullable_to_non_nullable + as String?, )); } } @@ -167,7 +209,12 @@ class _$ChatMessageModelImpl implements _ChatMessageModel { required this.data, @JsonKey(name: 'media_data') required final List? mediaData, @JsonKey(name: 'author_id') required this.authorId, - @JsonKey(name: 'updated_at') required this.updatedAt}) + @JsonKey(name: 'updated_at', fromJson: parseDate) required this.updatedAt, + @JsonKey(name: 'read_by_to_user', defaultValue: false) + required this.isRead, + @JsonKey(name: 'is_author_message_auth_user') + required this.isCurrentUserAuthor, + @JsonKey(name: 'room_id') required this.roomId}) : _mediaData = mediaData; factory _$ChatMessageModelImpl.fromJson(Map json) => @@ -192,12 +239,21 @@ class _$ChatMessageModelImpl implements _ChatMessageModel { @JsonKey(name: 'author_id') final int authorId; @override - @JsonKey(name: 'updated_at') - final String updatedAt; + @JsonKey(name: 'updated_at', fromJson: parseDate) + final DateTime updatedAt; + @override + @JsonKey(name: 'read_by_to_user', defaultValue: false) + final bool isRead; + @override + @JsonKey(name: 'is_author_message_auth_user') + final bool? isCurrentUserAuthor; + @override + @JsonKey(name: 'room_id') + final String? roomId; @override String toString() { - return 'ChatMessageModel(id: $id, data: $data, mediaData: $mediaData, authorId: $authorId, updatedAt: $updatedAt)'; + return 'ChatMessageModel(id: $id, data: $data, mediaData: $mediaData, authorId: $authorId, updatedAt: $updatedAt, isRead: $isRead, isCurrentUserAuthor: $isCurrentUserAuthor, roomId: $roomId)'; } @override @@ -212,13 +268,25 @@ class _$ChatMessageModelImpl implements _ChatMessageModel { (identical(other.authorId, authorId) || other.authorId == authorId) && (identical(other.updatedAt, updatedAt) || - other.updatedAt == updatedAt)); + other.updatedAt == updatedAt) && + (identical(other.isRead, isRead) || other.isRead == isRead) && + (identical(other.isCurrentUserAuthor, isCurrentUserAuthor) || + other.isCurrentUserAuthor == isCurrentUserAuthor) && + (identical(other.roomId, roomId) || other.roomId == roomId)); } @JsonKey(includeFromJson: false, includeToJson: false) @override - int get hashCode => Object.hash(runtimeType, id, data, - const DeepCollectionEquality().hash(_mediaData), authorId, updatedAt); + int get hashCode => Object.hash( + runtimeType, + id, + data, + const DeepCollectionEquality().hash(_mediaData), + authorId, + updatedAt, + isRead, + isCurrentUserAuthor, + roomId); /// Create a copy of ChatMessageModel /// with the given fields replaced by the non-null parameter values. @@ -243,7 +311,13 @@ abstract class _ChatMessageModel implements ChatMessageModel { required final String data, @JsonKey(name: 'media_data') required final List? mediaData, @JsonKey(name: 'author_id') required final int authorId, - @JsonKey(name: 'updated_at') required final String updatedAt}) = + @JsonKey(name: 'updated_at', fromJson: parseDate) + required final DateTime updatedAt, + @JsonKey(name: 'read_by_to_user', defaultValue: false) + required final bool isRead, + @JsonKey(name: 'is_author_message_auth_user') + required final bool? isCurrentUserAuthor, + @JsonKey(name: 'room_id') required final String? roomId}) = _$ChatMessageModelImpl; factory _ChatMessageModel.fromJson(Map json) = @@ -260,8 +334,17 @@ abstract class _ChatMessageModel implements ChatMessageModel { @JsonKey(name: 'author_id') int get authorId; @override - @JsonKey(name: 'updated_at') - String get updatedAt; + @JsonKey(name: 'updated_at', fromJson: parseDate) + DateTime get updatedAt; + @override + @JsonKey(name: 'read_by_to_user', defaultValue: false) + bool get isRead; + @override + @JsonKey(name: 'is_author_message_auth_user') + bool? get isCurrentUserAuthor; + @override + @JsonKey(name: 'room_id') + String? get roomId; /// Create a copy of ChatMessageModel /// with the given fields replaced by the non-null parameter values. diff --git a/lib/features/chat/models/chat_message_model.g.dart b/lib/features/chat/models/chat_message_model.g.dart index 3a8d1827..de7ea3c3 100644 --- a/lib/features/chat/models/chat_message_model.g.dart +++ b/lib/features/chat/models/chat_message_model.g.dart @@ -13,7 +13,10 @@ _$ChatMessageModelImpl _$$ChatMessageModelImplFromJson( data: json['data'] as String, mediaData: json['media_data'] as List?, authorId: (json['author_id'] as num).toInt(), - updatedAt: json['updated_at'] as String, + updatedAt: parseDate(json['updated_at'] as String), + isRead: json['read_by_to_user'] as bool? ?? false, + isCurrentUserAuthor: json['is_author_message_auth_user'] as bool?, + roomId: json['room_id'] as String?, ); Map _$$ChatMessageModelImplToJson( @@ -23,5 +26,8 @@ Map _$$ChatMessageModelImplToJson( 'data': instance.data, 'media_data': instance.mediaData, 'author_id': instance.authorId, - 'updated_at': instance.updatedAt, + 'updated_at': instance.updatedAt.toIso8601String(), + 'read_by_to_user': instance.isRead, + 'is_author_message_auth_user': instance.isCurrentUserAuthor, + 'room_id': instance.roomId, }; diff --git a/lib/features/chat/models/chat_room_preview_model.dart b/lib/features/chat/models/chat_room_preview_model.dart index 29c5e3ac..bc6e4b5d 100644 --- a/lib/features/chat/models/chat_room_preview_model.dart +++ b/lib/features/chat/models/chat_room_preview_model.dart @@ -1,3 +1,4 @@ +import 'package:eatvio_app/features/chat/models/chat_message_model.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; part 'chat_room_preview_model.freezed.dart'; @@ -17,26 +18,19 @@ class ChatRoom with _$ChatRoom { const factory ChatRoom({ @JsonKey(name: "id") required String id, @JsonKey(name: "room_name") required String roomName, - @JsonKey(name: "last_message") required dynamic lastMessage, + @JsonKey(name: "last_message", fromJson: parseLastMessage) required ChatMessageModel? lastMessage, @JsonKey(name: "users") required Users users, + @JsonKey(name: "unread_messages_count") required int unreadMessagesCount, }) = _ChatRoom; factory ChatRoom.fromJson(Map json) => _$ChatRoomFromJson(json); } -@freezed -class LastMessageClass with _$LastMessageClass { - const factory LastMessageClass({ - @JsonKey(name: "id") required int id, - @JsonKey(name: "data") required String data, - @JsonKey(name: "media_data") required List mediaData, - @JsonKey(name: "author_id") required int authorId, - @JsonKey(name: "is_author_message_auth_user") required bool isAuthorMessageAuthUser, - @JsonKey(name: "read_by_to_user") required bool readByToUser, - @JsonKey(name: "updated_at") required DateTime updatedAt, - }) = _LastMessageClass; - - factory LastMessageClass.fromJson(Map json) => _$LastMessageClassFromJson(json); +ChatMessageModel? parseLastMessage(dynamic json) { + if (json is List) { + return null; + } + return ChatMessageModel.fromJson(json); } @freezed diff --git a/lib/features/chat/models/chat_room_preview_model.freezed.dart b/lib/features/chat/models/chat_room_preview_model.freezed.dart index 4b006cf0..940c3106 100644 --- a/lib/features/chat/models/chat_room_preview_model.freezed.dart +++ b/lib/features/chat/models/chat_room_preview_model.freezed.dart @@ -190,10 +190,12 @@ mixin _$ChatRoom { String get id => throw _privateConstructorUsedError; @JsonKey(name: "room_name") String get roomName => throw _privateConstructorUsedError; - @JsonKey(name: "last_message") - dynamic get lastMessage => throw _privateConstructorUsedError; + @JsonKey(name: "last_message", fromJson: parseLastMessage) + ChatMessageModel? get lastMessage => throw _privateConstructorUsedError; @JsonKey(name: "users") Users get users => throw _privateConstructorUsedError; + @JsonKey(name: "unread_messages_count") + int get unreadMessagesCount => throw _privateConstructorUsedError; /// Serializes this ChatRoom to a JSON map. Map toJson() => throw _privateConstructorUsedError; @@ -213,9 +215,12 @@ abstract class $ChatRoomCopyWith<$Res> { $Res call( {@JsonKey(name: "id") String id, @JsonKey(name: "room_name") String roomName, - @JsonKey(name: "last_message") dynamic lastMessage, - @JsonKey(name: "users") Users users}); + @JsonKey(name: "last_message", fromJson: parseLastMessage) + ChatMessageModel? lastMessage, + @JsonKey(name: "users") Users users, + @JsonKey(name: "unread_messages_count") int unreadMessagesCount}); + $ChatMessageModelCopyWith<$Res>? get lastMessage; $UsersCopyWith<$Res> get users; } @@ -238,6 +243,7 @@ class _$ChatRoomCopyWithImpl<$Res, $Val extends ChatRoom> Object? roomName = null, Object? lastMessage = freezed, Object? users = null, + Object? unreadMessagesCount = null, }) { return _then(_value.copyWith( id: null == id @@ -251,14 +257,32 @@ class _$ChatRoomCopyWithImpl<$Res, $Val extends ChatRoom> lastMessage: freezed == lastMessage ? _value.lastMessage : lastMessage // ignore: cast_nullable_to_non_nullable - as dynamic, + as ChatMessageModel?, users: null == users ? _value.users : users // ignore: cast_nullable_to_non_nullable as Users, + unreadMessagesCount: null == unreadMessagesCount + ? _value.unreadMessagesCount + : unreadMessagesCount // ignore: cast_nullable_to_non_nullable + as int, ) as $Val); } + /// Create a copy of ChatRoom + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $ChatMessageModelCopyWith<$Res>? get lastMessage { + if (_value.lastMessage == null) { + return null; + } + + return $ChatMessageModelCopyWith<$Res>(_value.lastMessage!, (value) { + return _then(_value.copyWith(lastMessage: value) as $Val); + }); + } + /// Create a copy of ChatRoom /// with the given fields replaced by the non-null parameter values. @override @@ -281,9 +305,13 @@ abstract class _$$ChatRoomImplCopyWith<$Res> $Res call( {@JsonKey(name: "id") String id, @JsonKey(name: "room_name") String roomName, - @JsonKey(name: "last_message") dynamic lastMessage, - @JsonKey(name: "users") Users users}); + @JsonKey(name: "last_message", fromJson: parseLastMessage) + ChatMessageModel? lastMessage, + @JsonKey(name: "users") Users users, + @JsonKey(name: "unread_messages_count") int unreadMessagesCount}); + @override + $ChatMessageModelCopyWith<$Res>? get lastMessage; @override $UsersCopyWith<$Res> get users; } @@ -305,6 +333,7 @@ class __$$ChatRoomImplCopyWithImpl<$Res> Object? roomName = null, Object? lastMessage = freezed, Object? users = null, + Object? unreadMessagesCount = null, }) { return _then(_$ChatRoomImpl( id: null == id @@ -318,11 +347,15 @@ class __$$ChatRoomImplCopyWithImpl<$Res> lastMessage: freezed == lastMessage ? _value.lastMessage : lastMessage // ignore: cast_nullable_to_non_nullable - as dynamic, + as ChatMessageModel?, users: null == users ? _value.users : users // ignore: cast_nullable_to_non_nullable as Users, + unreadMessagesCount: null == unreadMessagesCount + ? _value.unreadMessagesCount + : unreadMessagesCount // ignore: cast_nullable_to_non_nullable + as int, )); } } @@ -333,8 +366,11 @@ class _$ChatRoomImpl implements _ChatRoom { const _$ChatRoomImpl( {@JsonKey(name: "id") required this.id, @JsonKey(name: "room_name") required this.roomName, - @JsonKey(name: "last_message") required this.lastMessage, - @JsonKey(name: "users") required this.users}); + @JsonKey(name: "last_message", fromJson: parseLastMessage) + required this.lastMessage, + @JsonKey(name: "users") required this.users, + @JsonKey(name: "unread_messages_count") + required this.unreadMessagesCount}); factory _$ChatRoomImpl.fromJson(Map json) => _$$ChatRoomImplFromJson(json); @@ -346,15 +382,18 @@ class _$ChatRoomImpl implements _ChatRoom { @JsonKey(name: "room_name") final String roomName; @override - @JsonKey(name: "last_message") - final dynamic lastMessage; + @JsonKey(name: "last_message", fromJson: parseLastMessage) + final ChatMessageModel? lastMessage; @override @JsonKey(name: "users") final Users users; + @override + @JsonKey(name: "unread_messages_count") + final int unreadMessagesCount; @override String toString() { - return 'ChatRoom(id: $id, roomName: $roomName, lastMessage: $lastMessage, users: $users)'; + return 'ChatRoom(id: $id, roomName: $roomName, lastMessage: $lastMessage, users: $users, unreadMessagesCount: $unreadMessagesCount)'; } @override @@ -365,15 +404,17 @@ class _$ChatRoomImpl implements _ChatRoom { (identical(other.id, id) || other.id == id) && (identical(other.roomName, roomName) || other.roomName == roomName) && - const DeepCollectionEquality() - .equals(other.lastMessage, lastMessage) && - (identical(other.users, users) || other.users == users)); + (identical(other.lastMessage, lastMessage) || + other.lastMessage == lastMessage) && + (identical(other.users, users) || other.users == users) && + (identical(other.unreadMessagesCount, unreadMessagesCount) || + other.unreadMessagesCount == unreadMessagesCount)); } @JsonKey(includeFromJson: false, includeToJson: false) @override - int get hashCode => Object.hash(runtimeType, id, roomName, - const DeepCollectionEquality().hash(lastMessage), users); + int get hashCode => Object.hash( + runtimeType, id, roomName, lastMessage, users, unreadMessagesCount); /// Create a copy of ChatRoom /// with the given fields replaced by the non-null parameter values. @@ -395,8 +436,11 @@ abstract class _ChatRoom implements ChatRoom { const factory _ChatRoom( {@JsonKey(name: "id") required final String id, @JsonKey(name: "room_name") required final String roomName, - @JsonKey(name: "last_message") required final dynamic lastMessage, - @JsonKey(name: "users") required final Users users}) = _$ChatRoomImpl; + @JsonKey(name: "last_message", fromJson: parseLastMessage) + required final ChatMessageModel? lastMessage, + @JsonKey(name: "users") required final Users users, + @JsonKey(name: "unread_messages_count") + required final int unreadMessagesCount}) = _$ChatRoomImpl; factory _ChatRoom.fromJson(Map json) = _$ChatRoomImpl.fromJson; @@ -408,11 +452,14 @@ abstract class _ChatRoom implements ChatRoom { @JsonKey(name: "room_name") String get roomName; @override - @JsonKey(name: "last_message") - dynamic get lastMessage; + @JsonKey(name: "last_message", fromJson: parseLastMessage) + ChatMessageModel? get lastMessage; @override @JsonKey(name: "users") Users get users; + @override + @JsonKey(name: "unread_messages_count") + int get unreadMessagesCount; /// Create a copy of ChatRoom /// with the given fields replaced by the non-null parameter values. @@ -422,327 +469,6 @@ abstract class _ChatRoom implements ChatRoom { throw _privateConstructorUsedError; } -LastMessageClass _$LastMessageClassFromJson(Map json) { - return _LastMessageClass.fromJson(json); -} - -/// @nodoc -mixin _$LastMessageClass { - @JsonKey(name: "id") - int get id => throw _privateConstructorUsedError; - @JsonKey(name: "data") - String get data => throw _privateConstructorUsedError; - @JsonKey(name: "media_data") - List get mediaData => throw _privateConstructorUsedError; - @JsonKey(name: "author_id") - int get authorId => throw _privateConstructorUsedError; - @JsonKey(name: "is_author_message_auth_user") - bool get isAuthorMessageAuthUser => throw _privateConstructorUsedError; - @JsonKey(name: "read_by_to_user") - bool get readByToUser => throw _privateConstructorUsedError; - @JsonKey(name: "updated_at") - DateTime get updatedAt => throw _privateConstructorUsedError; - - /// Serializes this LastMessageClass to a JSON map. - Map toJson() => throw _privateConstructorUsedError; - - /// Create a copy of LastMessageClass - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - $LastMessageClassCopyWith get copyWith => - throw _privateConstructorUsedError; -} - -/// @nodoc -abstract class $LastMessageClassCopyWith<$Res> { - factory $LastMessageClassCopyWith( - LastMessageClass value, $Res Function(LastMessageClass) then) = - _$LastMessageClassCopyWithImpl<$Res, LastMessageClass>; - @useResult - $Res call( - {@JsonKey(name: "id") int id, - @JsonKey(name: "data") String data, - @JsonKey(name: "media_data") List mediaData, - @JsonKey(name: "author_id") int authorId, - @JsonKey(name: "is_author_message_auth_user") - bool isAuthorMessageAuthUser, - @JsonKey(name: "read_by_to_user") bool readByToUser, - @JsonKey(name: "updated_at") DateTime updatedAt}); -} - -/// @nodoc -class _$LastMessageClassCopyWithImpl<$Res, $Val extends LastMessageClass> - implements $LastMessageClassCopyWith<$Res> { - _$LastMessageClassCopyWithImpl(this._value, this._then); - - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; - - /// Create a copy of LastMessageClass - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? id = null, - Object? data = null, - Object? mediaData = null, - Object? authorId = null, - Object? isAuthorMessageAuthUser = null, - Object? readByToUser = null, - Object? updatedAt = null, - }) { - return _then(_value.copyWith( - id: null == id - ? _value.id - : id // ignore: cast_nullable_to_non_nullable - as int, - data: null == data - ? _value.data - : data // ignore: cast_nullable_to_non_nullable - as String, - mediaData: null == mediaData - ? _value.mediaData - : mediaData // ignore: cast_nullable_to_non_nullable - as List, - authorId: null == authorId - ? _value.authorId - : authorId // ignore: cast_nullable_to_non_nullable - as int, - isAuthorMessageAuthUser: null == isAuthorMessageAuthUser - ? _value.isAuthorMessageAuthUser - : isAuthorMessageAuthUser // ignore: cast_nullable_to_non_nullable - as bool, - readByToUser: null == readByToUser - ? _value.readByToUser - : readByToUser // ignore: cast_nullable_to_non_nullable - as bool, - updatedAt: null == updatedAt - ? _value.updatedAt - : updatedAt // ignore: cast_nullable_to_non_nullable - as DateTime, - ) as $Val); - } -} - -/// @nodoc -abstract class _$$LastMessageClassImplCopyWith<$Res> - implements $LastMessageClassCopyWith<$Res> { - factory _$$LastMessageClassImplCopyWith(_$LastMessageClassImpl value, - $Res Function(_$LastMessageClassImpl) then) = - __$$LastMessageClassImplCopyWithImpl<$Res>; - @override - @useResult - $Res call( - {@JsonKey(name: "id") int id, - @JsonKey(name: "data") String data, - @JsonKey(name: "media_data") List mediaData, - @JsonKey(name: "author_id") int authorId, - @JsonKey(name: "is_author_message_auth_user") - bool isAuthorMessageAuthUser, - @JsonKey(name: "read_by_to_user") bool readByToUser, - @JsonKey(name: "updated_at") DateTime updatedAt}); -} - -/// @nodoc -class __$$LastMessageClassImplCopyWithImpl<$Res> - extends _$LastMessageClassCopyWithImpl<$Res, _$LastMessageClassImpl> - implements _$$LastMessageClassImplCopyWith<$Res> { - __$$LastMessageClassImplCopyWithImpl(_$LastMessageClassImpl _value, - $Res Function(_$LastMessageClassImpl) _then) - : super(_value, _then); - - /// Create a copy of LastMessageClass - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? id = null, - Object? data = null, - Object? mediaData = null, - Object? authorId = null, - Object? isAuthorMessageAuthUser = null, - Object? readByToUser = null, - Object? updatedAt = null, - }) { - return _then(_$LastMessageClassImpl( - id: null == id - ? _value.id - : id // ignore: cast_nullable_to_non_nullable - as int, - data: null == data - ? _value.data - : data // ignore: cast_nullable_to_non_nullable - as String, - mediaData: null == mediaData - ? _value._mediaData - : mediaData // ignore: cast_nullable_to_non_nullable - as List, - authorId: null == authorId - ? _value.authorId - : authorId // ignore: cast_nullable_to_non_nullable - as int, - isAuthorMessageAuthUser: null == isAuthorMessageAuthUser - ? _value.isAuthorMessageAuthUser - : isAuthorMessageAuthUser // ignore: cast_nullable_to_non_nullable - as bool, - readByToUser: null == readByToUser - ? _value.readByToUser - : readByToUser // ignore: cast_nullable_to_non_nullable - as bool, - updatedAt: null == updatedAt - ? _value.updatedAt - : updatedAt // ignore: cast_nullable_to_non_nullable - as DateTime, - )); - } -} - -/// @nodoc -@JsonSerializable() -class _$LastMessageClassImpl implements _LastMessageClass { - const _$LastMessageClassImpl( - {@JsonKey(name: "id") required this.id, - @JsonKey(name: "data") required this.data, - @JsonKey(name: "media_data") required final List mediaData, - @JsonKey(name: "author_id") required this.authorId, - @JsonKey(name: "is_author_message_auth_user") - required this.isAuthorMessageAuthUser, - @JsonKey(name: "read_by_to_user") required this.readByToUser, - @JsonKey(name: "updated_at") required this.updatedAt}) - : _mediaData = mediaData; - - factory _$LastMessageClassImpl.fromJson(Map json) => - _$$LastMessageClassImplFromJson(json); - - @override - @JsonKey(name: "id") - final int id; - @override - @JsonKey(name: "data") - final String data; - final List _mediaData; - @override - @JsonKey(name: "media_data") - List get mediaData { - if (_mediaData is EqualUnmodifiableListView) return _mediaData; - // ignore: implicit_dynamic_type - return EqualUnmodifiableListView(_mediaData); - } - - @override - @JsonKey(name: "author_id") - final int authorId; - @override - @JsonKey(name: "is_author_message_auth_user") - final bool isAuthorMessageAuthUser; - @override - @JsonKey(name: "read_by_to_user") - final bool readByToUser; - @override - @JsonKey(name: "updated_at") - final DateTime updatedAt; - - @override - String toString() { - return 'LastMessageClass(id: $id, data: $data, mediaData: $mediaData, authorId: $authorId, isAuthorMessageAuthUser: $isAuthorMessageAuthUser, readByToUser: $readByToUser, updatedAt: $updatedAt)'; - } - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$LastMessageClassImpl && - (identical(other.id, id) || other.id == id) && - (identical(other.data, data) || other.data == data) && - const DeepCollectionEquality() - .equals(other._mediaData, _mediaData) && - (identical(other.authorId, authorId) || - other.authorId == authorId) && - (identical( - other.isAuthorMessageAuthUser, isAuthorMessageAuthUser) || - other.isAuthorMessageAuthUser == isAuthorMessageAuthUser) && - (identical(other.readByToUser, readByToUser) || - other.readByToUser == readByToUser) && - (identical(other.updatedAt, updatedAt) || - other.updatedAt == updatedAt)); - } - - @JsonKey(includeFromJson: false, includeToJson: false) - @override - int get hashCode => Object.hash( - runtimeType, - id, - data, - const DeepCollectionEquality().hash(_mediaData), - authorId, - isAuthorMessageAuthUser, - readByToUser, - updatedAt); - - /// Create a copy of LastMessageClass - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - @override - @pragma('vm:prefer-inline') - _$$LastMessageClassImplCopyWith<_$LastMessageClassImpl> get copyWith => - __$$LastMessageClassImplCopyWithImpl<_$LastMessageClassImpl>( - this, _$identity); - - @override - Map toJson() { - return _$$LastMessageClassImplToJson( - this, - ); - } -} - -abstract class _LastMessageClass implements LastMessageClass { - const factory _LastMessageClass( - {@JsonKey(name: "id") required final int id, - @JsonKey(name: "data") required final String data, - @JsonKey(name: "media_data") required final List mediaData, - @JsonKey(name: "author_id") required final int authorId, - @JsonKey(name: "is_author_message_auth_user") - required final bool isAuthorMessageAuthUser, - @JsonKey(name: "read_by_to_user") required final bool readByToUser, - @JsonKey(name: "updated_at") required final DateTime updatedAt}) = - _$LastMessageClassImpl; - - factory _LastMessageClass.fromJson(Map json) = - _$LastMessageClassImpl.fromJson; - - @override - @JsonKey(name: "id") - int get id; - @override - @JsonKey(name: "data") - String get data; - @override - @JsonKey(name: "media_data") - List get mediaData; - @override - @JsonKey(name: "author_id") - int get authorId; - @override - @JsonKey(name: "is_author_message_auth_user") - bool get isAuthorMessageAuthUser; - @override - @JsonKey(name: "read_by_to_user") - bool get readByToUser; - @override - @JsonKey(name: "updated_at") - DateTime get updatedAt; - - /// Create a copy of LastMessageClass - /// with the given fields replaced by the non-null parameter values. - @override - @JsonKey(includeFromJson: false, includeToJson: false) - _$$LastMessageClassImplCopyWith<_$LastMessageClassImpl> get copyWith => - throw _privateConstructorUsedError; -} - Users _$UsersFromJson(Map json) { return _Users.fromJson(json); } diff --git a/lib/features/chat/models/chat_room_preview_model.g.dart b/lib/features/chat/models/chat_room_preview_model.g.dart index 5ae3f709..ad812d46 100644 --- a/lib/features/chat/models/chat_room_preview_model.g.dart +++ b/lib/features/chat/models/chat_room_preview_model.g.dart @@ -24,8 +24,9 @@ _$ChatRoomImpl _$$ChatRoomImplFromJson(Map json) => _$ChatRoomImpl( id: json['id'] as String, roomName: json['room_name'] as String, - lastMessage: json['last_message'], + lastMessage: parseLastMessage(json['last_message']), users: Users.fromJson(json['users'] as Map), + unreadMessagesCount: (json['unread_messages_count'] as num).toInt(), ); Map _$$ChatRoomImplToJson(_$ChatRoomImpl instance) => @@ -34,30 +35,7 @@ Map _$$ChatRoomImplToJson(_$ChatRoomImpl instance) => 'room_name': instance.roomName, 'last_message': instance.lastMessage, 'users': instance.users, - }; - -_$LastMessageClassImpl _$$LastMessageClassImplFromJson( - Map json) => - _$LastMessageClassImpl( - id: (json['id'] as num).toInt(), - data: json['data'] as String, - mediaData: json['media_data'] as List, - authorId: (json['author_id'] as num).toInt(), - isAuthorMessageAuthUser: json['is_author_message_auth_user'] as bool, - readByToUser: json['read_by_to_user'] as bool, - updatedAt: DateTime.parse(json['updated_at'] as String), - ); - -Map _$$LastMessageClassImplToJson( - _$LastMessageClassImpl instance) => - { - 'id': instance.id, - 'data': instance.data, - 'media_data': instance.mediaData, - 'author_id': instance.authorId, - 'is_author_message_auth_user': instance.isAuthorMessageAuthUser, - 'read_by_to_user': instance.readByToUser, - 'updated_at': instance.updatedAt.toIso8601String(), + 'unread_messages_count': instance.unreadMessagesCount, }; _$UsersImpl _$$UsersImplFromJson(Map json) => _$UsersImpl( diff --git a/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart b/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart index a072ad50..a7f94802 100644 --- a/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart +++ b/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart @@ -1,10 +1,13 @@ +import 'dart:async'; import 'dart:convert'; +import 'dart:developer'; import 'dart:io'; import 'package:audioplayers/audioplayers.dart'; +import 'package:dart_pusher_channels/dart_pusher_channels.dart'; import 'package:easy_debounce/easy_debounce.dart'; import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart'; -import 'package:eatvio_app/core/initialization_cubit/ws_manager.dart'; +import 'package:eatvio_app/core/websockets/ws_manager.dart'; import 'package:eatvio_app/features/chat/models/chat_message_model.dart'; import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_room_state.dart'; import 'package:eatvio_app/features/chat/repositories/chat_repository.dart'; @@ -14,6 +17,8 @@ class ChatRoomCubit extends Cubit { final ChatRepository chatRepository; final WsManager wsManager; + StreamSubscription? typingSubscription; + StreamSubscription? messagesSubscription; int currentPage = 1; late final int lastPage; bool isLoading = false; @@ -74,6 +79,9 @@ class ChatRoomCubit extends Cubit { ), ); } catch (e) { + typingSubscription?.cancel(); + messagesSubscription?.cancel(); + wsManager.unsubscribeDirectChatChannel(); emit(ChatRoomState.error(errorMessage: 'Ошибка получеия данных')); } } @@ -87,10 +95,12 @@ class ChatRoomCubit extends Cubit { wsManager.subscribeDirectChatChannel(); wsManager.directChatStream?.listen((event) { AudioPlayer().play((AssetSource('audio/recive_message.wav'))); + final message = ChatMessageModel.fromJson(jsonDecode(event.data)['message']); List messagesList = [ - ChatMessageModel.fromJson(jsonDecode(event.data)['message']), + message, ...(state as ChatRoomStateLoaded).chatMessages, ]; + chatRepository.emitNewMessageToChats(message); emit( ChatRoomState.loaded( chatRoom: (state as ChatRoomStateLoaded).chatRoom, @@ -125,17 +135,46 @@ class ChatRoomCubit extends Cubit { }); } - sendMessage({ + Future sendMessage({ required String chatId, String? data, List? files, }) async { - await chatRepository.sendMessage( - chatId: chatId, - data: data, - files: files != null ? await ImageCompressUtility.compressFilesList(files) : null, - ); - AudioPlayer().play((AssetSource('audio/send_message.wav'))); + try { + await chatRepository.sendMessage( + chatId: chatId, + data: data, + files: files != null ? await ImageCompressUtility.compressFilesList(files) : null, + ); + AudioPlayer().play((AssetSource('audio/send_message.wav'))); + return true; + } catch (e, st) { + log(e.toString(), stackTrace: st); + return false; + } + } + + Future markMessagesAsRead(String chatId, List<({int id, int index})> messagesIds) async { + try { + if (state is ChatRoomStateLoaded) { + final ids = messagesIds.map((e) => e.id).toList(); + await chatRepository.markMessagesAsRead(chatId, ids); + final currentMessages = [...(state as ChatRoomStateLoaded).chatMessages]; + messagesIds.forEach((elem) { + currentMessages[elem.index] = currentMessages[elem.index].copyWith(isRead: true); + }); + if (!isClosed) { + emit( + (state as ChatRoomStateLoaded).copyWith(chatMessages: currentMessages), + ); + } + return true; + } + return false; + } catch (e, st) { + log(e.toString(), stackTrace: st); + return false; + } } @override diff --git a/lib/features/chat/presentation/bloc/chat_select_cubit/chat_select_cubit.dart b/lib/features/chat/presentation/bloc/chat_select_cubit/chat_select_cubit.dart deleted file mode 100644 index bc8f7a96..00000000 --- a/lib/features/chat/presentation/bloc/chat_select_cubit/chat_select_cubit.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:eatvio_app/features/chat/presentation/bloc/chat_select_cubit/chat_select_state.dart'; -import 'package:eatvio_app/features/chat/repositories/chat_repository.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class ChatSelectCubit extends Cubit { - final ChatRepository chatRepository; - - ChatSelectCubit({ - required this.chatRepository, - }) : super(ChatSelectState()); - - getChatRooms({String? query}) async { - emit(ChatSelectState.loading()); - try { - final chatRooms = await chatRepository.getChatRoomPreviews(query: query); - emit( - ChatSelectState.loaded( - chatRooms: chatRooms, - ), - ); - } catch (e) { - emit( - ChatSelectState.error(errorMessage: e.toString()), - ); - } - } -} diff --git a/lib/features/chat/presentation/bloc/chat_select_cubit/chat_select_state.dart b/lib/features/chat/presentation/bloc/chat_select_cubit/chat_select_state.dart deleted file mode 100644 index eae74b97..00000000 --- a/lib/features/chat/presentation/bloc/chat_select_cubit/chat_select_state.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:eatvio_app/features/chat/models/chat_room_preview_model.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'chat_select_state.freezed.dart'; - -@freezed -abstract class ChatSelectState with _$ChatSelectState { - const factory ChatSelectState() = _ChatSelectState; - - const factory ChatSelectState.loading() = ChatSelectStateLoading; - - const factory ChatSelectState.error({ - required String errorMessage, - }) = ChatSelectStateError; - - const factory ChatSelectState.loaded({ - required List chatRooms, - }) = ChatSelectStateLoaded; -} diff --git a/lib/features/chat/presentation/bloc/chat_select_cubit/chat_select_state.freezed.dart b/lib/features/chat/presentation/bloc/chat_select_cubit/chat_select_state.freezed.dart deleted file mode 100644 index 99fa9054..00000000 --- a/lib/features/chat/presentation/bloc/chat_select_cubit/chat_select_state.freezed.dart +++ /dev/null @@ -1,646 +0,0 @@ -// 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 'chat_select_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 _$ChatSelectState { - @optionalTypeArgs - TResult when( - TResult Function() $default, { - required TResult Function() loading, - required TResult Function(String errorMessage) error, - required TResult Function(List chatRooms) loaded, - }) => - throw _privateConstructorUsedError; - @optionalTypeArgs - TResult? whenOrNull( - TResult? Function()? $default, { - TResult? Function()? loading, - TResult? Function(String errorMessage)? error, - TResult? Function(List chatRooms)? loaded, - }) => - throw _privateConstructorUsedError; - @optionalTypeArgs - TResult maybeWhen( - TResult Function()? $default, { - TResult Function()? loading, - TResult Function(String errorMessage)? error, - TResult Function(List chatRooms)? loaded, - required TResult orElse(), - }) => - throw _privateConstructorUsedError; - @optionalTypeArgs - TResult map( - TResult Function(_ChatSelectState value) $default, { - required TResult Function(ChatSelectStateLoading value) loading, - required TResult Function(ChatSelectStateError value) error, - required TResult Function(ChatSelectStateLoaded value) loaded, - }) => - throw _privateConstructorUsedError; - @optionalTypeArgs - TResult? mapOrNull( - TResult? Function(_ChatSelectState value)? $default, { - TResult? Function(ChatSelectStateLoading value)? loading, - TResult? Function(ChatSelectStateError value)? error, - TResult? Function(ChatSelectStateLoaded value)? loaded, - }) => - throw _privateConstructorUsedError; - @optionalTypeArgs - TResult maybeMap( - TResult Function(_ChatSelectState value)? $default, { - TResult Function(ChatSelectStateLoading value)? loading, - TResult Function(ChatSelectStateError value)? error, - TResult Function(ChatSelectStateLoaded value)? loaded, - required TResult orElse(), - }) => - throw _privateConstructorUsedError; -} - -/// @nodoc -abstract class $ChatSelectStateCopyWith<$Res> { - factory $ChatSelectStateCopyWith( - ChatSelectState value, $Res Function(ChatSelectState) then) = - _$ChatSelectStateCopyWithImpl<$Res, ChatSelectState>; -} - -/// @nodoc -class _$ChatSelectStateCopyWithImpl<$Res, $Val extends ChatSelectState> - implements $ChatSelectStateCopyWith<$Res> { - _$ChatSelectStateCopyWithImpl(this._value, this._then); - - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; - - /// Create a copy of ChatSelectState - /// with the given fields replaced by the non-null parameter values. -} - -/// @nodoc -abstract class _$$ChatSelectStateImplCopyWith<$Res> { - factory _$$ChatSelectStateImplCopyWith(_$ChatSelectStateImpl value, - $Res Function(_$ChatSelectStateImpl) then) = - __$$ChatSelectStateImplCopyWithImpl<$Res>; -} - -/// @nodoc -class __$$ChatSelectStateImplCopyWithImpl<$Res> - extends _$ChatSelectStateCopyWithImpl<$Res, _$ChatSelectStateImpl> - implements _$$ChatSelectStateImplCopyWith<$Res> { - __$$ChatSelectStateImplCopyWithImpl( - _$ChatSelectStateImpl _value, $Res Function(_$ChatSelectStateImpl) _then) - : super(_value, _then); - - /// Create a copy of ChatSelectState - /// with the given fields replaced by the non-null parameter values. -} - -/// @nodoc - -class _$ChatSelectStateImpl implements _ChatSelectState { - const _$ChatSelectStateImpl(); - - @override - String toString() { - return 'ChatSelectState()'; - } - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && other is _$ChatSelectStateImpl); - } - - @override - int get hashCode => runtimeType.hashCode; - - @override - @optionalTypeArgs - TResult when( - TResult Function() $default, { - required TResult Function() loading, - required TResult Function(String errorMessage) error, - required TResult Function(List chatRooms) loaded, - }) { - return $default(); - } - - @override - @optionalTypeArgs - TResult? whenOrNull( - TResult? Function()? $default, { - TResult? Function()? loading, - TResult? Function(String errorMessage)? error, - TResult? Function(List chatRooms)? loaded, - }) { - return $default?.call(); - } - - @override - @optionalTypeArgs - TResult maybeWhen( - TResult Function()? $default, { - TResult Function()? loading, - TResult Function(String errorMessage)? error, - TResult Function(List chatRooms)? loaded, - required TResult orElse(), - }) { - if ($default != null) { - return $default(); - } - return orElse(); - } - - @override - @optionalTypeArgs - TResult map( - TResult Function(_ChatSelectState value) $default, { - required TResult Function(ChatSelectStateLoading value) loading, - required TResult Function(ChatSelectStateError value) error, - required TResult Function(ChatSelectStateLoaded value) loaded, - }) { - return $default(this); - } - - @override - @optionalTypeArgs - TResult? mapOrNull( - TResult? Function(_ChatSelectState value)? $default, { - TResult? Function(ChatSelectStateLoading value)? loading, - TResult? Function(ChatSelectStateError value)? error, - TResult? Function(ChatSelectStateLoaded value)? loaded, - }) { - return $default?.call(this); - } - - @override - @optionalTypeArgs - TResult maybeMap( - TResult Function(_ChatSelectState value)? $default, { - TResult Function(ChatSelectStateLoading value)? loading, - TResult Function(ChatSelectStateError value)? error, - TResult Function(ChatSelectStateLoaded value)? loaded, - required TResult orElse(), - }) { - if ($default != null) { - return $default(this); - } - return orElse(); - } -} - -abstract class _ChatSelectState implements ChatSelectState { - const factory _ChatSelectState() = _$ChatSelectStateImpl; -} - -/// @nodoc -abstract class _$$ChatSelectStateLoadingImplCopyWith<$Res> { - factory _$$ChatSelectStateLoadingImplCopyWith( - _$ChatSelectStateLoadingImpl value, - $Res Function(_$ChatSelectStateLoadingImpl) then) = - __$$ChatSelectStateLoadingImplCopyWithImpl<$Res>; -} - -/// @nodoc -class __$$ChatSelectStateLoadingImplCopyWithImpl<$Res> - extends _$ChatSelectStateCopyWithImpl<$Res, _$ChatSelectStateLoadingImpl> - implements _$$ChatSelectStateLoadingImplCopyWith<$Res> { - __$$ChatSelectStateLoadingImplCopyWithImpl( - _$ChatSelectStateLoadingImpl _value, - $Res Function(_$ChatSelectStateLoadingImpl) _then) - : super(_value, _then); - - /// Create a copy of ChatSelectState - /// with the given fields replaced by the non-null parameter values. -} - -/// @nodoc - -class _$ChatSelectStateLoadingImpl implements ChatSelectStateLoading { - const _$ChatSelectStateLoadingImpl(); - - @override - String toString() { - return 'ChatSelectState.loading()'; - } - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$ChatSelectStateLoadingImpl); - } - - @override - int get hashCode => runtimeType.hashCode; - - @override - @optionalTypeArgs - TResult when( - TResult Function() $default, { - required TResult Function() loading, - required TResult Function(String errorMessage) error, - required TResult Function(List chatRooms) loaded, - }) { - return loading(); - } - - @override - @optionalTypeArgs - TResult? whenOrNull( - TResult? Function()? $default, { - TResult? Function()? loading, - TResult? Function(String errorMessage)? error, - TResult? Function(List chatRooms)? loaded, - }) { - return loading?.call(); - } - - @override - @optionalTypeArgs - TResult maybeWhen( - TResult Function()? $default, { - TResult Function()? loading, - TResult Function(String errorMessage)? error, - TResult Function(List chatRooms)? loaded, - required TResult orElse(), - }) { - if (loading != null) { - return loading(); - } - return orElse(); - } - - @override - @optionalTypeArgs - TResult map( - TResult Function(_ChatSelectState value) $default, { - required TResult Function(ChatSelectStateLoading value) loading, - required TResult Function(ChatSelectStateError value) error, - required TResult Function(ChatSelectStateLoaded value) loaded, - }) { - return loading(this); - } - - @override - @optionalTypeArgs - TResult? mapOrNull( - TResult? Function(_ChatSelectState value)? $default, { - TResult? Function(ChatSelectStateLoading value)? loading, - TResult? Function(ChatSelectStateError value)? error, - TResult? Function(ChatSelectStateLoaded value)? loaded, - }) { - return loading?.call(this); - } - - @override - @optionalTypeArgs - TResult maybeMap( - TResult Function(_ChatSelectState value)? $default, { - TResult Function(ChatSelectStateLoading value)? loading, - TResult Function(ChatSelectStateError value)? error, - TResult Function(ChatSelectStateLoaded value)? loaded, - required TResult orElse(), - }) { - if (loading != null) { - return loading(this); - } - return orElse(); - } -} - -abstract class ChatSelectStateLoading implements ChatSelectState { - const factory ChatSelectStateLoading() = _$ChatSelectStateLoadingImpl; -} - -/// @nodoc -abstract class _$$ChatSelectStateErrorImplCopyWith<$Res> { - factory _$$ChatSelectStateErrorImplCopyWith(_$ChatSelectStateErrorImpl value, - $Res Function(_$ChatSelectStateErrorImpl) then) = - __$$ChatSelectStateErrorImplCopyWithImpl<$Res>; - @useResult - $Res call({String errorMessage}); -} - -/// @nodoc -class __$$ChatSelectStateErrorImplCopyWithImpl<$Res> - extends _$ChatSelectStateCopyWithImpl<$Res, _$ChatSelectStateErrorImpl> - implements _$$ChatSelectStateErrorImplCopyWith<$Res> { - __$$ChatSelectStateErrorImplCopyWithImpl(_$ChatSelectStateErrorImpl _value, - $Res Function(_$ChatSelectStateErrorImpl) _then) - : super(_value, _then); - - /// Create a copy of ChatSelectState - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? errorMessage = null, - }) { - return _then(_$ChatSelectStateErrorImpl( - errorMessage: null == errorMessage - ? _value.errorMessage - : errorMessage // ignore: cast_nullable_to_non_nullable - as String, - )); - } -} - -/// @nodoc - -class _$ChatSelectStateErrorImpl implements ChatSelectStateError { - const _$ChatSelectStateErrorImpl({required this.errorMessage}); - - @override - final String errorMessage; - - @override - String toString() { - return 'ChatSelectState.error(errorMessage: $errorMessage)'; - } - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$ChatSelectStateErrorImpl && - (identical(other.errorMessage, errorMessage) || - other.errorMessage == errorMessage)); - } - - @override - int get hashCode => Object.hash(runtimeType, errorMessage); - - /// Create a copy of ChatSelectState - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - @override - @pragma('vm:prefer-inline') - _$$ChatSelectStateErrorImplCopyWith<_$ChatSelectStateErrorImpl> - get copyWith => - __$$ChatSelectStateErrorImplCopyWithImpl<_$ChatSelectStateErrorImpl>( - this, _$identity); - - @override - @optionalTypeArgs - TResult when( - TResult Function() $default, { - required TResult Function() loading, - required TResult Function(String errorMessage) error, - required TResult Function(List chatRooms) loaded, - }) { - return error(errorMessage); - } - - @override - @optionalTypeArgs - TResult? whenOrNull( - TResult? Function()? $default, { - TResult? Function()? loading, - TResult? Function(String errorMessage)? error, - TResult? Function(List chatRooms)? loaded, - }) { - return error?.call(errorMessage); - } - - @override - @optionalTypeArgs - TResult maybeWhen( - TResult Function()? $default, { - TResult Function()? loading, - TResult Function(String errorMessage)? error, - TResult Function(List chatRooms)? loaded, - required TResult orElse(), - }) { - if (error != null) { - return error(errorMessage); - } - return orElse(); - } - - @override - @optionalTypeArgs - TResult map( - TResult Function(_ChatSelectState value) $default, { - required TResult Function(ChatSelectStateLoading value) loading, - required TResult Function(ChatSelectStateError value) error, - required TResult Function(ChatSelectStateLoaded value) loaded, - }) { - return error(this); - } - - @override - @optionalTypeArgs - TResult? mapOrNull( - TResult? Function(_ChatSelectState value)? $default, { - TResult? Function(ChatSelectStateLoading value)? loading, - TResult? Function(ChatSelectStateError value)? error, - TResult? Function(ChatSelectStateLoaded value)? loaded, - }) { - return error?.call(this); - } - - @override - @optionalTypeArgs - TResult maybeMap( - TResult Function(_ChatSelectState value)? $default, { - TResult Function(ChatSelectStateLoading value)? loading, - TResult Function(ChatSelectStateError value)? error, - TResult Function(ChatSelectStateLoaded value)? loaded, - required TResult orElse(), - }) { - if (error != null) { - return error(this); - } - return orElse(); - } -} - -abstract class ChatSelectStateError implements ChatSelectState { - const factory ChatSelectStateError({required final String errorMessage}) = - _$ChatSelectStateErrorImpl; - - String get errorMessage; - - /// Create a copy of ChatSelectState - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - _$$ChatSelectStateErrorImplCopyWith<_$ChatSelectStateErrorImpl> - get copyWith => throw _privateConstructorUsedError; -} - -/// @nodoc -abstract class _$$ChatSelectStateLoadedImplCopyWith<$Res> { - factory _$$ChatSelectStateLoadedImplCopyWith( - _$ChatSelectStateLoadedImpl value, - $Res Function(_$ChatSelectStateLoadedImpl) then) = - __$$ChatSelectStateLoadedImplCopyWithImpl<$Res>; - @useResult - $Res call({List chatRooms}); -} - -/// @nodoc -class __$$ChatSelectStateLoadedImplCopyWithImpl<$Res> - extends _$ChatSelectStateCopyWithImpl<$Res, _$ChatSelectStateLoadedImpl> - implements _$$ChatSelectStateLoadedImplCopyWith<$Res> { - __$$ChatSelectStateLoadedImplCopyWithImpl(_$ChatSelectStateLoadedImpl _value, - $Res Function(_$ChatSelectStateLoadedImpl) _then) - : super(_value, _then); - - /// Create a copy of ChatSelectState - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? chatRooms = null, - }) { - return _then(_$ChatSelectStateLoadedImpl( - chatRooms: null == chatRooms - ? _value._chatRooms - : chatRooms // ignore: cast_nullable_to_non_nullable - as List, - )); - } -} - -/// @nodoc - -class _$ChatSelectStateLoadedImpl implements ChatSelectStateLoaded { - const _$ChatSelectStateLoadedImpl({required final List chatRooms}) - : _chatRooms = chatRooms; - - final List _chatRooms; - @override - List get chatRooms { - if (_chatRooms is EqualUnmodifiableListView) return _chatRooms; - // ignore: implicit_dynamic_type - return EqualUnmodifiableListView(_chatRooms); - } - - @override - String toString() { - return 'ChatSelectState.loaded(chatRooms: $chatRooms)'; - } - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$ChatSelectStateLoadedImpl && - const DeepCollectionEquality() - .equals(other._chatRooms, _chatRooms)); - } - - @override - int get hashCode => - Object.hash(runtimeType, const DeepCollectionEquality().hash(_chatRooms)); - - /// Create a copy of ChatSelectState - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - @override - @pragma('vm:prefer-inline') - _$$ChatSelectStateLoadedImplCopyWith<_$ChatSelectStateLoadedImpl> - get copyWith => __$$ChatSelectStateLoadedImplCopyWithImpl< - _$ChatSelectStateLoadedImpl>(this, _$identity); - - @override - @optionalTypeArgs - TResult when( - TResult Function() $default, { - required TResult Function() loading, - required TResult Function(String errorMessage) error, - required TResult Function(List chatRooms) loaded, - }) { - return loaded(chatRooms); - } - - @override - @optionalTypeArgs - TResult? whenOrNull( - TResult? Function()? $default, { - TResult? Function()? loading, - TResult? Function(String errorMessage)? error, - TResult? Function(List chatRooms)? loaded, - }) { - return loaded?.call(chatRooms); - } - - @override - @optionalTypeArgs - TResult maybeWhen( - TResult Function()? $default, { - TResult Function()? loading, - TResult Function(String errorMessage)? error, - TResult Function(List chatRooms)? loaded, - required TResult orElse(), - }) { - if (loaded != null) { - return loaded(chatRooms); - } - return orElse(); - } - - @override - @optionalTypeArgs - TResult map( - TResult Function(_ChatSelectState value) $default, { - required TResult Function(ChatSelectStateLoading value) loading, - required TResult Function(ChatSelectStateError value) error, - required TResult Function(ChatSelectStateLoaded value) loaded, - }) { - return loaded(this); - } - - @override - @optionalTypeArgs - TResult? mapOrNull( - TResult? Function(_ChatSelectState value)? $default, { - TResult? Function(ChatSelectStateLoading value)? loading, - TResult? Function(ChatSelectStateError value)? error, - TResult? Function(ChatSelectStateLoaded value)? loaded, - }) { - return loaded?.call(this); - } - - @override - @optionalTypeArgs - TResult maybeMap( - TResult Function(_ChatSelectState value)? $default, { - TResult Function(ChatSelectStateLoading value)? loading, - TResult Function(ChatSelectStateError value)? error, - TResult Function(ChatSelectStateLoaded value)? loaded, - required TResult orElse(), - }) { - if (loaded != null) { - return loaded(this); - } - return orElse(); - } -} - -abstract class ChatSelectStateLoaded implements ChatSelectState { - const factory ChatSelectStateLoaded( - {required final List chatRooms}) = _$ChatSelectStateLoadedImpl; - - List get chatRooms; - - /// Create a copy of ChatSelectState - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - _$$ChatSelectStateLoadedImplCopyWith<_$ChatSelectStateLoadedImpl> - get copyWith => throw _privateConstructorUsedError; -} diff --git a/lib/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart b/lib/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart new file mode 100644 index 00000000..f165ca83 --- /dev/null +++ b/lib/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart @@ -0,0 +1,144 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:eatvio_app/core/enums/base_state.dart'; +import 'package:eatvio_app/core/websockets/ws_manager.dart'; +import 'package:eatvio_app/features/chat/models/chat_message_model.dart'; +import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_state.dart'; +import 'package:eatvio_app/features/chat/repositories/chat_repository.dart'; +import 'package:eatvio_app/features/profile/repositories/profile_repository.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class ChatsCubit extends Cubit { + final ChatRepository chatRepository; + final ProfileRepository profileRepository; + final WsManager wsManager; + late StreamSubscription? newIncomingMessagesSubscription; + late StreamSubscription? newLastMessageSubscription; + + ChatsCubit({ + required this.chatRepository, + required this.wsManager, + required this.profileRepository, + }) : super(ChatsState()) { + newIncomingMessagesSubscription = wsManager.newMessagesStream?.listen((event) { + _handleNewMessageViaSocket(event.data); + }); + + newLastMessageSubscription = chatRepository.newUserMessagesStream.listen((message) { + _setNewLastMessage(message); + }); + } + String? _query; + Future getChatRoomsWithLoading({String? query}) async { + _query = query; + emit(state.copyWith(state: BaseState.loading)); + try { + final chatRooms = await chatRepository.getChatRoomPreviews(query: query); + final unreadChats = chatRooms.where( + (element) => element.unreadMessagesCount > 0, + ); + emit( + state.copyWith( + chatRooms: chatRooms, + chatsWithUnreadMessagesCount: unreadChats.length, + state: BaseState.success, + ), + ); + } catch (e) { + emit( + state.copyWith(errorMessage: e.toString(), state: BaseState.error), + ); + } + } + + Future getChatRooms({String? query}) async { + _query = query; + try { + final chatRooms = await chatRepository.getChatRoomPreviews(query: query); + final unreadChats = chatRooms.where( + (element) => element.unreadMessagesCount > 0, + ); + emit( + state.copyWith( + chatRooms: chatRooms, + chatsWithUnreadMessagesCount: unreadChats.length, + state: BaseState.success, + ), + ); + } catch (e) { + emit( + state.copyWith(errorMessage: e.toString(), state: BaseState.error), + ); + } + } + + void reduceUnreadMessagesCount({required String chatId, required int count}) { + bool isChatRead = false; + final updatedChatRooms = state.chatRooms.map((room) { + if (room.id == chatId) { + int unreadMessagesCount = room.unreadMessagesCount - count; + if (unreadMessagesCount <= 0) { + unreadMessagesCount = 0; + isChatRead = true; + } + return room.copyWith(unreadMessagesCount: unreadMessagesCount); + } + return room; + }).toList(); + emit( + state.copyWith( + chatRooms: updatedChatRooms, + chatsWithUnreadMessagesCount: isChatRead ? state.chatsWithUnreadMessagesCount - 1 : state.chatsWithUnreadMessagesCount, + ), + ); + } + + Future _handleNewMessageViaSocket(dynamic data) async { + final json = jsonDecode(data); + final message = ChatMessageModel.fromJson(json['message']); + + final currentChatRooms = [...state.chatRooms]; + + bool wasChanged = false; + bool willIncrementUnreadChats = false; + final updatedChatRooms = currentChatRooms.map((chatRoom) { + if (chatRoom.id == message.roomId) { + wasChanged = true; + if (chatRoom.unreadMessagesCount == 0) { + willIncrementUnreadChats = true; + } + return chatRoom.copyWith(lastMessage: message, unreadMessagesCount: chatRoom.unreadMessagesCount + 1); + } + return chatRoom; + }).toList(); + + if (!wasChanged && _query == null) { + getChatRooms(); + } else { + emit( + state.copyWith( + chatRooms: updatedChatRooms, + chatsWithUnreadMessagesCount: willIncrementUnreadChats ? state.chatsWithUnreadMessagesCount + 1 : state.chatsWithUnreadMessagesCount, + ), + ); + } + } + + void _setNewLastMessage(ChatMessageModel message) { + final chats = state.chatRooms.map((chat) { + if (chat.id == message.roomId) { + return chat.copyWith(lastMessage: message); + } + return chat; + }).toList(); + + emit(state.copyWith(chatRooms: chats)); + } + + @override + Future close() { + newIncomingMessagesSubscription?.cancel(); + return super.close(); + } +} diff --git a/lib/features/chat/presentation/bloc/chats_cubit/chats_state.dart b/lib/features/chat/presentation/bloc/chats_cubit/chats_state.dart new file mode 100644 index 00000000..24bde4fa --- /dev/null +++ b/lib/features/chat/presentation/bloc/chats_cubit/chats_state.dart @@ -0,0 +1,17 @@ +import 'package:eatvio_app/core/enums/base_state.dart'; +import 'package:eatvio_app/features/chat/models/chat_room_preview_model.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'chats_state.freezed.dart'; + +@freezed +class ChatsState with _$ChatsState { + + factory ChatsState({ + @Default(BaseState.initial) BaseState state, + @Default(0) int chatsWithUnreadMessagesCount, + @Default([]) List chatRooms, + @Default(null) String? errorMessage, + }) = _ChatsState; + +} \ No newline at end of file diff --git a/lib/features/chat/presentation/bloc/chats_cubit/chats_state.freezed.dart b/lib/features/chat/presentation/bloc/chats_cubit/chats_state.freezed.dart new file mode 100644 index 00000000..77fd911d --- /dev/null +++ b/lib/features/chat/presentation/bloc/chats_cubit/chats_state.freezed.dart @@ -0,0 +1,228 @@ +// 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 'chats_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 _$ChatsState { + BaseState get state => throw _privateConstructorUsedError; + int get chatsWithUnreadMessagesCount => throw _privateConstructorUsedError; + List get chatRooms => throw _privateConstructorUsedError; + String? get errorMessage => throw _privateConstructorUsedError; + + /// Create a copy of ChatsState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $ChatsStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ChatsStateCopyWith<$Res> { + factory $ChatsStateCopyWith( + ChatsState value, $Res Function(ChatsState) then) = + _$ChatsStateCopyWithImpl<$Res, ChatsState>; + @useResult + $Res call( + {BaseState state, + int chatsWithUnreadMessagesCount, + List chatRooms, + String? errorMessage}); +} + +/// @nodoc +class _$ChatsStateCopyWithImpl<$Res, $Val extends ChatsState> + implements $ChatsStateCopyWith<$Res> { + _$ChatsStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of ChatsState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? state = null, + Object? chatsWithUnreadMessagesCount = null, + Object? chatRooms = null, + Object? errorMessage = freezed, + }) { + return _then(_value.copyWith( + state: null == state + ? _value.state + : state // ignore: cast_nullable_to_non_nullable + as BaseState, + chatsWithUnreadMessagesCount: null == chatsWithUnreadMessagesCount + ? _value.chatsWithUnreadMessagesCount + : chatsWithUnreadMessagesCount // ignore: cast_nullable_to_non_nullable + as int, + chatRooms: null == chatRooms + ? _value.chatRooms + : chatRooms // ignore: cast_nullable_to_non_nullable + as List, + errorMessage: freezed == errorMessage + ? _value.errorMessage + : errorMessage // ignore: cast_nullable_to_non_nullable + as String?, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$ChatsStateImplCopyWith<$Res> + implements $ChatsStateCopyWith<$Res> { + factory _$$ChatsStateImplCopyWith( + _$ChatsStateImpl value, $Res Function(_$ChatsStateImpl) then) = + __$$ChatsStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {BaseState state, + int chatsWithUnreadMessagesCount, + List chatRooms, + String? errorMessage}); +} + +/// @nodoc +class __$$ChatsStateImplCopyWithImpl<$Res> + extends _$ChatsStateCopyWithImpl<$Res, _$ChatsStateImpl> + implements _$$ChatsStateImplCopyWith<$Res> { + __$$ChatsStateImplCopyWithImpl( + _$ChatsStateImpl _value, $Res Function(_$ChatsStateImpl) _then) + : super(_value, _then); + + /// Create a copy of ChatsState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? state = null, + Object? chatsWithUnreadMessagesCount = null, + Object? chatRooms = null, + Object? errorMessage = freezed, + }) { + return _then(_$ChatsStateImpl( + state: null == state + ? _value.state + : state // ignore: cast_nullable_to_non_nullable + as BaseState, + chatsWithUnreadMessagesCount: null == chatsWithUnreadMessagesCount + ? _value.chatsWithUnreadMessagesCount + : chatsWithUnreadMessagesCount // ignore: cast_nullable_to_non_nullable + as int, + chatRooms: null == chatRooms + ? _value._chatRooms + : chatRooms // ignore: cast_nullable_to_non_nullable + as List, + errorMessage: freezed == errorMessage + ? _value.errorMessage + : errorMessage // ignore: cast_nullable_to_non_nullable + as String?, + )); + } +} + +/// @nodoc + +class _$ChatsStateImpl implements _ChatsState { + _$ChatsStateImpl( + {this.state = BaseState.initial, + this.chatsWithUnreadMessagesCount = 0, + final List chatRooms = const [], + this.errorMessage = null}) + : _chatRooms = chatRooms; + + @override + @JsonKey() + final BaseState state; + @override + @JsonKey() + final int chatsWithUnreadMessagesCount; + final List _chatRooms; + @override + @JsonKey() + List get chatRooms { + if (_chatRooms is EqualUnmodifiableListView) return _chatRooms; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_chatRooms); + } + + @override + @JsonKey() + final String? errorMessage; + + @override + String toString() { + return 'ChatsState(state: $state, chatsWithUnreadMessagesCount: $chatsWithUnreadMessagesCount, chatRooms: $chatRooms, errorMessage: $errorMessage)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ChatsStateImpl && + (identical(other.state, state) || other.state == state) && + (identical(other.chatsWithUnreadMessagesCount, + chatsWithUnreadMessagesCount) || + other.chatsWithUnreadMessagesCount == + chatsWithUnreadMessagesCount) && + const DeepCollectionEquality() + .equals(other._chatRooms, _chatRooms) && + (identical(other.errorMessage, errorMessage) || + other.errorMessage == errorMessage)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + state, + chatsWithUnreadMessagesCount, + const DeepCollectionEquality().hash(_chatRooms), + errorMessage); + + /// Create a copy of ChatsState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$ChatsStateImplCopyWith<_$ChatsStateImpl> get copyWith => + __$$ChatsStateImplCopyWithImpl<_$ChatsStateImpl>(this, _$identity); +} + +abstract class _ChatsState implements ChatsState { + factory _ChatsState( + {final BaseState state, + final int chatsWithUnreadMessagesCount, + final List chatRooms, + final String? errorMessage}) = _$ChatsStateImpl; + + @override + BaseState get state; + @override + int get chatsWithUnreadMessagesCount; + @override + List get chatRooms; + @override + String? get errorMessage; + + /// Create a copy of ChatsState + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$ChatsStateImplCopyWith<_$ChatsStateImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/features/chat/presentation/pages/chat_screen.dart b/lib/features/chat/presentation/pages/chat_screen.dart index d446e4b2..4144fbd5 100644 --- a/lib/features/chat/presentation/pages/chat_screen.dart +++ b/lib/features/chat/presentation/pages/chat_screen.dart @@ -1,9 +1,12 @@ import 'dart:io'; import 'dart:math'; +import 'package:easy_debounce/easy_debounce.dart'; +import 'package:eatvio_app/core/widgets/data_error_widget.dart'; import 'package:eatvio_app/core/widgets/loader.dart'; import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart'; import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_room_state.dart'; +import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart'; import 'package:eatvio_app/features/chat/presentation/widgets/chat_bubble_widget.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/custom_text_style.dart'; @@ -12,12 +15,14 @@ import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:intl/intl.dart'; +import 'package:visibility_detector/visibility_detector.dart'; class ChatScreen extends StatefulWidget { final String chatId; - + final ChatsCubit chatsCubit; const ChatScreen({ required this.chatId, + required this.chatsCubit, super.key, }); @@ -29,6 +34,8 @@ class _ChatScreenState extends State { final scrollController = ScrollController(); final TextEditingController textEditingController = TextEditingController(); final ChatRoomCubit chatRoomCubit = sl(); + final List<({int id, int index})> unreadMessagesIds = []; + final List<({int id, int index})> unreadMessagesIdsCopy = []; @override void initState() { @@ -60,9 +67,7 @@ class _ChatScreenState extends State { } else if (state is ChatRoomStateError) { return Scaffold( appBar: AppBar(), - body: Center( - child: Text(state.errorMessage), - ), + body: DataErrorWidget(onReload: () => chatRoomCubit.initChat(widget.chatId)), ); } else { return Scaffold( @@ -152,25 +157,65 @@ class _ChatScreenState extends State { (context, index) { if (index < state.chatMessages.length) { if ((index + 1 != state.chatMessages.length && - DateTime.parse(state.chatMessages[index].updatedAt).day != DateTime.parse(state.chatMessages[index + 1].updatedAt).day) || + state.chatMessages[index].updatedAt.day != state.chatMessages[index + 1].updatedAt.day) || index + 1 == state.chatMessages.length) { + final isCurrentUserAuthor = state.chatRoom.users['to_chat_user']!.id != state.chatMessages[index].authorId; return Column( children: [ _messageDate(state.chatMessages[index].updatedAt), - ChatBubbleWidget( - sendTime: state.chatMessages[index].updatedAt, - text: state.chatMessages[index].data.trim(), - isCurrentUser: state.chatRoom.users['to_chat_user']!.id != state.chatMessages[index].authorId, - mediaData: state.chatMessages[index].mediaData ?? [], + VisibilityDetector( + key: Key(state.chatMessages[index].id.toString()), + onVisibilityChanged: (VisibilityInfo info) { + if (!state.chatMessages[index].isRead && !isCurrentUserAuthor && info.visibleFraction > 0) { + unreadMessagesIdsCopy.add((id: state.chatMessages[index].id, index: index)); + EasyDebounce.debounce( + 'mark_as_read', + Duration(milliseconds: 500), + () async { + final result = await context.read().markMessagesAsRead(state.chatRoom.id, unreadMessagesIdsCopy); + if (!result) return; + widget.chatsCubit.reduceUnreadMessagesCount(chatId: state.chatRoom.id, count: unreadMessagesIdsCopy.length); + unreadMessagesIdsCopy.clear(); + }, + ); + } + }, + child: ChatBubbleWidget( + isRead: state.chatMessages[index].isRead, + sendTime: state.chatMessages[index].updatedAt, + text: state.chatMessages[index].data.trim(), + isCurrentUser: isCurrentUserAuthor, + mediaData: state.chatMessages[index].mediaData ?? [], + ), ), ], ); } - return ChatBubbleWidget( - sendTime: state.chatMessages[index].updatedAt, - text: state.chatMessages[index].data.trim(), - isCurrentUser: state.chatRoom.users['to_chat_user']!.id != state.chatMessages[index].authorId, - mediaData: state.chatMessages[index].mediaData ?? [], + final isCurrentUserAuthor = state.chatRoom.users['to_chat_user']!.id != state.chatMessages[index].authorId; + return VisibilityDetector( + key: Key(state.chatMessages[index].id.toString()), + onVisibilityChanged: (VisibilityInfo info) { + if (!state.chatMessages[index].isRead && !isCurrentUserAuthor && info.visibleFraction > 0) { + unreadMessagesIds.add((id: state.chatMessages[index].id, index: index)); + EasyDebounce.debounce( + 'mark_as_read', + Duration(milliseconds: 500), + () async { + final result = await context.read().markMessagesAsRead(state.chatRoom.id, unreadMessagesIds); + if (!result) return; + widget.chatsCubit.reduceUnreadMessagesCount(chatId: state.chatRoom.id, count: unreadMessagesIds.length); + unreadMessagesIds.clear(); + }, + ); + } + }, + child: ChatBubbleWidget( + isRead: state.chatMessages[index].isRead, + sendTime: state.chatMessages[index].updatedAt, + text: state.chatMessages[index].data.trim(), + isCurrentUser: isCurrentUserAuthor, + mediaData: state.chatMessages[index].mediaData ?? [], + ), ); } else { return Padding( @@ -222,7 +267,7 @@ class _ChatScreenState extends State { if (result != null) { List files = result.paths.map((path) => File(path!)).toList(); - chatRoomCubit.sendMessage( + await chatRoomCubit.sendMessage( chatId: widget.chatId, files: files, ); @@ -329,13 +374,13 @@ class _ChatScreenState extends State { ); } - _messageDate(String date) { + _messageDate(DateTime date) { return Center( child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8), + padding: const EdgeInsets.only(top: 32), child: Text( DateFormat.yMMMMd('RU').format( - DateTime.parse(date), + date, ), style: const TextStyle( height: 1, diff --git a/lib/features/chat/presentation/pages/chat_select_screen.dart b/lib/features/chat/presentation/pages/chat_select_screen.dart deleted file mode 100644 index ce286468..00000000 --- a/lib/features/chat/presentation/pages/chat_select_screen.dart +++ /dev/null @@ -1,113 +0,0 @@ -import 'package:easy_debounce/easy_debounce.dart'; -import 'package:eatvio_app/core/widgets/loader.dart'; -import 'package:eatvio_app/features/chat/presentation/bloc/chat_select_cubit/chat_select_cubit.dart'; -import 'package:eatvio_app/features/chat/presentation/bloc/chat_select_cubit/chat_select_state.dart'; -import 'package:eatvio_app/features/chat/presentation/widgets/chat_entry_widget.dart'; -import 'package:eatvio_app/features/chat/presentation/widgets/chat_search_bar_widget.dart'; -import 'package:eatvio_app/core/locator_service.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class ChatSelectScreen extends StatefulWidget { - const ChatSelectScreen({Key? key}) : super(key: key); - - @override - State createState() => _ChatSelectScreenState(); -} - -class _ChatSelectScreenState extends State { - final ChatSelectCubit chatSelectCubit = sl(); - final searchController = TextEditingController(); - - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => chatSelectCubit..getChatRooms(), - child: BlocBuilder( - builder: (context, state) { - return Scaffold( - appBar: AppBar( - backgroundColor: Colors.white, - toolbarHeight: 0, - ), - body: NestedScrollView( - headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { - return [ - SliverPadding( - padding: const EdgeInsets.only(top: 8), - sliver: SliverAppBar( - automaticallyImplyLeading: false, - stretchTriggerOffset: 1, - scrolledUnderElevation: 0, - floating: true, - snap: true, - title: Padding( - padding: const EdgeInsets.only(top: 8), - child: ChatSearchBarWidget( - onSuffixIconTap: () => context.read().getChatRooms(), - onChanged: (query) { - if (query.length > 2) { - EasyDebounce.debounce( - 'searchChatRoom', - Duration(milliseconds: 300), - () => context.read().getChatRooms(query: query), - ); - } else { - context.read().getChatRooms(); - } - }, - controller: searchController, - fillColor: Color(0xFFF0F2F5), - hintText: "Поиск", - hintStyle: TextStyle( - color: Color(0xFF7B8794), - ), - ), - ), - ), - ), - ]; - }, - body: stateBuilder(state), - ), - ); - }, - ), - ); - } - - stateBuilder(state) { - if (state is ChatSelectStateLoading) { - return Scaffold( - body: const Loader(), - ); - } else if (state is ChatSelectStateLoaded) { - return _buildChatList(context, state); - } else if (state is ChatSelectStateError) { - return Padding( - padding: const EdgeInsets.all(16), - child: Center(child: Text(state.errorMessage)), - ); - } else { - return Scaffold( - body: const Loader(), - ); - } - } -} - -_buildChatList(BuildContext context, ChatSelectStateLoaded state) { - return ListView.separated( - padding: EdgeInsets.all(16), - shrinkWrap: true, - physics: NeverScrollableScrollPhysics(), - separatorBuilder: (context, index) { - return SizedBox( - height: 8, - ); - }, - itemCount: state.chatRooms.length, - itemBuilder: (context, index) { - return ChatEntryWidget(chatRoom: state.chatRooms[index]); - }, - ); -} diff --git a/lib/features/chat/presentation/pages/chats_screen.dart b/lib/features/chat/presentation/pages/chats_screen.dart new file mode 100644 index 00000000..18f2b581 --- /dev/null +++ b/lib/features/chat/presentation/pages/chats_screen.dart @@ -0,0 +1,109 @@ +import 'package:easy_debounce/easy_debounce.dart'; +import 'package:eatvio_app/core/enums/base_state.dart'; +import 'package:eatvio_app/core/widgets/data_error_widget.dart'; +import 'package:eatvio_app/core/widgets/loader.dart'; +import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart'; +import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_state.dart'; +import 'package:eatvio_app/features/chat/presentation/widgets/chat_entry_widget.dart'; +import 'package:eatvio_app/features/chat/presentation/widgets/chat_search_bar_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class ChatsScreen extends StatefulWidget { + const ChatsScreen({Key? key}) : super(key: key); + + @override + State createState() => _ChatsScreenState(); +} + +class _ChatsScreenState extends State { + final searchController = TextEditingController(); + + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return Scaffold( + appBar: AppBar( + backgroundColor: Colors.white, + toolbarHeight: 0, + ), + body: NestedScrollView( + headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { + return [ + SliverPadding( + padding: const EdgeInsets.only(top: 8), + sliver: SliverAppBar( + automaticallyImplyLeading: false, + stretchTriggerOffset: 1, + scrolledUnderElevation: 0, + floating: true, + snap: true, + title: Padding( + padding: const EdgeInsets.only(top: 8), + child: ChatSearchBarWidget( + onSuffixIconTap: () => context.read().getChatRooms(), + onChanged: (query) { + if (query.length > 2) { + EasyDebounce.debounce( + 'searchChatRoom', + Duration(milliseconds: 300), + () => context.read().getChatRooms(query: query), + ); + } else { + context.read().getChatRooms(); + } + }, + controller: searchController, + fillColor: Color(0xFFF0F2F5), + hintText: "Поиск", + hintStyle: TextStyle( + color: Color(0xFF7B8794), + ), + ), + ), + ), + ), + ]; + }, + body: stateBuilder(state), + ), + ); + }, + ); + } + + stateBuilder(ChatsState state) { + if (state.state == BaseState.loading) { + return Scaffold( + body: const Loader(), + ); + } else if (state.state == BaseState.success) { + return _buildChatList(context, state); + } else if (state.state == BaseState.error) { + return Scaffold( + body: DataErrorWidget(onReload: () => context.read().getChatRooms()), + ); + } else { + return Scaffold( + body: const Loader(), + ); + } + } + + _buildChatList(BuildContext context, ChatsState state) { + return ListView.separated( + padding: EdgeInsets.all(16), + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + separatorBuilder: (context, index) { + return SizedBox( + height: 8, + ); + }, + itemCount: state.chatRooms.length, + itemBuilder: (context, index) { + return ChatEntryWidget(chatRoom: state.chatRooms[index]); + }, + ); + } +} diff --git a/lib/features/chat/presentation/widgets/chat_bubble_widget.dart b/lib/features/chat/presentation/widgets/chat_bubble_widget.dart index 11c44530..195f19de 100644 --- a/lib/features/chat/presentation/widgets/chat_bubble_widget.dart +++ b/lib/features/chat/presentation/widgets/chat_bubble_widget.dart @@ -15,118 +15,146 @@ class ChatBubbleWidget extends StatelessWidget { required this.text, required this.isCurrentUser, required this.mediaData, + required this.isRead, }); - final String sendTime; + final DateTime sendTime; final String text; final bool isCurrentUser; + final bool isRead; final List mediaData; @override Widget build(BuildContext context) { - return Padding( - padding: EdgeInsets.fromLTRB(isCurrentUser ? 64 : 16, 4, isCurrentUser ? 16 : 64, 4), - child: Align( - alignment: isCurrentUser ? Alignment.centerRight : Alignment.centerLeft, - child: DecoratedBox( - decoration: BoxDecoration( - color: isCurrentUser ? appTheme.gray50 : appTheme.gray60004, - borderRadius: mediaData.length > 0 - ? BorderRadius.circular(24) - : BorderRadius.horizontal( - left: isCurrentUser ? Radius.circular(32) : Radius.circular(16), - right: isCurrentUser ? Radius.circular(16) : Radius.circular(24), - ), - ), - child: Padding( - padding: mediaData.length > 0 ? EdgeInsets.all(8) : EdgeInsets.only(top: 8, bottom: 8, left: isCurrentUser ? 16 : 8, right: isCurrentUser ? 8 : 16), - child: Column( - crossAxisAlignment: isCurrentUser ? CrossAxisAlignment.end : CrossAxisAlignment.start, - children: [ - if (mediaData.isNotEmpty) - CustomScrollView( - shrinkWrap: true, - physics: NeverScrollableScrollPhysics(), - slivers: [ - SliverList.separated( - itemBuilder: (BuildContext context, int index) { - if (!mediaData[index].toString().contains('.png') && - !mediaData[index].toString().contains('.jpg') && - !mediaData[index].toString().contains('.jpeg')) { - return ListTile( - contentPadding: EdgeInsets.all(4), - horizontalTitleGap: 8, - onTap: () async { - if (!await launchUrl(Uri.parse(baseUrl + '/storage/' + mediaData[index].toString()))) { - throw Exception('Could not launch ${mediaData[index].toString()}'); - } - }, - leading: Icon( - Icons.file_copy_outlined, - color: Colors.white, - ), - trailing: Icon( - Icons.download_outlined, - color: Colors.white, - ), - title: Text( - mediaData[index].toString().replaceAll('chat_files/', ''), - maxLines: 2, - style: TextStyle( - color: Colors.white, - fontSize: 12, - overflow: TextOverflow.ellipsis, - ), - ), - ); - } - return GestureDetector( - onTap: () { - context.pushTransparentRoute( - PhotoViewScreen( - imageUrls: mediaData.map((e) => baseUrl + '/storage/' + e.toString()).toList(), - index: index, - ), - ); - }, - child: Hero( - tag: baseUrl + '/storage/' + '${mediaData[index]}', - transitionOnUserGestures: true, - child: ClipRRect( - borderRadius: BorderRadius.circular(16), - child: CachedNetworkImage( - imageUrl: baseUrl + '/storage/' + '${mediaData[index]}', - ), - ), - ), - ); - }, - separatorBuilder: (BuildContext context, int index) => const SizedBox(height: 8), - itemCount: mediaData.length, - ), - ], - ), - if (mediaData.length > 0 && text.isNotEmpty) SizedBox(height: 8), - if (text.isNotEmpty) - Text( - text, - style: TextStyles.textFS13FW400.copyWith(color: appTheme.black, fontSize: 16), - ), - SizedBox(height: 8), - Padding( - padding: mediaData.length > 0 ? const EdgeInsets.only(right: 8, top: 4, left: 8) : EdgeInsets.zero, - child: Text( - DateTime.parse(sendTime).format('hh:mm'), - style: TextStyle( - color: Color(0xFF7B8794), - height: 1, - fontSize: 10, - ), + final size = MediaQuery.sizeOf(context); + return Container( + width: size.width, + child: Padding( + padding: EdgeInsets.fromLTRB(isCurrentUser ? 64 : 16, 10, isCurrentUser ? 16 : 64, 4), + child: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisAlignment: isCurrentUser ? MainAxisAlignment.end : MainAxisAlignment.start, + children: [ + if (isCurrentUser) + Padding( + padding: const EdgeInsets.only(right: 4, bottom: 4), + child: Text( + sendTime.format('hh:mm'), + style: TextStyle( + color: Color(0xFF7B8794), + height: 1, + fontSize: 10, ), ), - ], + ), + Container( + constraints: BoxConstraints(maxWidth: size.width * 0.7), + decoration: BoxDecoration( + color: isCurrentUser ? appTheme.gray50 : appTheme.gray60004, + borderRadius: mediaData.length > 0 + ? BorderRadius.circular(24) + : BorderRadius.horizontal( + left: isCurrentUser ? Radius.circular(32) : Radius.circular(16), + right: isCurrentUser ? Radius.circular(16) : Radius.circular(24), + ), + ), + child: Padding( + padding: + mediaData.isNotEmpty ? EdgeInsets.all(8) : EdgeInsets.only(top: 8, bottom: 8, left: isCurrentUser ? 16 : 8, right: isCurrentUser ? 8 : 16), + child: Column( + crossAxisAlignment: isCurrentUser ? CrossAxisAlignment.end : CrossAxisAlignment.start, + children: [ + if (mediaData.isNotEmpty) + SizedBox( + width: size.width * 0.7, + child: CustomScrollView( + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + slivers: [ + SliverList.separated( + itemBuilder: (BuildContext context, int index) { + if (!mediaData[index].toString().contains('.png') && + !mediaData[index].toString().contains('.jpg') && + !mediaData[index].toString().contains('.jpeg')) { + return ListTile( + contentPadding: EdgeInsets.all(4), + horizontalTitleGap: 8, + onTap: () async { + if (!await launchUrl(Uri.parse(baseUrl + '/storage/' + mediaData[index].toString()))) { + throw Exception('Could not launch ${mediaData[index].toString()}'); + } + }, + leading: Icon( + Icons.file_copy_outlined, + color: Colors.white, + ), + trailing: Icon( + Icons.download_outlined, + color: Colors.white, + ), + title: Text( + mediaData[index].toString().replaceAll('chat_files/', ''), + maxLines: 2, + style: TextStyle( + color: Colors.white, + fontSize: 12, + overflow: TextOverflow.ellipsis, + ), + ), + ); + } + return GestureDetector( + onTap: () { + context.pushTransparentRoute( + PhotoViewScreen( + imageUrls: mediaData.map((e) => baseUrl + '/storage/' + e.toString()).toList(), + index: index, + ), + ); + }, + child: Hero( + tag: baseUrl + '/storage/' + '${mediaData[index]}', + transitionOnUserGestures: true, + child: ClipRRect( + borderRadius: BorderRadius.circular(16), + child: CachedNetworkImage( + imageUrl: baseUrl + '/storage/' + '${mediaData[index]}', + ), + ), + ), + ); + }, + separatorBuilder: (BuildContext context, int index) => const SizedBox(height: 8), + itemCount: mediaData.length, + ), + ], + ), + ), + if (mediaData.isNotEmpty && text.isNotEmpty) SizedBox(height: 8), + if (text.isNotEmpty) + Text( + text, + style: TextStyles.textFS13FW400.copyWith(color: appTheme.black, fontSize: 16), + ), + ], + ), + ), ), - ), + if (!isCurrentUser) + Padding( + padding: const EdgeInsets.only(left: 4, bottom: 4), + child: Text( + sendTime.format('hh:mm'), + style: TextStyle( + color: Color(0xFF7B8794), + height: 1, + fontSize: 10, + fontWeight: FontWeight.w500, + ), + ), + ), + ], ), ), ); diff --git a/lib/features/chat/presentation/widgets/chat_entry_widget.dart b/lib/features/chat/presentation/widgets/chat_entry_widget.dart index 34b13c94..61e6ea17 100644 --- a/lib/features/chat/presentation/widgets/chat_entry_widget.dart +++ b/lib/features/chat/presentation/widgets/chat_entry_widget.dart @@ -2,7 +2,9 @@ import 'package:eatvio_app/core/utils/date_time_utils.dart'; import 'package:eatvio_app/features/chat/models/chat_room_preview_model.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/chat/presentation/bloc/chats_cubit/chats_cubit.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; class ChatEntryWidget extends StatefulWidget { @@ -18,26 +20,19 @@ class ChatEntryWidget extends StatefulWidget { } class _ChatEntryWidgetState extends State { - LastMessageClass? lastMessage; - - @override - void initState() { - super.initState(); - if (widget.chatRoom.lastMessage is Map) { - lastMessage = LastMessageClass.fromJson(widget.chatRoom.lastMessage); - } - } - @override Widget build(BuildContext context) { return InkWell( borderRadius: BorderRadius.circular(16), onTap: () => GoRouter.of(context).pushNamed( 'chatScreen', - extra: widget.chatRoom.id.toString(), + extra: { + 'chatId': widget.chatRoom.id.toString(), + 'chatsCubit': context.read(), + }, ), child: Ink( - padding: EdgeInsets.all(16), + padding: EdgeInsets.all(12), decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), color: Color(0xFFF6FAFF), @@ -54,39 +49,54 @@ class _ChatEntryWidgetState extends State { backgroundColor: Colors.white, ), const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Text( - widget.chatRoom.users.toChatUser.name.length <= 20 - ? widget.chatRoom.users.toChatUser.name - : widget.chatRoom.users.toChatUser.name.substring(0, 20) + '...', - style: theme.textTheme.titleMedium, - ), - Spacer(), - if (lastMessage != null) - Padding( - padding: EdgeInsets.only(left: 8), - child: Text( - lastMessage!.updatedAt.format('dd MMM', 'ru'), - style: theme.textTheme.bodyMedium, - ), - ), - ], - ), - SizedBox(height: 8), - if (lastMessage != null) - Text( - lastMessage!.data.toString().trim(), - maxLines: 2, + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.chatRoom.users.toChatUser.name.length <= 20 + ? widget.chatRoom.users.toChatUser.name + : widget.chatRoom.users.toChatUser.name.substring(0, 20) + '...', + style: theme.textTheme.titleMedium, + ), + SizedBox(height: 8), + if (widget.chatRoom.lastMessage != null) + SizedBox( + width: MediaQuery.sizeOf(context).width * 0.5, + child: Text( + widget.chatRoom.lastMessage!.data.toString().trim(), + maxLines: 1, overflow: TextOverflow.ellipsis, style: CustomTextStyles.bodyMediumBluegray800, ), - ], - ), + ), + ], + ), + Spacer(), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + if (widget.chatRoom.lastMessage != null) + Padding( + padding: EdgeInsets.only(left: 8), + child: Text( + widget.chatRoom.lastMessage!.updatedAt.format('dd MMM', 'ru'), + style: theme.textTheme.bodyMedium, + ), + ), + SizedBox(height: 4), + if (widget.chatRoom.unreadMessagesCount > 0) + Container( + padding: EdgeInsets.symmetric(horizontal: 8, vertical: 6), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: appTheme.blueA400, + ), + child: Text( + widget.chatRoom.unreadMessagesCount.toString(), + style: TextStyles.textXSSemibold.copyWith(color: appTheme.whiteA700), + ), + ), + ], ), ], ), diff --git a/lib/features/chat/repositories/chat_repository.dart b/lib/features/chat/repositories/chat_repository.dart index 67b49b73..9458d9f5 100644 --- a/lib/features/chat/repositories/chat_repository.dart +++ b/lib/features/chat/repositories/chat_repository.dart @@ -1,6 +1,8 @@ +import 'dart:async'; import 'dart:io'; import 'package:dio/dio.dart'; +import 'package:eatvio_app/features/chat/models/chat_message_model.dart'; import 'package:eatvio_app/features/chat/models/chat_messages_model.dart'; import 'package:eatvio_app/features/chat/models/chat_room_model.dart'; import 'package:eatvio_app/features/chat/models/chat_room_preview_model.dart'; @@ -10,10 +12,18 @@ import 'package:http_parser/http_parser.dart'; class ChatRepository { final ChatService chatService; + final _newUserMessagesController = StreamController(); + + Stream get newUserMessagesStream => _newUserMessagesController.stream; + ChatRepository({ required this.chatService, }); + void emitNewMessageToChats(ChatMessageModel message) { + _newUserMessagesController.add(message); + } + Future sendMessage({ required String chatId, String? data, @@ -66,7 +76,7 @@ class ChatRepository { } catch (e) { rethrow; } - } + } Future getChatRoomIdByUserSlug({required String slug}) async { try { @@ -75,4 +85,12 @@ class ChatRepository { rethrow; } } + + Future markMessagesAsRead(String chatId, List messageIds) async { + try { + await chatService.markMessagesAsRead(id: chatId, body: {'messages_ids': messageIds}); + } catch (e) { + rethrow; + } + } } diff --git a/lib/features/chat/services/chat_service.dart b/lib/features/chat/services/chat_service.dart index 499f2584..2eb2423f 100644 --- a/lib/features/chat/services/chat_service.dart +++ b/lib/features/chat/services/chat_service.dart @@ -38,4 +38,7 @@ abstract class ChatService { Future getChatRoomIdByUserSlug({ @Path() required String user, }); + + @POST('/api/chat/{id}/messages/markAsRead') + Future markMessagesAsRead({@Path() required String id, @Body() required Map> body}); } diff --git a/lib/features/chat/services/chat_service.g.dart b/lib/features/chat/services/chat_service.g.dart index 25c122bc..4cd570df 100644 --- a/lib/features/chat/services/chat_service.g.dart +++ b/lib/features/chat/services/chat_service.g.dart @@ -167,6 +167,29 @@ class _ChatService implements ChatService { return _value; } + @override + Future markMessagesAsRead({ + required String id, + required Map> body, + }) async { + final _extra = {}; + final queryParameters = {}; + final _headers = {}; + final _data = {}; + _data.addAll(body); + final _options = _setStreamType( + Options(method: 'POST', headers: _headers, extra: _extra) + .compose( + _dio.options, + '/api/chat/${id}/messages/markAsRead', + queryParameters: queryParameters, + data: _data, + ) + .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), + ); + await _dio.fetch(_options); + } + RequestOptions _setStreamType(RequestOptions requestOptions) { if (T != dynamic && !(requestOptions.responseType == ResponseType.bytes || diff --git a/lib/features/complaining/models/complain_model.dart b/lib/features/complaining/models/complain_model.dart new file mode 100644 index 00000000..aa7be75f --- /dev/null +++ b/lib/features/complaining/models/complain_model.dart @@ -0,0 +1,15 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'complain_model.freezed.dart'; +part 'complain_model.g.dart'; + +@freezed +class ComplainModel with _$ComplainModel { + + const factory ComplainModel({ + @Default(null) String? description, + @Default([]) List tags, + }) = _ComplainModel; + + factory ComplainModel.fromJson(Map json) => _$ComplainModelFromJson(json); +} diff --git a/lib/features/complaining/models/complain_model.freezed.dart b/lib/features/complaining/models/complain_model.freezed.dart new file mode 100644 index 00000000..464a38e4 --- /dev/null +++ b/lib/features/complaining/models/complain_model.freezed.dart @@ -0,0 +1,193 @@ +// 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 'complain_model.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +ComplainModel _$ComplainModelFromJson(Map json) { + return _ComplainModel.fromJson(json); +} + +/// @nodoc +mixin _$ComplainModel { + String? get description => throw _privateConstructorUsedError; + List get tags => throw _privateConstructorUsedError; + + /// Serializes this ComplainModel to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of ComplainModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $ComplainModelCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ComplainModelCopyWith<$Res> { + factory $ComplainModelCopyWith( + ComplainModel value, $Res Function(ComplainModel) then) = + _$ComplainModelCopyWithImpl<$Res, ComplainModel>; + @useResult + $Res call({String? description, List tags}); +} + +/// @nodoc +class _$ComplainModelCopyWithImpl<$Res, $Val extends ComplainModel> + implements $ComplainModelCopyWith<$Res> { + _$ComplainModelCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of ComplainModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? description = freezed, + Object? tags = null, + }) { + return _then(_value.copyWith( + description: freezed == description + ? _value.description + : description // ignore: cast_nullable_to_non_nullable + as String?, + tags: null == tags + ? _value.tags + : tags // ignore: cast_nullable_to_non_nullable + as List, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$ComplainModelImplCopyWith<$Res> + implements $ComplainModelCopyWith<$Res> { + factory _$$ComplainModelImplCopyWith( + _$ComplainModelImpl value, $Res Function(_$ComplainModelImpl) then) = + __$$ComplainModelImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({String? description, List tags}); +} + +/// @nodoc +class __$$ComplainModelImplCopyWithImpl<$Res> + extends _$ComplainModelCopyWithImpl<$Res, _$ComplainModelImpl> + implements _$$ComplainModelImplCopyWith<$Res> { + __$$ComplainModelImplCopyWithImpl( + _$ComplainModelImpl _value, $Res Function(_$ComplainModelImpl) _then) + : super(_value, _then); + + /// Create a copy of ComplainModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? description = freezed, + Object? tags = null, + }) { + return _then(_$ComplainModelImpl( + description: freezed == description + ? _value.description + : description // ignore: cast_nullable_to_non_nullable + as String?, + tags: null == tags + ? _value._tags + : tags // ignore: cast_nullable_to_non_nullable + as List, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$ComplainModelImpl implements _ComplainModel { + const _$ComplainModelImpl( + {this.description = null, final List tags = const []}) + : _tags = tags; + + factory _$ComplainModelImpl.fromJson(Map json) => + _$$ComplainModelImplFromJson(json); + + @override + @JsonKey() + final String? description; + final List _tags; + @override + @JsonKey() + List get tags { + if (_tags is EqualUnmodifiableListView) return _tags; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_tags); + } + + @override + String toString() { + return 'ComplainModel(description: $description, tags: $tags)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ComplainModelImpl && + (identical(other.description, description) || + other.description == description) && + const DeepCollectionEquality().equals(other._tags, _tags)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, description, const DeepCollectionEquality().hash(_tags)); + + /// Create a copy of ComplainModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$ComplainModelImplCopyWith<_$ComplainModelImpl> get copyWith => + __$$ComplainModelImplCopyWithImpl<_$ComplainModelImpl>(this, _$identity); + + @override + Map toJson() { + return _$$ComplainModelImplToJson( + this, + ); + } +} + +abstract class _ComplainModel implements ComplainModel { + const factory _ComplainModel( + {final String? description, + final List tags}) = _$ComplainModelImpl; + + factory _ComplainModel.fromJson(Map json) = + _$ComplainModelImpl.fromJson; + + @override + String? get description; + @override + List get tags; + + /// Create a copy of ComplainModel + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$ComplainModelImplCopyWith<_$ComplainModelImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/features/complaining/models/complain_model.g.dart b/lib/features/complaining/models/complain_model.g.dart new file mode 100644 index 00000000..d9d3226f --- /dev/null +++ b/lib/features/complaining/models/complain_model.g.dart @@ -0,0 +1,21 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'complain_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$ComplainModelImpl _$$ComplainModelImplFromJson(Map json) => + _$ComplainModelImpl( + description: json['description'] as String? ?? null, + tags: + (json['tags'] as List?)?.map((e) => e as String).toList() ?? + const [], + ); + +Map _$$ComplainModelImplToJson(_$ComplainModelImpl instance) => + { + 'description': instance.description, + 'tags': instance.tags, + }; diff --git a/lib/features/complaining/presentation/complaining_bottom_sheet.dart b/lib/features/complaining/presentation/complaining_bottom_sheet.dart new file mode 100644 index 00000000..68770acc --- /dev/null +++ b/lib/features/complaining/presentation/complaining_bottom_sheet.dart @@ -0,0 +1,590 @@ +import 'package:eatvio_app/core/app_router.dart'; +import 'package:eatvio_app/core/enums/complained_content.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/bottom_sheet_handler_bar.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/loader.dart'; +import 'package:eatvio_app/features/complaining/presentation/cubit/complaining_cubit.dart'; +import 'package:eatvio_app/features/complaining/presentation/cubit/complaining_state.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:go_router/go_router.dart'; +import 'package:collection/collection.dart'; + +class ComplainingBottomSheet extends StatefulWidget { + const ComplainingBottomSheet({super.key, required this.complainedContentType, this.slug, this.id}); + final ComplainedContent complainedContentType; + final String? slug; + final int? id; + @override + State createState() => _ComplainingBottomSheetState(); +} + +class _ComplainingBottomSheetState extends State { + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => sl()..setContentTypeAndIdentifier(contentType: widget.complainedContentType, slug: widget.slug, id: widget.id), + child: Container( + padding: EdgeInsets.only(top: 8, bottom: 24, left: 16, right: 16), + decoration: BoxDecoration( + borderRadius: BorderRadius.vertical(top: Radius.circular(12)), + ), + child: BlocBuilder( + builder: (context, state) { + final category = state.category; + if (state.wasSentSuccessfully == true) { + return _ComplaintSentWidget(); + } else if (state.wasSentSuccessfully == false) { + return _ComplaintFailedWidget(); + } + if (category != null) { + return switch (category) { + ComplainType.spam => _SpamWidget(), + ComplainType.lawViolation => _LawViolationWidget(), + ComplainType.deception => _DeceptionWidget(), + ComplainType.violenceDndHostility => _ViolenceAndHostility(), + ComplainType.explicitImage => _ExplicitImageWidget(), + }; + } + return _CategoryWidget(); + }, + ), + ), + ); + } +} + +class _CategoryWidget extends StatelessWidget { + const _CategoryWidget(); + final categories = ComplainType.values; + @override + Widget build(BuildContext context) { + final s = S.of(context); + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + BottomSheetHandlerBar(), + SizedBox(height: 20), + Text( + s.complaint, + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), + ), + SizedBox(height: 8), + Text( + s.complaint_question, + textAlign: TextAlign.center, + style: TextStyles.textMMedium.copyWith(color: appTheme.black), + ), + SizedBox(height: 24), + ...List.generate( + categories.length, + (index) => Column( + children: [ + _CategoryButton( + category: categories[index], + onTap: () { + context.read().setCategory(categories[index]); + }, + ), + if (index != categories.length - 1) SizedBox(height: 20), + ], + ), + ), + SizedBox(height: 24), + ], + ); + } +} + +class _SpamWidget extends StatelessWidget { + const _SpamWidget(); + + @override + Widget build(BuildContext context) { + final s = S.of(context); + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + BottomSheetHandlerBar(), + SizedBox(height: 20), + Text( + s.spam, + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), + ), + SizedBox(height: 8), + SizedBox( + width: MediaQuery.sizeOf(context).width * 0.7, + child: Text( + s.spam_description, + textAlign: TextAlign.center, + style: TextStyles.textMMedium.copyWith(color: appTheme.black), + ), + ), + SizedBox(height: 24), + _ComplainButton(), + ], + ); + } +} + +class _ExplicitImageWidget extends StatelessWidget { + const _ExplicitImageWidget(); + + @override + Widget build(BuildContext context) { + final s = S.of(context); + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + BottomSheetHandlerBar(), + SizedBox(height: 20), + Text( + s.explicitImages, + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), + ), + SizedBox(height: 8), + SizedBox( + width: MediaQuery.sizeOf(context).width * 0.7, + child: Text( + s.explicitImagesDescription, + textAlign: TextAlign.center, + style: TextStyles.textMMedium.copyWith(color: appTheme.black), + ), + ), + SizedBox(height: 24), + _ComplainButton(), + ], + ); + } +} + +class _LawViolationWidget extends StatefulWidget { + const _LawViolationWidget(); + + @override + State<_LawViolationWidget> createState() => _LawViolationWidgetState(); +} + +class _LawViolationWidgetState extends State<_LawViolationWidget> { + final List<(bool, String)> tags = []; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + final s = S.of(context); + if (tags.isEmpty) { + tags.addAll([ + (false, s.alcohol), + (false, s.drugs), + (false, s.nicotine), + (false, s.weapon), + (false, s.fake_documents), + (false, s.prostitution), + (false, s.other), + ]); + } + } + + @override + Widget build(BuildContext context) { + final s = S.of(context); + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + BottomSheetHandlerBar(), + SizedBox(height: 20), + Text( + s.lawViolation, + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), + ), + SizedBox(height: 8), + SizedBox( + width: MediaQuery.sizeOf(context).width * 0.7, + child: Text( + s.what_do_you_want_to_complaint_about, + textAlign: TextAlign.center, + style: TextStyles.textMMedium.copyWith(color: appTheme.black), + ), + ), + SizedBox(height: 24), + ...tags.mapIndexed((index, element) { + return Row( + children: [ + Checkbox( + activeColor: appTheme.blueA400, + side: BorderSide(width: 2, color: tags[index].$1 ? appTheme.blueA400 : appTheme.blueGray), + value: element.$1, + onChanged: (isChecked) { + setState(() { + if (isChecked == null) return; + if (index == tags.length - 1 && !isChecked) { + context.read().toggleDescription(); + } + tags[index] = (isChecked, element.$2); + final activeTags = tags.where((elem) => elem.$1).map((e) => e.$2).toList(); + context.read().setTags(activeTags); + }); + }, + ), + Text( + element.$2, + style: TextStyles.textLSemibold.copyWith(color: appTheme.black), + ), + ], + ); + }).toList(), + if (tags.last.$1) ...[ + SizedBox(height: 12), + CustomTextFormField( + hintText: s.type_here_your_complaint, + onChanged: (value) { + context.read().setDescription(value); + }, + ), + ], + SizedBox(height: 20), + _ComplainButton(), + ], + ); + } +} + +class _ViolenceAndHostility extends StatefulWidget { + const _ViolenceAndHostility(); + + @override + State<_ViolenceAndHostility> createState() => _ViolenceAndHostilityState(); +} + +class _ViolenceAndHostilityState extends State<_ViolenceAndHostility> { + final List<(bool, String)> tags = []; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + final s = S.of(context); + if (tags.isEmpty) { + tags.addAll([ + (false, s.violence_against_people_or_animal), + (false, s.hatred), + (false, s.suicide), + (false, s.hatred_description), + (false, s.extremism), + (false, s.calls_for_bullying), + ]); + } + } + + @override + Widget build(BuildContext context) { + final s = S.of(context); + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + BottomSheetHandlerBar(), + SizedBox(height: 20), + Text( + s.lawViolation, + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), + ), + SizedBox(height: 8), + SizedBox( + width: MediaQuery.sizeOf(context).width * 0.7, + child: Text( + s.what_do_you_want_to_complaint_about, + textAlign: TextAlign.center, + style: TextStyles.textMMedium.copyWith(color: appTheme.black), + ), + ), + SizedBox(height: 24), + ...tags.mapIndexed((index, element) { + return Row( + children: [ + Checkbox( + activeColor: appTheme.blueA400, + side: BorderSide(width: 2, color: tags[index].$1 ? appTheme.blueA400 : appTheme.blueGray), + value: element.$1, + onChanged: (isChecked) { + setState(() { + if (isChecked == null) return; + tags[index] = (isChecked, element.$2); + final activeTags = tags.where((elem) => elem.$1).map((e) => e.$2).toList(); + context.read().setTags(activeTags); + }); + }, + ), + Text( + element.$2, + style: TextStyles.textLSemibold.copyWith(color: appTheme.black), + ), + ], + ); + }).toList(), + SizedBox(height: 20), + _ComplainButton(), + ], + ); + } +} + +class _DeceptionWidget extends StatefulWidget { + const _DeceptionWidget(); + + @override + State<_DeceptionWidget> createState() => _DeceptionWidgetState(); +} + +class _DeceptionWidgetState extends State<_DeceptionWidget> { + final List<(bool, String)> tags = []; + final List descriptions = []; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + final s = S.of(context); + if (tags.isEmpty && descriptions.isEmpty) { + tags.addAll([ + (false, s.misrepresentation), + (false, s.fraud), + ]); + + descriptions.addAll([ + s.misrepresentation_description, + s.fraud_description, + ]); + } + } + + @override + Widget build(BuildContext context) { + final s = S.of(context); + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + BottomSheetHandlerBar(), + SizedBox(height: 20), + Text( + s.lawViolation, + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), + ), + SizedBox(height: 8), + SizedBox( + width: MediaQuery.sizeOf(context).width * 0.7, + child: Text( + s.what_do_you_want_to_complaint_about, + textAlign: TextAlign.center, + style: TextStyles.textMMedium.copyWith(color: appTheme.black), + ), + ), + SizedBox(height: 24), + ...tags.mapIndexed((index, element) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Checkbox( + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + activeColor: appTheme.blueA400, + side: BorderSide(width: 2, color: tags[index].$1 ? appTheme.blueA400 : appTheme.blueGray), + value: element.$1, + onChanged: (isChecked) { + setState(() { + if (isChecked == null) return; + tags[index] = (isChecked, element.$2); + final activeTags = tags.where((elem) => elem.$1).map((e) => e.$2).toList(); + context.read().setTags(activeTags); + }); + }, + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 8), + Text( + element.$2, + style: TextStyles.textLSemibold.copyWith(color: appTheme.black), + ), + SizedBox( + width: MediaQuery.sizeOf(context).width * 0.7, + child: Text( + descriptions[index], + style: TextStyles.textSSemibold.copyWith(color: appTheme.lightGray600), + ), + ), + ], + ), + ], + ); + }).toList(), + SizedBox(height: 20), + _ComplainButton(), + ], + ); + } +} + +class _ComplaintSentWidget extends StatelessWidget { + const _ComplaintSentWidget(); + + @override + Widget build(BuildContext context) { + final s = S.of(context); + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + BottomSheetHandlerBar(), + SizedBox(height: 20), + Text( + s.complaint_sent, + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), + ), + SizedBox(height: 8), + SizedBox( + width: MediaQuery.sizeOf(context).width * 0.7, + child: Text( + s.complaint_sent_description, + textAlign: TextAlign.center, + style: TextStyles.textMMedium.copyWith(color: appTheme.black), + ), + ), + SizedBox(height: 24), + CustomOutlinedButton( + text: s.ok, + height: 52, + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), + buttonStyle: ButtonStyle( + side: WidgetStatePropertyAll(BorderSide.none), + backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), + ), + onPressed: () { + AppRouter.navigatorKey.currentContext?.pop(); + }, + ), + ], + ); + } +} + +class _ComplaintFailedWidget extends StatelessWidget { + const _ComplaintFailedWidget(); + + @override + Widget build(BuildContext context) { + final s = S.of(context); + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + BottomSheetHandlerBar(), + SizedBox(height: 20), + Text( + s.could_not_send_complain, + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), + ), + SizedBox(height: 8), + SizedBox( + width: MediaQuery.sizeOf(context).width * 0.7, + child: Text( + s.please_try_again_later, + textAlign: TextAlign.center, + style: TextStyles.textMMedium.copyWith(color: appTheme.black), + ), + ), + SizedBox(height: 24), + CustomOutlinedButton( + text: s.ok, + height: 52, + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), + buttonStyle: ButtonStyle( + side: WidgetStatePropertyAll(BorderSide.none), + backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), + ), + onPressed: () { + AppRouter.navigatorKey.currentContext?.pop(); + }, + ), + ], + ); + } +} + +class _ComplainButton extends StatelessWidget { + const _ComplainButton(); + + @override + Widget build(BuildContext context) { + final s = S.of(context); + return BlocBuilder( + builder: (context, state) { + if (state.isLoading) { + return Loader(); + } + return CustomOutlinedButton( + text: s.send_complain, + height: 52, + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), + buttonStyle: ButtonStyle( + side: WidgetStatePropertyAll(BorderSide.none), + backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), + ), + onPressed: () async { + await context.read().complain(); + }, + ); + }, + ); + } +} + +class _CategoryButton extends StatelessWidget { + const _CategoryButton({required this.category, required this.onTap}); + final ComplainType category; + final void Function() onTap; + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap, + child: Ink( + child: Row( + children: [ + Text( + category.getTranslatedText(), + style: TextStyles.textLSemibold.copyWith(color: appTheme.black), + ), + Spacer(), + SvgPicture.asset('assets/icons/right_arrow.svg'), + ], + ), + ), + ); + } +} + +enum ComplainType { + spam, + lawViolation, + deception, + violenceDndHostility, + explicitImage; + + String getTranslatedText() => switch (this) { + ComplainType.spam => S.current.spam, + ComplainType.lawViolation => S.current.lawViolation, + ComplainType.deception => S.current.deception, + ComplainType.violenceDndHostility => S.current.violenceAndHostility, + ComplainType.explicitImage => S.current.explicitImage, + }; + + String getValue() => switch (this) { + ComplainType.spam => 'spam', + ComplainType.lawViolation => 'legalViolation', + ComplainType.deception => 'fraud', + ComplainType.violenceDndHostility => 'violenceAndHatred', + ComplainType.explicitImage => 'explicitImage', + }; +} diff --git a/lib/features/complaining/presentation/complaining_screen.dart b/lib/features/complaining/presentation/complaining_screen.dart deleted file mode 100644 index 2c3e0fd3..00000000 --- a/lib/features/complaining/presentation/complaining_screen.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'package:flutter/material.dart'; - -class ComplainingScreen extends StatelessWidget { - const ComplainingScreen({super.key}); - - @override - Widget build(BuildContext context) { - return const Placeholder(); - } -} \ No newline at end of file diff --git a/lib/features/complaining/presentation/cubit/complaining_cubit.dart b/lib/features/complaining/presentation/cubit/complaining_cubit.dart new file mode 100644 index 00000000..61843c04 --- /dev/null +++ b/lib/features/complaining/presentation/cubit/complaining_cubit.dart @@ -0,0 +1,79 @@ +import 'dart:developer'; + +import 'package:eatvio_app/core/enums/complained_content.dart'; +import 'package:eatvio_app/features/complaining/presentation/complaining_bottom_sheet.dart'; +import 'package:eatvio_app/features/complaining/presentation/cubit/complaining_state.dart'; +import 'package:eatvio_app/features/complaining/repositories/complaining_repository.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class ComplainingCubit extends Cubit { + final ComplainingRepository repository; + ComplainingCubit({required this.repository}) : super(ComplainingState()); + + void setContentTypeAndIdentifier({required ComplainedContent contentType, int? id, String? slug}) { + emit(state.copyWith(contentType: contentType, contentId: id, contentSlug: slug)); + } + + void setCategory(ComplainType category) { + emit(state.copyWith(category: category)); + } + + void setTags(List tags) { + emit(state.copyWith.complainInfo(tags: tags)); + } + + void toggleDescription() { + if (state.complainInfo.description != null) { + emit(state.copyWith.complainInfo(description: null)); + } else { + emit(state.copyWith.complainInfo(description: '')); + } + } + + void setDescription(String description) { + emit(state.copyWith.complainInfo(description: description)); + } + + Future complain() async { + try { + final category = state.category; + final id = state.contentId; + final slug = state.contentSlug; + final contentType = state.contentType; + if (category != null && contentType != null) { + switch (contentType) { + case ComplainedContent.diet: + emit(state.copyWith(wasSentSuccessfully: true)); + return; + case ComplainedContent.plan: + if (slug != null) { + await repository.sendPlanComplain(slug: slug, category: category.getValue(), body: state.complainInfo); + emit(state.copyWith(wasSentSuccessfully: true)); + return; + } + case ComplainedContent.recipe: + if (slug != null) { + await repository.sendRecipeComplain(slug: slug, category: category.getValue(), body: state.complainInfo); + emit(state.copyWith(wasSentSuccessfully: true)); + return; + } + case ComplainedContent.profile: + if (slug != null) { + await repository.sendProfileComplain(slug: slug, category: category.getValue(), body: state.complainInfo); + emit(state.copyWith(wasSentSuccessfully: true)); + return; + } + case ComplainedContent.posts: + if (id != null) { + await repository.sendBlogPostsComplain(id: id, category: category.getValue(), body: state.complainInfo); + emit(state.copyWith(wasSentSuccessfully: true)); + return; + } + } + } + } catch (e, st) { + log(e.toString(), stackTrace: st); + } + emit(state.copyWith(wasSentSuccessfully: false)); + } +} diff --git a/lib/features/complaining/presentation/cubit/complaining_state.dart b/lib/features/complaining/presentation/cubit/complaining_state.dart new file mode 100644 index 00000000..1f3e69a2 --- /dev/null +++ b/lib/features/complaining/presentation/cubit/complaining_state.dart @@ -0,0 +1,20 @@ +import 'package:eatvio_app/core/enums/complained_content.dart'; +import 'package:eatvio_app/features/complaining/models/complain_model.dart'; +import 'package:eatvio_app/features/complaining/presentation/complaining_bottom_sheet.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'complaining_state.freezed.dart'; + +@freezed +class ComplainingState with _$ComplainingState { + + factory ComplainingState({ + @Default(false) bool isLoading, + @Default(null) bool? wasSentSuccessfully, + @Default(null) ComplainType? category, + @Default(null) ComplainedContent? contentType, + @Default(null) int? contentId, + @Default(null) String? contentSlug, + @Default(ComplainModel()) ComplainModel complainInfo, + }) = _ComplainingState; +} \ No newline at end of file diff --git a/lib/features/complaining/presentation/cubit/complaining_state.freezed.dart b/lib/features/complaining/presentation/cubit/complaining_state.freezed.dart new file mode 100644 index 00000000..d2719d8e --- /dev/null +++ b/lib/features/complaining/presentation/cubit/complaining_state.freezed.dart @@ -0,0 +1,298 @@ +// 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 'complaining_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 _$ComplainingState { + bool get isLoading => throw _privateConstructorUsedError; + bool? get wasSentSuccessfully => throw _privateConstructorUsedError; + ComplainType? get category => throw _privateConstructorUsedError; + ComplainedContent? get contentType => throw _privateConstructorUsedError; + int? get contentId => throw _privateConstructorUsedError; + String? get contentSlug => throw _privateConstructorUsedError; + ComplainModel get complainInfo => throw _privateConstructorUsedError; + + /// Create a copy of ComplainingState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $ComplainingStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ComplainingStateCopyWith<$Res> { + factory $ComplainingStateCopyWith( + ComplainingState value, $Res Function(ComplainingState) then) = + _$ComplainingStateCopyWithImpl<$Res, ComplainingState>; + @useResult + $Res call( + {bool isLoading, + bool? wasSentSuccessfully, + ComplainType? category, + ComplainedContent? contentType, + int? contentId, + String? contentSlug, + ComplainModel complainInfo}); + + $ComplainModelCopyWith<$Res> get complainInfo; +} + +/// @nodoc +class _$ComplainingStateCopyWithImpl<$Res, $Val extends ComplainingState> + implements $ComplainingStateCopyWith<$Res> { + _$ComplainingStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of ComplainingState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? isLoading = null, + Object? wasSentSuccessfully = freezed, + Object? category = freezed, + Object? contentType = freezed, + Object? contentId = freezed, + Object? contentSlug = freezed, + Object? complainInfo = null, + }) { + return _then(_value.copyWith( + isLoading: null == isLoading + ? _value.isLoading + : isLoading // ignore: cast_nullable_to_non_nullable + as bool, + wasSentSuccessfully: freezed == wasSentSuccessfully + ? _value.wasSentSuccessfully + : wasSentSuccessfully // ignore: cast_nullable_to_non_nullable + as bool?, + category: freezed == category + ? _value.category + : category // ignore: cast_nullable_to_non_nullable + as ComplainType?, + contentType: freezed == contentType + ? _value.contentType + : contentType // ignore: cast_nullable_to_non_nullable + as ComplainedContent?, + contentId: freezed == contentId + ? _value.contentId + : contentId // ignore: cast_nullable_to_non_nullable + as int?, + contentSlug: freezed == contentSlug + ? _value.contentSlug + : contentSlug // ignore: cast_nullable_to_non_nullable + as String?, + complainInfo: null == complainInfo + ? _value.complainInfo + : complainInfo // ignore: cast_nullable_to_non_nullable + as ComplainModel, + ) as $Val); + } + + /// Create a copy of ComplainingState + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $ComplainModelCopyWith<$Res> get complainInfo { + return $ComplainModelCopyWith<$Res>(_value.complainInfo, (value) { + return _then(_value.copyWith(complainInfo: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$ComplainingStateImplCopyWith<$Res> + implements $ComplainingStateCopyWith<$Res> { + factory _$$ComplainingStateImplCopyWith(_$ComplainingStateImpl value, + $Res Function(_$ComplainingStateImpl) then) = + __$$ComplainingStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {bool isLoading, + bool? wasSentSuccessfully, + ComplainType? category, + ComplainedContent? contentType, + int? contentId, + String? contentSlug, + ComplainModel complainInfo}); + + @override + $ComplainModelCopyWith<$Res> get complainInfo; +} + +/// @nodoc +class __$$ComplainingStateImplCopyWithImpl<$Res> + extends _$ComplainingStateCopyWithImpl<$Res, _$ComplainingStateImpl> + implements _$$ComplainingStateImplCopyWith<$Res> { + __$$ComplainingStateImplCopyWithImpl(_$ComplainingStateImpl _value, + $Res Function(_$ComplainingStateImpl) _then) + : super(_value, _then); + + /// Create a copy of ComplainingState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? isLoading = null, + Object? wasSentSuccessfully = freezed, + Object? category = freezed, + Object? contentType = freezed, + Object? contentId = freezed, + Object? contentSlug = freezed, + Object? complainInfo = null, + }) { + return _then(_$ComplainingStateImpl( + isLoading: null == isLoading + ? _value.isLoading + : isLoading // ignore: cast_nullable_to_non_nullable + as bool, + wasSentSuccessfully: freezed == wasSentSuccessfully + ? _value.wasSentSuccessfully + : wasSentSuccessfully // ignore: cast_nullable_to_non_nullable + as bool?, + category: freezed == category + ? _value.category + : category // ignore: cast_nullable_to_non_nullable + as ComplainType?, + contentType: freezed == contentType + ? _value.contentType + : contentType // ignore: cast_nullable_to_non_nullable + as ComplainedContent?, + contentId: freezed == contentId + ? _value.contentId + : contentId // ignore: cast_nullable_to_non_nullable + as int?, + contentSlug: freezed == contentSlug + ? _value.contentSlug + : contentSlug // ignore: cast_nullable_to_non_nullable + as String?, + complainInfo: null == complainInfo + ? _value.complainInfo + : complainInfo // ignore: cast_nullable_to_non_nullable + as ComplainModel, + )); + } +} + +/// @nodoc + +class _$ComplainingStateImpl implements _ComplainingState { + _$ComplainingStateImpl( + {this.isLoading = false, + this.wasSentSuccessfully = null, + this.category = null, + this.contentType = null, + this.contentId = null, + this.contentSlug = null, + this.complainInfo = const ComplainModel()}); + + @override + @JsonKey() + final bool isLoading; + @override + @JsonKey() + final bool? wasSentSuccessfully; + @override + @JsonKey() + final ComplainType? category; + @override + @JsonKey() + final ComplainedContent? contentType; + @override + @JsonKey() + final int? contentId; + @override + @JsonKey() + final String? contentSlug; + @override + @JsonKey() + final ComplainModel complainInfo; + + @override + String toString() { + return 'ComplainingState(isLoading: $isLoading, wasSentSuccessfully: $wasSentSuccessfully, category: $category, contentType: $contentType, contentId: $contentId, contentSlug: $contentSlug, complainInfo: $complainInfo)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ComplainingStateImpl && + (identical(other.isLoading, isLoading) || + other.isLoading == isLoading) && + (identical(other.wasSentSuccessfully, wasSentSuccessfully) || + other.wasSentSuccessfully == wasSentSuccessfully) && + (identical(other.category, category) || + other.category == category) && + (identical(other.contentType, contentType) || + other.contentType == contentType) && + (identical(other.contentId, contentId) || + other.contentId == contentId) && + (identical(other.contentSlug, contentSlug) || + other.contentSlug == contentSlug) && + (identical(other.complainInfo, complainInfo) || + other.complainInfo == complainInfo)); + } + + @override + int get hashCode => Object.hash(runtimeType, isLoading, wasSentSuccessfully, + category, contentType, contentId, contentSlug, complainInfo); + + /// Create a copy of ComplainingState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$ComplainingStateImplCopyWith<_$ComplainingStateImpl> get copyWith => + __$$ComplainingStateImplCopyWithImpl<_$ComplainingStateImpl>( + this, _$identity); +} + +abstract class _ComplainingState implements ComplainingState { + factory _ComplainingState( + {final bool isLoading, + final bool? wasSentSuccessfully, + final ComplainType? category, + final ComplainedContent? contentType, + final int? contentId, + final String? contentSlug, + final ComplainModel complainInfo}) = _$ComplainingStateImpl; + + @override + bool get isLoading; + @override + bool? get wasSentSuccessfully; + @override + ComplainType? get category; + @override + ComplainedContent? get contentType; + @override + int? get contentId; + @override + String? get contentSlug; + @override + ComplainModel get complainInfo; + + /// Create a copy of ComplainingState + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$ComplainingStateImplCopyWith<_$ComplainingStateImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/features/complaining/repositories/complaining_repository.dart b/lib/features/complaining/repositories/complaining_repository.dart new file mode 100644 index 00000000..af3a10b6 --- /dev/null +++ b/lib/features/complaining/repositories/complaining_repository.dart @@ -0,0 +1,55 @@ +import 'package:eatvio_app/features/complaining/models/complain_model.dart'; +import 'package:eatvio_app/features/complaining/services/complaining_service.dart'; + +class ComplainingRepository { + final ComplainingService service; + ComplainingRepository({required this.service}); + + Future sendPlanComplain({ + required String slug, + required String category, + required ComplainModel body, + }) async { + try { + service.sendPlanComplain(slug: slug, category: category, body: body); + } catch (e) { + rethrow; + } + } + + Future sendRecipeComplain({ + required String slug, + required String category, + required ComplainModel body, + }) async { + try { + service.sendRecipeComplain(slug: slug, category: category, body: body); + } catch (e) { + rethrow; + } + } + + Future sendBlogPostsComplain({ + required int id, + required String category, + required ComplainModel body, + }) async { + try { + service.sendBlogPostsComplain(id: id, category: category, body: body); + } catch (e) { + rethrow; + } + } + + Future sendProfileComplain({ + required String slug, + required String category, + required ComplainModel body, + }) async { + try { + service.sendProfileComplain(slug: slug, category: category, body: body); + } catch (e) { + rethrow; + } + } +} diff --git a/lib/features/complaining/services/complaining_service.dart b/lib/features/complaining/services/complaining_service.dart new file mode 100644 index 00000000..cf214149 --- /dev/null +++ b/lib/features/complaining/services/complaining_service.dart @@ -0,0 +1,39 @@ +import 'package:dio/dio.dart'; +import 'package:eatvio_app/features/complaining/models/complain_model.dart'; +import 'package:retrofit/retrofit.dart'; + +part 'complaining_service.g.dart'; + +@RestApi() +abstract class ComplainingService { + factory ComplainingService(Dio dio, {String baseUrl}) = _ComplainingService; + + @POST('/api/plans/{slug}/complain/{category}') + Future sendPlanComplain({ + @Path() required String slug, + @Path() required String category, + @Body() required ComplainModel body, + }); + + @POST('/api/recipes/{slug}/complain/{category}') + Future sendRecipeComplain({ + @Path() required String slug, + @Path() required String category, + @Body() required ComplainModel body, + }); + + @POST('/api/blogPosts/{id}/complain/{category}') + Future sendBlogPostsComplain({ + @Path() required int id, + @Path() required String category, + @Body() required ComplainModel body, + }); + + @POST('/api/profile/{slug}/complain/{category}') + Future sendProfileComplain({ + @Path() required String slug, + @Path() required String category, + @Body() required ComplainModel body, + }); +} + diff --git a/lib/features/complaining/services/complaining_service.g.dart b/lib/features/complaining/services/complaining_service.g.dart new file mode 100644 index 00000000..8b3d6595 --- /dev/null +++ b/lib/features/complaining/services/complaining_service.g.dart @@ -0,0 +1,138 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'complaining_service.dart'; + +// ************************************************************************** +// RetrofitGenerator +// ************************************************************************** + +// ignore_for_file: unnecessary_brace_in_string_interps,no_leading_underscores_for_local_identifiers,unused_element,unnecessary_string_interpolations + +class _ComplainingService implements ComplainingService { + _ComplainingService(this._dio, {this.baseUrl, this.errorLogger}); + + final Dio _dio; + + String? baseUrl; + + final ParseErrorLogger? errorLogger; + + @override + Future sendPlanComplain({ + required String slug, + required String category, + required ComplainModel body, + }) async { + final _extra = {}; + final queryParameters = {}; + final _headers = {}; + final _data = body; + final _options = _setStreamType( + Options(method: 'POST', headers: _headers, extra: _extra) + .compose( + _dio.options, + '/api/plans/${slug}/complain/${category}', + queryParameters: queryParameters, + data: _data, + ) + .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), + ); + await _dio.fetch(_options); + } + + @override + Future sendRecipeComplain({ + required String slug, + required String category, + required ComplainModel body, + }) async { + final _extra = {}; + final queryParameters = {}; + final _headers = {}; + final _data = body; + final _options = _setStreamType( + Options(method: 'POST', headers: _headers, extra: _extra) + .compose( + _dio.options, + '/api/recipes/${slug}/complain/${category}', + queryParameters: queryParameters, + data: _data, + ) + .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), + ); + await _dio.fetch(_options); + } + + @override + Future sendBlogPostsComplain({ + required int id, + required String category, + required ComplainModel body, + }) async { + final _extra = {}; + final queryParameters = {}; + final _headers = {}; + final _data = body; + final _options = _setStreamType( + Options(method: 'POST', headers: _headers, extra: _extra) + .compose( + _dio.options, + '/api/blogPosts/${id}/complain/${category}', + queryParameters: queryParameters, + data: _data, + ) + .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), + ); + await _dio.fetch(_options); + } + + @override + Future sendProfileComplain({ + required String slug, + required String category, + required ComplainModel body, + }) async { + final _extra = {}; + final queryParameters = {}; + final _headers = {}; + final _data = body; + final _options = _setStreamType( + Options(method: 'POST', headers: _headers, extra: _extra) + .compose( + _dio.options, + '/api/profile/${slug}/complain/${category}', + 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/consultant_rating/models/rating_model.dart b/lib/features/consultant_rating/models/rating_model.dart new file mode 100644 index 00000000..fbd4fe74 --- /dev/null +++ b/lib/features/consultant_rating/models/rating_model.dart @@ -0,0 +1,16 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'rating_model.freezed.dart'; +part 'rating_model.g.dart'; + +@freezed +class RatingModel with _$RatingModel { + + factory RatingModel({ + required DateTime date, + required String title, + required int amount, + }) = _RatingModel; + + factory RatingModel.fromJson(Map json) => _$RatingModelFromJson(json); +} \ No newline at end of file diff --git a/lib/features/consultant_rating/models/rating_model.freezed.dart b/lib/features/consultant_rating/models/rating_model.freezed.dart new file mode 100644 index 00000000..74e1f0c7 --- /dev/null +++ b/lib/features/consultant_rating/models/rating_model.freezed.dart @@ -0,0 +1,200 @@ +// 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 'rating_model.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +RatingModel _$RatingModelFromJson(Map json) { + return _RatingModel.fromJson(json); +} + +/// @nodoc +mixin _$RatingModel { + DateTime get date => throw _privateConstructorUsedError; + String get title => throw _privateConstructorUsedError; + int get amount => throw _privateConstructorUsedError; + + /// Serializes this RatingModel to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of RatingModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $RatingModelCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $RatingModelCopyWith<$Res> { + factory $RatingModelCopyWith( + RatingModel value, $Res Function(RatingModel) then) = + _$RatingModelCopyWithImpl<$Res, RatingModel>; + @useResult + $Res call({DateTime date, String title, int amount}); +} + +/// @nodoc +class _$RatingModelCopyWithImpl<$Res, $Val extends RatingModel> + implements $RatingModelCopyWith<$Res> { + _$RatingModelCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of RatingModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? date = null, + Object? title = null, + Object? amount = null, + }) { + return _then(_value.copyWith( + date: null == date + ? _value.date + : date // ignore: cast_nullable_to_non_nullable + as DateTime, + title: null == title + ? _value.title + : title // ignore: cast_nullable_to_non_nullable + as String, + amount: null == amount + ? _value.amount + : amount // ignore: cast_nullable_to_non_nullable + as int, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$RatingModelImplCopyWith<$Res> + implements $RatingModelCopyWith<$Res> { + factory _$$RatingModelImplCopyWith( + _$RatingModelImpl value, $Res Function(_$RatingModelImpl) then) = + __$$RatingModelImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({DateTime date, String title, int amount}); +} + +/// @nodoc +class __$$RatingModelImplCopyWithImpl<$Res> + extends _$RatingModelCopyWithImpl<$Res, _$RatingModelImpl> + implements _$$RatingModelImplCopyWith<$Res> { + __$$RatingModelImplCopyWithImpl( + _$RatingModelImpl _value, $Res Function(_$RatingModelImpl) _then) + : super(_value, _then); + + /// Create a copy of RatingModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? date = null, + Object? title = null, + Object? amount = null, + }) { + return _then(_$RatingModelImpl( + date: null == date + ? _value.date + : date // ignore: cast_nullable_to_non_nullable + as DateTime, + title: null == title + ? _value.title + : title // ignore: cast_nullable_to_non_nullable + as String, + amount: null == amount + ? _value.amount + : amount // ignore: cast_nullable_to_non_nullable + as int, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$RatingModelImpl implements _RatingModel { + _$RatingModelImpl( + {required this.date, required this.title, required this.amount}); + + factory _$RatingModelImpl.fromJson(Map json) => + _$$RatingModelImplFromJson(json); + + @override + final DateTime date; + @override + final String title; + @override + final int amount; + + @override + String toString() { + return 'RatingModel(date: $date, title: $title, amount: $amount)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$RatingModelImpl && + (identical(other.date, date) || other.date == date) && + (identical(other.title, title) || other.title == title) && + (identical(other.amount, amount) || other.amount == amount)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, date, title, amount); + + /// Create a copy of RatingModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$RatingModelImplCopyWith<_$RatingModelImpl> get copyWith => + __$$RatingModelImplCopyWithImpl<_$RatingModelImpl>(this, _$identity); + + @override + Map toJson() { + return _$$RatingModelImplToJson( + this, + ); + } +} + +abstract class _RatingModel implements RatingModel { + factory _RatingModel( + {required final DateTime date, + required final String title, + required final int amount}) = _$RatingModelImpl; + + factory _RatingModel.fromJson(Map json) = + _$RatingModelImpl.fromJson; + + @override + DateTime get date; + @override + String get title; + @override + int get amount; + + /// Create a copy of RatingModel + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$RatingModelImplCopyWith<_$RatingModelImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/features/consultant_rating/models/rating_model.g.dart b/lib/features/consultant_rating/models/rating_model.g.dart new file mode 100644 index 00000000..f3458ac6 --- /dev/null +++ b/lib/features/consultant_rating/models/rating_model.g.dart @@ -0,0 +1,21 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'rating_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$RatingModelImpl _$$RatingModelImplFromJson(Map json) => + _$RatingModelImpl( + date: DateTime.parse(json['date'] as String), + title: json['title'] as String, + amount: (json['amount'] as num).toInt(), + ); + +Map _$$RatingModelImplToJson(_$RatingModelImpl instance) => + { + 'date': instance.date.toIso8601String(), + 'title': instance.title, + 'amount': instance.amount, + }; diff --git a/lib/features/consultant_rating/presentation/consultant_rating_screen.dart b/lib/features/consultant_rating/presentation/consultant_rating_screen.dart new file mode 100644 index 00000000..75434b3e --- /dev/null +++ b/lib/features/consultant_rating/presentation/consultant_rating_screen.dart @@ -0,0 +1,76 @@ +import 'package:eatvio_app/core/theme/custom_text_style.dart'; +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/core/widgets/custom_outlined_button.dart'; +import 'package:eatvio_app/features/consultant_rating/models/rating_model.dart'; +import 'package:eatvio_app/features/consultant_rating/presentation/widgets/rating_history_widget.dart'; +import 'package:eatvio_app/features/consultant_rating/presentation/widgets/rating_widget.dart'; +import 'package:eatvio_app/features/consultant_rating/presentation/widgets/statistic_widget.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; + +class ConsultantRatingScreen extends StatelessWidget { + static const name = 'consultantRatingScreen'; + static const path = '/consultant-rating-screen'; + const ConsultantRatingScreen({super.key}); + + @override + Widget build(BuildContext context) { + return const ConsultantRatingPageContent(); + } +} + +class ConsultantRatingPageContent extends StatelessWidget { + const ConsultantRatingPageContent({super.key}); + + @override + Widget build(BuildContext context) { + final s = S.of(context); + return Scaffold( + backgroundColor: appTheme.gray100, + appBar: AppBar( + centerTitle: true, + title: Text( + s.rating_and_statistics, + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), + ), + ), + body: SingleChildScrollView( + child: Column( + children: [ + SizedBox(height: 16), + RatingWidget( + rating: 1058, + ), + SizedBox(height: 16), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: CustomOutlinedButton( + text: s.copy_referral_link, + height: 52, + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), + buttonStyle: ButtonStyle( + side: WidgetStatePropertyAll(BorderSide.none), + backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), + ), + onPressed: () {}, + ), + ), + SizedBox(height: 16), + StatisticWidget(), + SizedBox(height: 16), + RatingHistoryWidget( + ratingHistory: [ + RatingModel(date: DateTime.now(), title: ' djkasjdasjkdkjasjkdjkaskjdjklasjkld', amount: 201300), + RatingModel(date: DateTime.now(), title: 'djkasjdasjkdkjasjkdjkaskjdjklasjkld', amount: 89323), + RatingModel(date: DateTime.now(), title: 'djkasjdasjkdkjasjkdjkaskjdjklasjkld', amount: 20), + RatingModel(date: DateTime.now(), title: 'djkasjdasjkdkjasjkdjkaskjdjklasjkld', amount: 1212), + RatingModel(date: DateTime.now(), title: 'qweqweeq', amount: 323), + RatingModel(date: DateTime.now(), title: 'ee', amount: 1), + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/consultant_rating/presentation/widgets/period_filter_widget.dart b/lib/features/consultant_rating/presentation/widgets/period_filter_widget.dart new file mode 100644 index 00000000..e78c7b26 --- /dev/null +++ b/lib/features/consultant_rating/presentation/widgets/period_filter_widget.dart @@ -0,0 +1,67 @@ +import 'package:eatvio_app/core/theme/custom_text_style.dart'; +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/core/widgets/custom_drop_down_button.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:collection/collection.dart'; + +class PeriodFilterWidget extends StatefulWidget { + const PeriodFilterWidget({super.key}); + + @override + State createState() => _PeriodFilterWidgetState(); +} + +class _PeriodFilterWidgetState extends State { + final List periodDropDownTitles = []; + int periodDropDownIndex = 0; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + final s = S.of(context); + if (periodDropDownTitles.isEmpty) { + periodDropDownTitles.addAll([ + s.for_whole_period_of_time, + s.for_a_year, + s.for_a_month, + s.for_a_week, + ]); + } + } + + @override + Widget build(BuildContext context) { + final s = S.of(context); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CustomDropDownButton( + hint: Text( + s.choose_diet_duration, + style: TextStyles.textSMedium.copyWith(color: appTheme.black), + ), + onChanged: (value) { + if (value != null) { + setState(() { + periodDropDownIndex = periodDropDownTitles.indexOf(value); + }); + } + }, + items: periodDropDownTitles + .mapIndexed( + (index, value) => DropdownMenuItem( + child: Text( + periodDropDownTitles[index], + style: TextStyles.textMMedium.copyWith(color: appTheme.black), + ), + value: periodDropDownTitles[index], + ), + ) + .toList(), + value: periodDropDownTitles[periodDropDownIndex], + ), + ], + ); + } +} diff --git a/lib/features/consultant_rating/presentation/widgets/rating_history_item.dart b/lib/features/consultant_rating/presentation/widgets/rating_history_item.dart new file mode 100644 index 00000000..0a4c052c --- /dev/null +++ b/lib/features/consultant_rating/presentation/widgets/rating_history_item.dart @@ -0,0 +1,49 @@ +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/date_time_utils.dart'; +import 'package:flutter/material.dart'; + +class RatingHistoryItem extends StatelessWidget { + const RatingHistoryItem({super.key, required this.title, required this.amount, required this.date}); + final String title; + final int amount; + final DateTime date; + @override + Widget build(BuildContext context) { + return Container( + height: 80, + padding: EdgeInsets.all(12), + decoration: BoxDecoration( + color: appTheme.gray100, + borderRadius: BorderRadius.circular(20), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + date.format('dd.MM.yyyy'), + style: TextStyles.textXSSemibold.copyWith(color: appTheme.gray), + ), + Spacer(), + Text( + title, + style: TextStyles.textMMedium.copyWith(color: appTheme.black), + ), + ], + ), + ), + Text( + '+$amount', + textAlign: TextAlign.center, + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.greenA700), + ), + ], + ), + ); + } +} diff --git a/lib/features/consultant_rating/presentation/widgets/rating_history_widget.dart b/lib/features/consultant_rating/presentation/widgets/rating_history_widget.dart new file mode 100644 index 00000000..835cc216 --- /dev/null +++ b/lib/features/consultant_rating/presentation/widgets/rating_history_widget.dart @@ -0,0 +1,69 @@ +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/main_background_bouble.dart'; +import 'package:eatvio_app/features/consultant_rating/models/rating_model.dart'; +import 'package:eatvio_app/features/consultant_rating/presentation/widgets/period_filter_widget.dart'; +import 'package:eatvio_app/features/consultant_rating/presentation/widgets/rating_history_item.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; + +class RatingHistoryWidget extends StatelessWidget { + const RatingHistoryWidget({super.key, required this.ratingHistory}); + final List ratingHistory; + @override + Widget build(BuildContext context) { + final s = S.of(context); + return MainBackgroundBouble( + padding: EdgeInsets.only( + left: 16, + right: 16, + top: 16, + ), + child: Column( + children: [ + Row( + children: [ + Text( + s.rating_history, + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), + ), + Spacer(), + SvgPicture.asset( + 'assets/icons/question_mark.svg', + height: 22, + width: 22, + colorFilter: ColorFilter.mode( + appTheme.blueA400, + BlendMode.srcIn, + ), + ), + ], + ), + SizedBox( + height: 16, + ), + PeriodFilterWidget(), + SizedBox( + height: 12, + ), + ListView.builder( + physics: NeverScrollableScrollPhysics(), + itemBuilder: (context, index) { + return Padding( + padding: index == ratingHistory.length - 1 ? EdgeInsets.zero : const EdgeInsets.only(bottom: 12), + child: RatingHistoryItem( + title: ratingHistory[index].title, + amount: ratingHistory[index].amount, + date: ratingHistory[index].date, + ), + ); + }, + itemCount: ratingHistory.length, + shrinkWrap: true, + ), + ], + ), + ); + } +} diff --git a/lib/features/consultant_rating/presentation/widgets/rating_widget.dart b/lib/features/consultant_rating/presentation/widgets/rating_widget.dart new file mode 100644 index 00000000..bffdb6ff --- /dev/null +++ b/lib/features/consultant_rating/presentation/widgets/rating_widget.dart @@ -0,0 +1,56 @@ +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/main_background_bouble.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; + +class RatingWidget extends StatelessWidget { + const RatingWidget({super.key, required this.rating}); + + final int rating; + + @override + Widget build(BuildContext context) { + final s = S.of(context); + return MainBackgroundBouble( + child: SizedBox( + height: 130, + child: Stack( + children: [ + Positioned( + right: 12, + top: 12, + child: SvgPicture.asset( + 'assets/icons/question_mark.svg', + height: 22, + width: 22, + colorFilter: ColorFilter.mode( + appTheme.blueA400, + BlendMode.srcIn, + ), + ), + ), + Align( + alignment: Alignment.center, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + rating.toString(), + style: TextStyles.headingFS40FW600.copyWith(color: appTheme.blueA400), + ), + SizedBox(height: 4), + Text( + s.your_rating, + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/consultant_rating/presentation/widgets/statistic_bouble.dart b/lib/features/consultant_rating/presentation/widgets/statistic_bouble.dart new file mode 100644 index 00000000..d306eb51 --- /dev/null +++ b/lib/features/consultant_rating/presentation/widgets/statistic_bouble.dart @@ -0,0 +1,37 @@ +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 StatisticBouble extends StatelessWidget { + const StatisticBouble({super.key, required this.number, required this.title}); + final int number; + final String title; + @override + Widget build(BuildContext context) { + return Container( + height: 80, + padding: EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration( + color: appTheme.gray100, + borderRadius: BorderRadius.circular(20), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + number.toString(), + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.blueA400), + ), + SizedBox( + height: 4, + ), + Text( + title, + textAlign: TextAlign.center, + style: TextStyles.textXSSemibold.copyWith(color: appTheme.black), + ), + ], + ), + ); + } +} diff --git a/lib/features/consultant_rating/presentation/widgets/statistic_widget.dart b/lib/features/consultant_rating/presentation/widgets/statistic_widget.dart new file mode 100644 index 00000000..911007c1 --- /dev/null +++ b/lib/features/consultant_rating/presentation/widgets/statistic_widget.dart @@ -0,0 +1,79 @@ +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/main_background_bouble.dart'; +import 'package:eatvio_app/features/consultant_rating/presentation/widgets/period_filter_widget.dart'; +import 'package:eatvio_app/features/consultant_rating/presentation/widgets/statistic_bouble.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; + +class StatisticWidget extends StatelessWidget { + const StatisticWidget({super.key}); + + @override + Widget build(BuildContext context) { + final s = S.of(context); + return MainBackgroundBouble( + padding: EdgeInsets.all(16), + child: Column( + children: [ + Row( + children: [ + Text( + s.statistic, + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), + ), + Spacer(), + SvgPicture.asset( + 'assets/icons/question_mark.svg', + height: 22, + width: 22, + colorFilter: ColorFilter.mode( + appTheme.blueA400, + BlendMode.srcIn, + ), + ), + ], + ), + SizedBox( + height: 16, + ), + PeriodFilterWidget(), + SizedBox( + height: 12, + ), + Row( + children: [ + Expanded(child: StatisticBouble(number: 12, title: s.invited_people_count)), + SizedBox(width: 8), + Expanded(child: StatisticBouble(number: 12, title: s.published_posts_count)), + ], + ), + SizedBox( + height: 8, + ), + Row( + children: [ + Expanded(child: StatisticBouble(number: 12, title: s.published_recipes_count)), + SizedBox(width: 8), + Expanded(child: StatisticBouble(number: 12, title: s.published_diets_count)), + ], + ), + SizedBox( + height: 8, + ), + Row( + children: [ + Expanded(child: StatisticBouble(number: 12, title: s.consultations_count)), + SizedBox(width: 8), + Expanded(child: StatisticBouble(number: 12, title: s.active_consultations_count)), + ], + ), + SizedBox( + height: 8, + ), + ], + ), + ); + } +} diff --git a/lib/features/create_diet/models/create_diet_model.dart b/lib/features/create_diet/models/create_diet_model.dart index 84639c77..49f1b752 100644 --- a/lib/features/create_diet/models/create_diet_model.dart +++ b/lib/features/create_diet/models/create_diet_model.dart @@ -19,5 +19,3 @@ class CreateDietModel with _$CreateDietModel { List>>? dailySchedule, }) = _CreateDietModel; } - - diff --git a/lib/features/create_diet/presentation/create_diet_screen.dart b/lib/features/create_diet/presentation/create_diet_screen.dart index cb1cd953..632fb5f4 100644 --- a/lib/features/create_diet/presentation/create_diet_screen.dart +++ b/lib/features/create_diet/presentation/create_diet_screen.dart @@ -2,12 +2,13 @@ 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/loader.dart'; import 'package:eatvio_app/core/widgets/main_background_bouble.dart'; +import 'package:eatvio_app/core/widgets/show_error_dialog.dart'; +import 'package:eatvio_app/core/widgets/success_published_diet_bottom_sheet.dart'; import 'package:eatvio_app/features/create_diet/presentation/cubit/create_diet_cubit.dart'; import 'package:eatvio_app/features/create_diet/presentation/cubit/create_diet_state.dart'; import 'package:eatvio_app/features/create_diet/presentation/widgets/day_widget.dart'; @@ -48,6 +49,7 @@ class CreateDietScreenContent extends StatefulWidget { class _CreateDietScreenContentState extends State { final nameController = TextEditingController(); final descriptionController = TextEditingController(); + final scaffoldKey = GlobalKey(); @override void initState() { @@ -65,49 +67,22 @@ class _CreateDietScreenContentState extends State { builder: (context, state) { if (state.published) { Future.delayed(Duration(milliseconds: 500), () { - context.pop('${state.dietData.name ?? 'Диета'}'); + context.pop(); + showBottomSheetWithBlur( + userRootNavigator: true, + context: context, + child: SuccessPublishedDietBottomSheet( + title: '${state.dietData.name ?? 'Диета'}', + ), + ); }); } else if (state.drafted) { Future.delayed(Duration(milliseconds: 500), () { - context.pop('${state.dietData.name ?? 'Диета'}'); + context.pop(); showBottomSheetWithBlur( context: context, - 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: () { - Navigator.of(context, rootNavigator: true).pop(); - }, - text: 'Хорошо', - height: 52, - buttonTextStyle: TextStyles.textSSemibold.copyWith(color: appTheme.whiteA700), - buttonStyle: ButtonStyle( - side: WidgetStatePropertyAll(BorderSide.none), - backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), - ), - ), - ), - SizedBox( - height: 20, - ), - ], + child: SuccessPublishedDietBottomSheet( + title: '${state.dietData.name ?? 'Диета'} сохраненна в черновиках', ), ); }); @@ -232,8 +207,20 @@ class _CreateDietScreenContentState extends State { Loader() else CustomOutlinedButton( - onPressed: () { - context.read().publishDiet(); + onPressed: () async { + if (widget.diet != null) { + final result = await context.read().updateDiet( + widget.diet!.id, + ); + if (result == 'error') { + showErrorDialog(context, title: '${s.could_not_update_diet} ${state.dietData.name}'); + } + } else { + final result = await context.read().createDiet(); + if (result == 'error') { + showErrorDialog(context, title: '${s.could_not_publish_diet} ${state.dietData.name}'); + } + } }, text: widget.diet != null ? s.save : s.publish, height: 52, diff --git a/lib/features/create_diet/presentation/cubit/create_diet_cubit.dart b/lib/features/create_diet/presentation/cubit/create_diet_cubit.dart index 771e2531..80f1850a 100644 --- a/lib/features/create_diet/presentation/cubit/create_diet_cubit.dart +++ b/lib/features/create_diet/presentation/cubit/create_diet_cubit.dart @@ -18,6 +18,7 @@ class CreateDietCubit extends Cubit { final ProfileRepository profileRepository; final GlossaryRepository glossaryRepository; String? tempImagePath; + int? dietId; static const _dietStateValues = DietState.values; CreateDietCubit({required this.profileRepository, required this.createDietRepository, required this.glossaryRepository}) : super(CreateDietState.initial()); @@ -25,10 +26,12 @@ class CreateDietCubit extends Cubit { Future setInitialData(Diet? diet) async { if (diet == null) return; emit(state.copyWith(isLoading: true, isDietBeingEdited: true)); - final countOfDays = diet.dailySchedule.length; + dietId = diet.id; + final dailyScheduleWithNutrition = await createDietRepository.getDietPortionsNutrition(diet.id); + final countOfDays = dailyScheduleWithNutrition.length; final List>> dailySchedule = List.generate(countOfDays, (index) => [[], [], [], [], []]); - for (final dayEntry in diet.dailySchedule.entries) { + for (final dayEntry in dailyScheduleWithNutrition.entries) { for (final mealEntry in dayEntry.value.entries) { for (final product in mealEntry.value.values) { dailySchedule[dayEntry.key - 1][mealEntry.key - 1].add(product); @@ -318,9 +321,9 @@ class CreateDietCubit extends Cubit { return diet; } - Future publishDiet() async { + Future createDiet() async { emit(state.copyWith(isDietPublishing: true)); - if (!_isFromValid()) return null; + if (!_isFromValid()) return 'validation'; try { final image = await ImageCompressUtility.compressFile(File(state.dietData.image!.path)); final dietState = state.dietData.state!; @@ -339,12 +342,39 @@ class CreateDietCubit extends Cubit { ); emit(state.copyWith(published: dietState != DietState.draft, drafted: dietState == DietState.draft)); - return state.dietData.name; + return null; } on DioException catch (e, stackTrace) { log(e.toString(), stackTrace: stackTrace); } emit(state.copyWith(isDietPublishing: false)); - return null; + return 'error'; + } + + Future updateDiet(int dietId) async { + if (!_isFromValid()) return 'error'; + try { + File? image; + if (state.dietData.image != null) { + image = await ImageCompressUtility.compressFile(File(state.dietData.image!.path)); + } + await createDietRepository.updateDiet( + dietId: dietId, + name: state.dietData.name, + duration: state.dietData.duration, + state: state.dietData.state ?? DietState.active, + image: image, + meatType: state.dietData.type['meat'] == true ? '1' : '0', + carbType: state.dietData.type['carb'] == true ? '1' : '0', + vegType: state.dietData.type['veg'] == true ? '1' : '0', + lightType: state.dietData.type['light'] == true ? '1' : '0', + details: state.dietData.details, + dailySchedule: state.dietData.dailySchedule, + ); + return null; + } catch (e, st) { + log(e.toString(), stackTrace: st); + return 'error'; + } } bool _isFromValid() { diff --git a/lib/features/create_diet/presentation/diet_preview_screen.dart b/lib/features/create_diet/presentation/diet_preview_screen.dart index 879e5446..066ac06e 100644 --- a/lib/features/create_diet/presentation/diet_preview_screen.dart +++ b/lib/features/create_diet/presentation/diet_preview_screen.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; +import 'package:eatvio_app/core/enums/product_type.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'; @@ -300,7 +301,7 @@ class _DietPreviewScreenState extends State { ), ), SizedBox(height: 12), - InfoRow(diet: widget.diet), + InfoRow(diet: widget.diet, type: ProductType.diet,), ExpandableText( text: widget.diet.details, ), diff --git a/lib/features/create_diet/presentation/widgets/add_meal_widget.dart b/lib/features/create_diet/presentation/widgets/add_meal_widget.dart index c0c76b03..c5d377f7 100644 --- a/lib/features/create_diet/presentation/widgets/add_meal_widget.dart +++ b/lib/features/create_diet/presentation/widgets/add_meal_widget.dart @@ -1,5 +1,7 @@ +import 'package:easy_debounce/easy_debounce.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/core/widgets/custom_divider.dart'; import 'package:eatvio_app/core/widgets/custom_text_form_field.dart'; import 'package:eatvio_app/features/create_diet/presentation/cubit/create_diet_cubit.dart'; import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart'; @@ -7,7 +9,6 @@ import 'package:eatvio_app/features/diary/presentation/widgets/glossary_moderati import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; import 'package:eatvio_app/features/glossary/models/glossary_model.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_typeahead/flutter_typeahead.dart'; class AddMealWidget extends StatefulWidget { const AddMealWidget({ @@ -33,6 +34,7 @@ class _AddMealWidgetState extends State { GlossaryModel? selectedGlossary; final productNameController = TextEditingController(); final quantityController = TextEditingController(); + List glossariesList = []; @override void initState() { @@ -66,47 +68,27 @@ class _AddMealWidgetState extends State { children: [ SizedBox( width: MediaQuery.of(context).size.width * 0.8 - 32, - child: TypeAheadField( + child: CustomTextFormField( controller: productNameController, - itemBuilder: (context, glossary) { - return Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - glossary.name, - style: TextStyle( - height: 1, - fontSize: 16, - ), - ), + autofocus: true, + hintText: 'Введите название продукта/блюда', + onChanged: (query) { + EasyDebounce.debounce( + 'glossary-search', + Duration(milliseconds: 250), + () async { + selectedGlossary = null; + if (query.isEmpty) { + glossariesList = []; + selectedGlossary = null; + setState(() {}); + return; + } + glossariesList = await widget.createDietCubit.getGlossaryListByQuery(query); + setState(() {}); + }, ); }, - itemSeparatorBuilder: (context, index) => _divider(context), - onSelected: (newSelectedGlossary) async { - setState(() { - selectedGlossary = newSelectedGlossary; - productNameController.text = selectedGlossary!.name; - }); - }, - suggestionsCallback: (query) async { - if (query.length < 1) return []; - final suggestionsList = await widget.createDietCubit.getGlossaryListByQuery(query); - return suggestionsList; - }, - builder: (context, controller, focusNode) { - return CustomTextFormField( - controller: controller, - autofocus: true, - focusNode: focusNode, - hintText: 'Введите название продукта/блюда', - ); - }, - emptyBuilder: (context) { - return const SizedBox.shrink(); - }, - errorBuilder: (context, object) => Padding( - padding: const EdgeInsets.all(8.0), - child: Text('Нет подходящих вариантов'), - ), ), ), const SizedBox(width: 4), @@ -129,7 +111,56 @@ class _AddMealWidgetState extends State { ), ], ), - const SizedBox(height: 16), + glossariesList.isEmpty ? const SizedBox(height: 16) : const SizedBox.shrink(), + Container( + constraints: BoxConstraints( + maxHeight: 256, + ), + child: ListView.separated( + physics: const BouncingScrollPhysics(), + shrinkWrap: true, + itemBuilder: (context, i) { + return ListTile( + onTap: () async { + setState(() { + selectedGlossary = glossariesList[i]; + productNameController.text = selectedGlossary!.name; + }); + glossariesList = []; + setState(() {}); + FocusManager.instance.primaryFocus?.nextFocus(); + }, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + title: Text( + glossariesList[i].name, + style: TextStyle( + height: 1, + ), + ), + subtitle: glossariesList[i].grInPortion == 0 + ? null + : Text( + '${glossariesList[i].grInPortion} г', + style: TextStyle( + height: 1, + color: Colors.grey.shade600, + ), + ), + trailing: Icon( + Icons.add_rounded, + color: Colors.grey.shade400, + ), + contentPadding: EdgeInsets.only(left: 12), + ); + }, + itemCount: glossariesList.length, + separatorBuilder: (BuildContext context, int index) { + return CustomDivider(); + }, + ), + ), Row( children: [ if (widget.portionIndex != null) @@ -188,7 +219,7 @@ class _AddMealWidgetState extends State { Future addPortion() async { final portionIndex = widget.portionIndex; - final measurementUnits = selectedGlossary?.grInPortion == null + var measurementUnits = selectedGlossary?.grInPortion == null ? Portion.PCS : selectedGlossary?.grInPortion == 0 ? Portion.GRAMS @@ -199,7 +230,7 @@ class _AddMealWidgetState extends State { final portion = DailySchedule( dailyNutrition: DailyNutrition(energy: calories, proteins: proteins, fats: fats, carbohydrates: carbohydrates), quantity: int.parse(quantityController.text), - name: glossary.name, + name: glossary.name, productNutritionalValueId: glossary.id.toString(), portion: measurementUnits, ); @@ -234,7 +265,8 @@ class _AddMealWidgetState extends State { ), ); if (newGlossary != null) { - final [calories, carbohydrates, proteins, fats] = _getCPFC(newGlossary, Portion.GRAMS); + measurementUnits = Portion.GRAMS; + final [calories, carbohydrates, proteins, fats] = _getCPFC(newGlossary, measurementUnits); final portion = DailySchedule( dailyNutrition: DailyNutrition(energy: calories, proteins: proteins, fats: fats, carbohydrates: carbohydrates), quantity: int.parse(quantityController.text), @@ -278,15 +310,5 @@ class _AddMealWidgetState extends State { } return [calories, carbohydrates, proteins, fats]; } - - _divider(BuildContext context) { - return Container( - height: 0.4, - width: MediaQuery.of(context).size.width - 32, - decoration: BoxDecoration( - color: Colors.black26, - borderRadius: BorderRadius.circular(16), - ), - ); - } + } diff --git a/lib/features/create_diet/repositories/create_diet_repository.dart b/lib/features/create_diet/repositories/create_diet_repository.dart index ba9a6fa4..3661dfd9 100644 --- a/lib/features/create_diet/repositories/create_diet_repository.dart +++ b/lib/features/create_diet/repositories/create_diet_repository.dart @@ -48,7 +48,7 @@ class CreateDietRepository { ), }); - final formData = FormData.fromMap(dietMap); + final formData = FormData.fromMap(dietMap); switch (state) { case DietState.active: return createDietService.createPublishedDiet(formData: formData); @@ -58,4 +58,97 @@ class CreateDietRepository { return createDietService.createPersonalDiet(formData: formData); } } + + Future>>> getDietPortionsNutrition(int dietId) async { + try { + final response = await createDietService.getDietPortionsNutrition(diet: dietId); + final data = response.data; + final dailySchedule = (data as Map).map((key, value) { + return MapEntry( + int.parse(key), + (value as Map).map((key, value) { + return MapEntry( + int.parse(key), + (value as Map).map((key, value) { + return MapEntry(int.parse(key), DailySchedule.fromJson(value as Map)); + }), + ); + }), + ); + }); + return dailySchedule; + } catch (e) { + rethrow; + } + } + + Future updateDiet({ + required int dietId, + required DietState state, + String? name, + int? duration, + String? meatType, + String? carbType, + String? vegType, + String? lightType, + String? details, + File? image, + List>>? dailySchedule, + }) { + final dietMap = {}; + if (dailySchedule != null) { + for (final (dayIndex, day) in dailySchedule.indexed) { + for (final (mealIndex, meal) in day.indexed) { + for (final (productIndex, product) in meal.indexed) { + final jsonProduct = product.toJson(); + dietMap.addAll({ + 'daily_schedule[${dayIndex + 1}][${mealIndex + 1}][${productIndex}][name]': jsonProduct['name'], + 'daily_schedule[${dayIndex + 1}][${mealIndex + 1}][${productIndex}][portion]': jsonProduct['portion'], + 'daily_schedule[${dayIndex + 1}][${mealIndex + 1}][${productIndex}][quantity]': jsonProduct['quantity'], + 'daily_schedule[${dayIndex + 1}][${mealIndex + 1}][${productIndex}][product_nutritional_value_id]': jsonProduct['product_nutritional_value_id'], + }); + } + } + } + } + if (name != null) { + dietMap.addAll({'name': name}); + } + if (details != null) { + dietMap.addAll({'details': details}); + } + if (duration != null) { + dietMap.addAll({'duration': duration}); + } + if (meatType != null) { + dietMap.addAll({'type[meat]': meatType}); + } + if (carbType != null) { + dietMap.addAll({'type[carb]': carbType}); + } + if (vegType != null) { + dietMap.addAll({'type[veg]': vegType}); + } + if (lightType != null) { + dietMap.addAll({'type[light]': lightType}); + } + if (image != null) { + dietMap.addAll({ + 'image': MultipartFile.fromFileSync( + image.path, + filename: image.path.split(Platform.pathSeparator).last, + ), + }); + } + + final formData = FormData.fromMap(dietMap); + switch (state) { + case DietState.active: + return createDietService.updatePublished(diet: dietId, formData: formData); + case DietState.draft: + return createDietService.updateDrafted(diet: dietId, formData: formData); + case DietState.personal: + return createDietService.updatePersonal(diet: dietId, formData: formData); + } + } } diff --git a/lib/features/create_diet/services/create_diet_service.dart b/lib/features/create_diet/services/create_diet_service.dart index 471681ee..3c0d8923 100644 --- a/lib/features/create_diet/services/create_diet_service.dart +++ b/lib/features/create_diet/services/create_diet_service.dart @@ -25,4 +25,30 @@ abstract class CreateDietService { Future createPersonalDiet({ @Body() required FormData formData, }); + + @POST('/api/diets/update/published/{diet}') + @MultiPart() + Future updatePublished({ + @Path() required int diet, + @Body() required FormData formData, + }); + + @POST('/api/diets/update/drafted/{diet}') + @MultiPart() + Future updateDrafted({ + @Path() required int diet, + @Body() required FormData formData, + }); + + @POST('/api/diets/update/personalized/{diet}') + @MultiPart() + Future updatePersonal({ + @Path() required int diet, + @Body() required FormData formData, + }); + + @GET('/api/diets/show/{diet}/per_portion_nutritions') + Future getDietPortionsNutrition({ + @Path() required int diet, + }); } diff --git a/lib/features/create_diet/services/create_diet_service.g.dart b/lib/features/create_diet/services/create_diet_service.g.dart index 8d039872..61af7e06 100644 --- a/lib/features/create_diet/services/create_diet_service.g.dart +++ b/lib/features/create_diet/services/create_diet_service.g.dart @@ -113,6 +113,111 @@ class _CreateDietService implements CreateDietService { return _value; } + @override + Future updatePublished({ + required int diet, + required FormData formData, + }) async { + final _extra = {}; + final queryParameters = {}; + final _headers = {}; + final _data = formData; + final _options = _setStreamType( + Options( + method: 'POST', + headers: _headers, + extra: _extra, + contentType: 'multipart/form-data', + ) + .compose( + _dio.options, + '/api/diets/update/published/${diet}', + queryParameters: queryParameters, + data: _data, + ) + .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), + ); + await _dio.fetch(_options); + } + + @override + Future updateDrafted({ + required int diet, + required FormData formData, + }) async { + final _extra = {}; + final queryParameters = {}; + final _headers = {}; + final _data = formData; + final _options = _setStreamType( + Options( + method: 'POST', + headers: _headers, + extra: _extra, + contentType: 'multipart/form-data', + ) + .compose( + _dio.options, + '/api/diets/update/drafted/${diet}', + queryParameters: queryParameters, + data: _data, + ) + .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), + ); + await _dio.fetch(_options); + } + + @override + Future updatePersonal({ + required int diet, + required FormData formData, + }) async { + final _extra = {}; + final queryParameters = {}; + final _headers = {}; + final _data = formData; + final _options = _setStreamType( + Options( + method: 'POST', + headers: _headers, + extra: _extra, + contentType: 'multipart/form-data', + ) + .compose( + _dio.options, + '/api/diets/update/personalized/${diet}', + queryParameters: queryParameters, + data: _data, + ) + .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), + ); + await _dio.fetch(_options); + } + + @override + Future> getDietPortionsNutrition({ + required int diet, + }) async { + final _extra = {}; + final queryParameters = {}; + final _headers = {}; + const Map? _data = null; + final _options = _setStreamType>( + Options(method: 'GET', headers: _headers, extra: _extra) + .compose( + _dio.options, + '/api/diets/show/${diet}/per_portion_nutritions', + queryParameters: queryParameters, + data: _data, + ) + .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), + ); + final _result = await _dio.fetch(_options); + final _value = _result.data; + final httpResponse = HttpResponse(_value, _result); + return httpResponse; + } + RequestOptions _setStreamType(RequestOptions requestOptions) { if (T != dynamic && !(requestOptions.responseType == ResponseType.bytes || diff --git a/lib/features/recipes/entities/recipe_create_entity.dart b/lib/features/create_recipe/entities/recipe_create_entity.dart similarity index 93% rename from lib/features/recipes/entities/recipe_create_entity.dart rename to lib/features/create_recipe/entities/recipe_create_entity.dart index 6ca01db3..9792ea63 100644 --- a/lib/features/recipes/entities/recipe_create_entity.dart +++ b/lib/features/create_recipe/entities/recipe_create_entity.dart @@ -1,8 +1,8 @@ import 'dart:io'; -import 'package:eatvio_app/features/recipes/models/ingredient.dart'; -import 'package:eatvio_app/features/recipes/models/recipe_model.dart'; -import 'package:eatvio_app/features/recipes/models/step.dart'; +import 'package:eatvio_app/features/create_recipe/models/ingredient.dart'; +import 'package:eatvio_app/features/create_recipe/models/recipe_model.dart'; +import 'package:eatvio_app/features/create_recipe/models/step.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; part 'recipe_create_entity.freezed.dart'; diff --git a/lib/features/recipes/entities/recipe_create_entity.freezed.dart b/lib/features/create_recipe/entities/recipe_create_entity.freezed.dart similarity index 100% rename from lib/features/recipes/entities/recipe_create_entity.freezed.dart rename to lib/features/create_recipe/entities/recipe_create_entity.freezed.dart diff --git a/lib/features/recipes/models/ingredient.dart b/lib/features/create_recipe/models/ingredient.dart similarity index 100% rename from lib/features/recipes/models/ingredient.dart rename to lib/features/create_recipe/models/ingredient.dart diff --git a/lib/features/recipes/models/ingredient.freezed.dart b/lib/features/create_recipe/models/ingredient.freezed.dart similarity index 100% rename from lib/features/recipes/models/ingredient.freezed.dart rename to lib/features/create_recipe/models/ingredient.freezed.dart diff --git a/lib/features/recipes/models/ingredient.g.dart b/lib/features/create_recipe/models/ingredient.g.dart similarity index 100% rename from lib/features/recipes/models/ingredient.g.dart rename to lib/features/create_recipe/models/ingredient.g.dart diff --git a/lib/features/recipes/models/product.dart b/lib/features/create_recipe/models/product.dart similarity index 100% rename from lib/features/recipes/models/product.dart rename to lib/features/create_recipe/models/product.dart diff --git a/lib/features/recipes/models/product.freezed.dart b/lib/features/create_recipe/models/product.freezed.dart similarity index 100% rename from lib/features/recipes/models/product.freezed.dart rename to lib/features/create_recipe/models/product.freezed.dart diff --git a/lib/features/recipes/models/product.g.dart b/lib/features/create_recipe/models/product.g.dart similarity index 100% rename from lib/features/recipes/models/product.g.dart rename to lib/features/create_recipe/models/product.g.dart diff --git a/lib/features/recipes/models/recipe_create_model.dart b/lib/features/create_recipe/models/recipe_create_model.dart similarity index 87% rename from lib/features/recipes/models/recipe_create_model.dart rename to lib/features/create_recipe/models/recipe_create_model.dart index e8f4dbf7..747e165f 100644 --- a/lib/features/recipes/models/recipe_create_model.dart +++ b/lib/features/create_recipe/models/recipe_create_model.dart @@ -1,5 +1,5 @@ -import 'package:eatvio_app/features/recipes/models/ingredient.dart'; -import 'package:eatvio_app/features/recipes/models/step.dart'; +import 'package:eatvio_app/features/create_recipe/models/ingredient.dart'; +import 'package:eatvio_app/features/create_recipe/models/step.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; part 'recipe_create_model.freezed.dart'; diff --git a/lib/features/recipes/models/recipe_create_model.freezed.dart b/lib/features/create_recipe/models/recipe_create_model.freezed.dart similarity index 100% rename from lib/features/recipes/models/recipe_create_model.freezed.dart rename to lib/features/create_recipe/models/recipe_create_model.freezed.dart diff --git a/lib/features/recipes/models/recipe_create_model.g.dart b/lib/features/create_recipe/models/recipe_create_model.g.dart similarity index 100% rename from lib/features/recipes/models/recipe_create_model.g.dart rename to lib/features/create_recipe/models/recipe_create_model.g.dart diff --git a/lib/features/recipes/models/recipe_model.dart b/lib/features/create_recipe/models/recipe_model.dart similarity index 81% rename from lib/features/recipes/models/recipe_model.dart rename to lib/features/create_recipe/models/recipe_model.dart index 588650dd..b79425d1 100644 --- a/lib/features/recipes/models/recipe_model.dart +++ b/lib/features/create_recipe/models/recipe_model.dart @@ -1,5 +1,5 @@ -import 'package:eatvio_app/features/recipes/models/ingredient.dart'; -import 'package:eatvio_app/features/recipes/models/step.dart'; +import 'package:eatvio_app/features/create_recipe/models/ingredient.dart'; +import 'package:eatvio_app/features/create_recipe/models/step.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; part 'recipe_model.freezed.dart'; diff --git a/lib/features/recipes/models/recipe_model.freezed.dart b/lib/features/create_recipe/models/recipe_model.freezed.dart similarity index 100% rename from lib/features/recipes/models/recipe_model.freezed.dart rename to lib/features/create_recipe/models/recipe_model.freezed.dart diff --git a/lib/features/recipes/models/recipe_model.g.dart b/lib/features/create_recipe/models/recipe_model.g.dart similarity index 100% rename from lib/features/recipes/models/recipe_model.g.dart rename to lib/features/create_recipe/models/recipe_model.g.dart diff --git a/lib/features/recipes/models/recipe_tag_model.dart b/lib/features/create_recipe/models/recipe_tag_model.dart similarity index 100% rename from lib/features/recipes/models/recipe_tag_model.dart rename to lib/features/create_recipe/models/recipe_tag_model.dart diff --git a/lib/features/recipes/models/recipe_tag_model.freezed.dart b/lib/features/create_recipe/models/recipe_tag_model.freezed.dart similarity index 100% rename from lib/features/recipes/models/recipe_tag_model.freezed.dart rename to lib/features/create_recipe/models/recipe_tag_model.freezed.dart diff --git a/lib/features/recipes/models/recipe_tag_model.g.dart b/lib/features/create_recipe/models/recipe_tag_model.g.dart similarity index 100% rename from lib/features/recipes/models/recipe_tag_model.g.dart rename to lib/features/create_recipe/models/recipe_tag_model.g.dart diff --git a/lib/features/recipes/models/step.dart b/lib/features/create_recipe/models/step.dart similarity index 100% rename from lib/features/recipes/models/step.dart rename to lib/features/create_recipe/models/step.dart diff --git a/lib/features/recipes/models/step.freezed.dart b/lib/features/create_recipe/models/step.freezed.dart similarity index 100% rename from lib/features/recipes/models/step.freezed.dart rename to lib/features/create_recipe/models/step.freezed.dart diff --git a/lib/features/recipes/models/step.g.dart b/lib/features/create_recipe/models/step.g.dart similarity index 100% rename from lib/features/recipes/models/step.g.dart rename to lib/features/create_recipe/models/step.g.dart diff --git a/lib/features/recipes/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart b/lib/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart similarity index 94% rename from lib/features/recipes/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart rename to lib/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart index a91e44dd..c171e720 100644 --- a/lib/features/recipes/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart +++ b/lib/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart @@ -1,10 +1,10 @@ import 'dart:developer'; import 'dart:io'; -import 'package:eatvio_app/features/recipes/entities/recipe_create_entity.dart'; -import 'package:eatvio_app/features/recipes/models/ingredient.dart'; -import 'package:eatvio_app/features/recipes/presentation/bloc/create_recipe_cubit/create_recipe_state.dart'; -import 'package:eatvio_app/features/recipes/reporitories/recipe_repository.dart'; +import 'package:eatvio_app/features/create_recipe/entities/recipe_create_entity.dart'; +import 'package:eatvio_app/features/create_recipe/models/ingredient.dart'; +import 'package:eatvio_app/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_state.dart'; +import 'package:eatvio_app/features/create_recipe/reporitories/recipe_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class CreateRecipeCubit extends Cubit { diff --git a/lib/features/recipes/presentation/bloc/create_recipe_cubit/create_recipe_state.dart b/lib/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_state.dart similarity index 86% rename from lib/features/recipes/presentation/bloc/create_recipe_cubit/create_recipe_state.dart rename to lib/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_state.dart index fd95a839..6bf9e961 100644 --- a/lib/features/recipes/presentation/bloc/create_recipe_cubit/create_recipe_state.dart +++ b/lib/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_state.dart @@ -1,4 +1,4 @@ -import 'package:eatvio_app/features/recipes/entities/recipe_create_entity.dart'; +import 'package:eatvio_app/features/create_recipe/entities/recipe_create_entity.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; part 'create_recipe_state.freezed.dart'; diff --git a/lib/features/recipes/presentation/bloc/create_recipe_cubit/create_recipe_state.freezed.dart b/lib/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_state.freezed.dart similarity index 100% rename from lib/features/recipes/presentation/bloc/create_recipe_cubit/create_recipe_state.freezed.dart rename to lib/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_state.freezed.dart diff --git a/lib/features/recipes/presentation/pages/recipe_create_screen.dart b/lib/features/create_recipe/presentation/pages/recipe_create_screen.dart similarity index 67% rename from lib/features/recipes/presentation/pages/recipe_create_screen.dart rename to lib/features/create_recipe/presentation/pages/recipe_create_screen.dart index f383da68..3647ebdd 100644 --- a/lib/features/recipes/presentation/pages/recipe_create_screen.dart +++ b/lib/features/create_recipe/presentation/pages/recipe_create_screen.dart @@ -1,13 +1,17 @@ import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; -import 'package:eatvio_app/core/utils/toasts.dart'; -import 'package:eatvio_app/features/recipes/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart'; -import 'package:eatvio_app/features/recipes/presentation/bloc/create_recipe_cubit/create_recipe_state.dart'; -import 'package:eatvio_app/features/recipes/presentation/widgets/info_about_recipe_widget.dart'; -import 'package:eatvio_app/features/recipes/presentation/widgets/ingredients_widget.dart'; -import 'package:eatvio_app/features/recipes/presentation/widgets/steps_widget.dart'; +import 'package:eatvio_app/core/widgets/bottom_sheet_with_blur.dart'; +import 'package:eatvio_app/core/widgets/show_error_dialog.dart'; +import 'package:eatvio_app/core/widgets/success_published_recipe_bottom_sheet.dart'; +import 'package:eatvio_app/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart'; +import 'package:eatvio_app/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_state.dart'; +import 'package:eatvio_app/features/create_recipe/presentation/widgets/info_about_recipe_widget.dart'; +import 'package:eatvio_app/features/create_recipe/presentation/widgets/ingredients_widget.dart'; +import 'package:eatvio_app/features/create_recipe/presentation/widgets/steps_widget.dart'; +import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; class RecipeCreateScreen extends StatefulWidget { const RecipeCreateScreen({super.key}); @@ -22,6 +26,7 @@ class RecipeCreateScreen extends StatefulWidget { class _RecipeCreateScreenState extends State { @override Widget build(BuildContext context) { + final s = S.of(context); return BlocProvider( create: (BuildContext context) => sl(), child: Scaffold( @@ -53,10 +58,18 @@ class _RecipeCreateScreenState extends State { if (context.read().validateAll()) { final isSuccess = await context.read().publishRecipe(); if (isSuccess) { - showSuccessToast('Рецепт успешно опубликован'); - Navigator.pop(context); + Future.delayed(Duration(milliseconds: 500), () { + context.pop(); + showBottomSheetWithBlur( + userRootNavigator: true, + context: context, + child: SuccessPublishedRecipeBottomSheet( + title: state.entity.title ?? 'Рецепт', + ), + ); + }); } else { - showErrorToast('Не удалось опубликовать рецепт'); + showErrorDialog(context, title: '${s.could_not_publish_recipe} ${state.entity.title}'); } } }, diff --git a/lib/features/recipes/presentation/widgets/image_widget.dart b/lib/features/create_recipe/presentation/widgets/image_widget.dart similarity index 96% rename from lib/features/recipes/presentation/widgets/image_widget.dart rename to lib/features/create_recipe/presentation/widgets/image_widget.dart index 251e3345..5422d895 100644 --- a/lib/features/recipes/presentation/widgets/image_widget.dart +++ b/lib/features/create_recipe/presentation/widgets/image_widget.dart @@ -1,6 +1,6 @@ import 'dart:io'; -import 'package:eatvio_app/features/recipes/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart'; +import 'package:eatvio_app/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; diff --git a/lib/features/recipes/presentation/widgets/info_about_recipe_widget.dart b/lib/features/create_recipe/presentation/widgets/info_about_recipe_widget.dart similarity index 90% rename from lib/features/recipes/presentation/widgets/info_about_recipe_widget.dart rename to lib/features/create_recipe/presentation/widgets/info_about_recipe_widget.dart index 0a7cb9fe..86a14896 100644 --- a/lib/features/recipes/presentation/widgets/info_about_recipe_widget.dart +++ b/lib/features/create_recipe/presentation/widgets/info_about_recipe_widget.dart @@ -3,13 +3,13 @@ import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/widgets/custom_drop_down.dart'; import 'package:eatvio_app/core/widgets/custom_text_form_field.dart'; import 'package:eatvio_app/features/create_diet/presentation/widgets/error_widget.dart'; -import 'package:eatvio_app/features/recipes/entities/recipe_create_entity.dart'; -import 'package:eatvio_app/features/recipes/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart'; -import 'package:eatvio_app/features/recipes/presentation/bloc/create_recipe_cubit/create_recipe_state.dart'; -import 'package:eatvio_app/features/recipes/presentation/widgets/image_widget.dart'; -import 'package:eatvio_app/features/recipes/presentation/widgets/portion_widget.dart'; -import 'package:eatvio_app/features/recipes/presentation/widgets/tags_widget.dart'; -import 'package:eatvio_app/features/recipes/presentation/widgets/time_of_cook_widget.dart'; +import 'package:eatvio_app/features/create_recipe/entities/recipe_create_entity.dart'; +import 'package:eatvio_app/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart'; +import 'package:eatvio_app/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_state.dart'; +import 'package:eatvio_app/features/create_recipe/presentation/widgets/image_widget.dart'; +import 'package:eatvio_app/features/create_recipe/presentation/widgets/portion_widget.dart'; +import 'package:eatvio_app/features/create_recipe/presentation/widgets/tags_widget.dart'; +import 'package:eatvio_app/features/create_recipe/presentation/widgets/time_of_cook_widget.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; diff --git a/lib/features/recipes/presentation/widgets/ingredients_widget.dart b/lib/features/create_recipe/presentation/widgets/ingredients_widget.dart similarity index 97% rename from lib/features/recipes/presentation/widgets/ingredients_widget.dart rename to lib/features/create_recipe/presentation/widgets/ingredients_widget.dart index 3983bccf..a3700bb3 100644 --- a/lib/features/recipes/presentation/widgets/ingredients_widget.dart +++ b/lib/features/create_recipe/presentation/widgets/ingredients_widget.dart @@ -1,8 +1,8 @@ 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/recipes/models/ingredient.dart'; -import 'package:eatvio_app/features/recipes/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart'; -import 'package:eatvio_app/features/recipes/presentation/bloc/create_recipe_cubit/create_recipe_state.dart'; +import 'package:eatvio_app/features/create_recipe/models/ingredient.dart'; +import 'package:eatvio_app/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart'; +import 'package:eatvio_app/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_state.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; diff --git a/lib/features/recipes/presentation/widgets/portion_widget.dart b/lib/features/create_recipe/presentation/widgets/portion_widget.dart similarity index 94% rename from lib/features/recipes/presentation/widgets/portion_widget.dart rename to lib/features/create_recipe/presentation/widgets/portion_widget.dart index 569f2c7c..a6044738 100644 --- a/lib/features/recipes/presentation/widgets/portion_widget.dart +++ b/lib/features/create_recipe/presentation/widgets/portion_widget.dart @@ -1,7 +1,7 @@ import 'package:eatvio_app/core/widgets/custom_text_form_field.dart'; import 'package:eatvio_app/features/create_diet/presentation/widgets/error_widget.dart'; -import 'package:eatvio_app/features/recipes/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart'; -import 'package:eatvio_app/features/recipes/presentation/bloc/create_recipe_cubit/create_recipe_state.dart'; +import 'package:eatvio_app/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart'; +import 'package:eatvio_app/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_state.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; diff --git a/lib/features/recipes/presentation/widgets/step_image_widget.dart b/lib/features/create_recipe/presentation/widgets/step_image_widget.dart similarity index 96% rename from lib/features/recipes/presentation/widgets/step_image_widget.dart rename to lib/features/create_recipe/presentation/widgets/step_image_widget.dart index ad4c9549..b2922c92 100644 --- a/lib/features/recipes/presentation/widgets/step_image_widget.dart +++ b/lib/features/create_recipe/presentation/widgets/step_image_widget.dart @@ -1,6 +1,6 @@ import 'dart:io'; -import 'package:eatvio_app/features/recipes/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart'; +import 'package:eatvio_app/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; diff --git a/lib/features/recipes/presentation/widgets/steps_widget.dart b/lib/features/create_recipe/presentation/widgets/steps_widget.dart similarity index 93% rename from lib/features/recipes/presentation/widgets/steps_widget.dart rename to lib/features/create_recipe/presentation/widgets/steps_widget.dart index 6feb9aa6..726438d8 100644 --- a/lib/features/recipes/presentation/widgets/steps_widget.dart +++ b/lib/features/create_recipe/presentation/widgets/steps_widget.dart @@ -1,8 +1,8 @@ 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/recipes/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart'; -import 'package:eatvio_app/features/recipes/presentation/bloc/create_recipe_cubit/create_recipe_state.dart'; -import 'package:eatvio_app/features/recipes/presentation/widgets/step_image_widget.dart'; +import 'package:eatvio_app/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart'; +import 'package:eatvio_app/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_state.dart'; +import 'package:eatvio_app/features/create_recipe/presentation/widgets/step_image_widget.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; diff --git a/lib/features/recipes/presentation/widgets/tags_widget.dart b/lib/features/create_recipe/presentation/widgets/tags_widget.dart similarity index 100% rename from lib/features/recipes/presentation/widgets/tags_widget.dart rename to lib/features/create_recipe/presentation/widgets/tags_widget.dart diff --git a/lib/features/recipes/presentation/widgets/time_of_cook_widget.dart b/lib/features/create_recipe/presentation/widgets/time_of_cook_widget.dart similarity index 97% rename from lib/features/recipes/presentation/widgets/time_of_cook_widget.dart rename to lib/features/create_recipe/presentation/widgets/time_of_cook_widget.dart index e845a923..29c2bdaa 100644 --- a/lib/features/recipes/presentation/widgets/time_of_cook_widget.dart +++ b/lib/features/create_recipe/presentation/widgets/time_of_cook_widget.dart @@ -1,8 +1,8 @@ import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/widgets/custom_text_form_field.dart'; import 'package:eatvio_app/features/create_diet/presentation/widgets/error_widget.dart'; -import 'package:eatvio_app/features/recipes/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart'; -import 'package:eatvio_app/features/recipes/presentation/bloc/create_recipe_cubit/create_recipe_state.dart'; +import 'package:eatvio_app/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart'; +import 'package:eatvio_app/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_state.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; diff --git a/lib/features/recipes/reporitories/recipe_repository.dart b/lib/features/create_recipe/reporitories/recipe_repository.dart similarity index 86% rename from lib/features/recipes/reporitories/recipe_repository.dart rename to lib/features/create_recipe/reporitories/recipe_repository.dart index 4dec15c0..f1284fd6 100644 --- a/lib/features/recipes/reporitories/recipe_repository.dart +++ b/lib/features/create_recipe/reporitories/recipe_repository.dart @@ -1,16 +1,17 @@ +import 'dart:async'; import 'dart:convert'; import 'dart:io'; - import 'package:dio/dio.dart'; import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart'; -import 'package:eatvio_app/features/recipes/models/recipe_model.dart'; -import 'package:eatvio_app/features/recipes/models/recipe_tag_model.dart'; -import 'package:eatvio_app/features/recipes/models/step.dart'; -import 'package:eatvio_app/features/recipes/services/recipe_service.dart'; -import 'package:eatvio_app/features/recipes/models/product.dart'; +import 'package:eatvio_app/features/create_recipe/models/recipe_model.dart'; +import 'package:eatvio_app/features/create_recipe/models/recipe_tag_model.dart'; +import 'package:eatvio_app/features/create_recipe/models/step.dart'; +import 'package:eatvio_app/features/create_recipe/services/recipe_service.dart'; +import 'package:eatvio_app/features/create_recipe/models/product.dart'; class RecipeRepository { - final RecipeService recipeService; + final CreateRecipeService recipeService; + RecipeRepository({required this.recipeService}); @@ -99,5 +100,5 @@ class RecipeRepository { } catch (e) { rethrow; } - } + } } diff --git a/lib/features/create_recipe/services/recipe_service.dart b/lib/features/create_recipe/services/recipe_service.dart new file mode 100644 index 00000000..4bc9c85f --- /dev/null +++ b/lib/features/create_recipe/services/recipe_service.dart @@ -0,0 +1,47 @@ +import 'package:dio/dio.dart'; +import 'package:eatvio_app/features/create_recipe/models/recipe_model.dart'; +import 'package:eatvio_app/features/create_recipe/models/recipe_tag_model.dart'; +import 'package:retrofit/retrofit.dart'; +import 'package:eatvio_app/features/create_recipe/models/product.dart'; + +part 'recipe_service.g.dart'; + +@RestApi() +abstract class CreateRecipeService { + factory CreateRecipeService(Dio dio, {String baseUrl}) = _CreateRecipeService; + + @POST('/api/recipes/{slug}/like') + Future likeRecipe({ + @Path() required String slug, + }); + + @POST('/api/recipes/{slug}/add_to_favorite') + Future addToFavorite({ + @Path() required String slug, + }); + + @POST('/api/recipes/{slug}/remove_from_favorite') + Future removeFromFavorite({ + @Path() required String slug, + }); + + @GET('/api/recipes/show/{id}') + Future getRecipe(@Path() String id); + + @GET('/api/diary/product_nutritional_values/filter') + Future>> getInputProduct(@Query('name') String name); + + @POST('/api/recipes/create') + @MultiPart() + Future publishRecipe({ + @Body() required FormData formData, + }); + + @POST('/api/recipes/update/{id}') + Future editRecipe(@Part() Map recipe, String id); + + @GET('/api/recipes/tags') + Future getTags(); + + +} \ No newline at end of file diff --git a/lib/features/recipes/services/recipe_service.g.dart b/lib/features/create_recipe/services/recipe_service.g.dart similarity index 73% rename from lib/features/recipes/services/recipe_service.g.dart rename to lib/features/create_recipe/services/recipe_service.g.dart index e1e9d280..58b81059 100644 --- a/lib/features/recipes/services/recipe_service.g.dart +++ b/lib/features/create_recipe/services/recipe_service.g.dart @@ -8,8 +8,8 @@ part of 'recipe_service.dart'; // ignore_for_file: unnecessary_brace_in_string_interps,no_leading_underscores_for_local_identifiers,unused_element,unnecessary_string_interpolations -class _RecipeService implements RecipeService { - _RecipeService(this._dio, {this.baseUrl, this.errorLogger}); +class _CreateRecipeService implements CreateRecipeService { + _CreateRecipeService(this._dio, {this.baseUrl, this.errorLogger}); final Dio _dio; @@ -17,6 +17,63 @@ class _RecipeService implements RecipeService { final ParseErrorLogger? errorLogger; + @override + Future likeRecipe({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/recipes/${slug}/like', + queryParameters: queryParameters, + data: _data, + ) + .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), + ); + await _dio.fetch(_options); + } + + @override + Future addToFavorite({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/recipes/${slug}/add_to_favorite', + queryParameters: queryParameters, + data: _data, + ) + .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), + ); + await _dio.fetch(_options); + } + + @override + Future removeFromFavorite({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/recipes/${slug}/remove_from_favorite', + queryParameters: queryParameters, + data: _data, + ) + .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), + ); + await _dio.fetch(_options); + } + @override Future getRecipe(String id) async { final _extra = {}; diff --git a/lib/features/dev_tools/test_cases.dart b/lib/features/dev_tools/test_cases.dart index 30eb9c6a..8c92a104 100644 --- a/lib/features/dev_tools/test_cases.dart +++ b/lib/features/dev_tools/test_cases.dart @@ -8,7 +8,7 @@ import 'package:eatvio_app/features/chat/services/chat_service.dart'; import 'package:eatvio_app/features/diets/services/diet_service.dart'; import 'package:eatvio_app/features/feed/services/feed_service.dart'; import 'package:eatvio_app/features/profile/services/profile_service.dart'; -import 'package:eatvio_app/features/recipes/services/recipe_service.dart'; +import 'package:eatvio_app/features/create_recipe/services/recipe_service.dart'; sealed class TestCases { String get name => throw UnimplementedError(); @@ -247,7 +247,7 @@ class GetRecipes implements TestCases { try { final dio = sl(); final feedService = FeedService(dio, baseUrl: baseUrl); - final recipeService = RecipeService(dio, baseUrl: baseUrl); + final recipeService = CreateRecipeService(dio, baseUrl: baseUrl); final tagsModel = await recipeService.getTags(); final tags = tagsModel.data; final tagsIds = tags.map((e) => e.id).toList(); @@ -431,7 +431,7 @@ class GetRecipesComments implements TestCases { try { final dio = sl(); final feedService = FeedService(dio, baseUrl: baseUrl); - final recipeService = RecipeService(dio, baseUrl: baseUrl); + final recipeService = CreateRecipeService(dio, baseUrl: baseUrl); final tagsModel = await recipeService.getTags(); final tags = tagsModel.data; final tagsIds = tags.map((e) => e.id).toList(); diff --git a/lib/features/diary/presentation/pages/diary_screen.dart b/lib/features/diary/presentation/pages/diary_screen.dart index d0ef5bcb..250f3007 100644 --- a/lib/features/diary/presentation/pages/diary_screen.dart +++ b/lib/features/diary/presentation/pages/diary_screen.dart @@ -1,5 +1,6 @@ import 'dart:developer'; +import 'package:eatvio_app/core/widgets/data_error_widget.dart'; import 'package:eatvio_app/core/widgets/loader.dart'; import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart'; import 'package:eatvio_app/features/diary/presentation/bloc/diary_state.dart'; @@ -27,6 +28,11 @@ class _DiaryScreenState extends State { if (state is DiaryStateLoaded) { log(state.diary.id.toString()); } + if (state is DiaryStateError) { + return Scaffold( + body: DataErrorWidget(onReload: context.read().fetchDiary()), + ); + } return Scaffold( backgroundColor: Colors.grey.shade200, appBar: AppBar( diff --git a/lib/features/diary/presentation/widgets/add_product_modal.dart b/lib/features/diary/presentation/widgets/add_product_modal.dart index 01c5a64d..88d30914 100644 --- a/lib/features/diary/presentation/widgets/add_product_modal.dart +++ b/lib/features/diary/presentation/widgets/add_product_modal.dart @@ -89,6 +89,12 @@ class _AddProductModalState extends State { 'glossary-search', Duration(milliseconds: 250), () async { + selectedGlossary = null; + if (query.isEmpty) { + glossariesList = []; + setState(() {}); + return; + } glossariesList = await widget.diaryCubit.getGlossaryListByQuery(query); setState(() {}); }, @@ -131,7 +137,7 @@ class _AddProductModalState extends State { selectedGlossary = glossariesList[i]; productNameController.text = selectedGlossary!.name; }); - glossariesList = await widget.diaryCubit.getGlossaryListByQuery(glossariesList[i].name); + glossariesList = []; setState(() {}); FocusManager.instance.primaryFocus?.nextFocus(); }, diff --git a/lib/features/diary/presentation/widgets/glossary_moderation_request_modal_widget.dart b/lib/features/diary/presentation/widgets/glossary_moderation_request_modal_widget.dart index 09fd00cb..23e9f00e 100644 --- a/lib/features/diary/presentation/widgets/glossary_moderation_request_modal_widget.dart +++ b/lib/features/diary/presentation/widgets/glossary_moderation_request_modal_widget.dart @@ -31,13 +31,13 @@ class _GlossaryModerationRequestModalWidgetState extends State _alert(), + GlossaryState.addNewProduct => _addNewGlossaryProduct(), + GlossaryState.success => _success(), + }, + ); } @override diff --git a/lib/features/diets/presentation/cubit/about_diet_cubit.dart b/lib/features/diets/presentation/cubit/about_diet_cubit.dart index ea57351d..07532799 100644 --- a/lib/features/diets/presentation/cubit/about_diet_cubit.dart +++ b/lib/features/diets/presentation/cubit/about_diet_cubit.dart @@ -26,6 +26,7 @@ class AboutDietCubit extends Cubit { emit(AboutDietState.loaded(diet: diet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet)); } catch (e) { log(e.toString()); + emit(AboutDietState.error(message: e.toString())); } } @@ -49,6 +50,7 @@ class AboutDietCubit extends Cubit { emit(AboutDietState.loaded(diet: diet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet)); } catch (e) { log(e.toString()); + emit(AboutDietState.error(message: e.toString())); } } diff --git a/lib/features/diets/presentation/pages/about_diet_screen.dart b/lib/features/diets/presentation/pages/about_diet_screen.dart index a701d978..93668286 100644 --- a/lib/features/diets/presentation/pages/about_diet_screen.dart +++ b/lib/features/diets/presentation/pages/about_diet_screen.dart @@ -2,10 +2,13 @@ import 'dart:io'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/enums/complained_content.dart'; +import 'package:eatvio_app/core/enums/product_type.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/custom_divider.dart'; +import 'package:eatvio_app/core/widgets/data_error_widget.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'; @@ -313,6 +316,8 @@ class _AboutDietScreenState extends State { visible: isMoreOptionsVisible, portalFollower: MoreOptions( path: 'diet/${widget.diet.id}', + complainedContentType: ComplainedContent.diet, + id: widget.diet.id, ), anchor: Aligned( shiftToWithinBound: AxisFlag(x: true, y: true), @@ -419,7 +424,10 @@ class _AboutDietScreenState extends State { ), ), SizedBox(height: 12), - InfoRow(diet: widget.diet), + InfoRow( + diet: widget.diet, + type: ProductType.diet, + ), ExpandableText( text: widget.diet.details, ), @@ -466,7 +474,7 @@ class _AboutDietScreenState extends State { padding: EdgeInsets.symmetric(horizontal: 16), alignment: Alignment.centerLeft, child: Text( - dietState.diet.totalComments.toString() + ' ' + getCommentForm(widget.diet.totalComments, s), + dietState.diet.totalComments.toString() + ' ' + getCommentForm(dietState.diet.totalComments, s), style: TextStyles.textMMedium.copyWith( color: appTheme.gray500, ), @@ -489,7 +497,90 @@ class _AboutDietScreenState extends State { ), ); } + if (dietState is DietStateError) { + return Scaffold( + appBar: AppBar( + centerTitle: true, + title: Text( + widget.diet.name, + maxLines: 2, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + softWrap: true, + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), + ), + actions: [ + PortalTarget( + visible: isMoreOptionsVisible, + portalFollower: MoreOptions( + path: 'diet/${widget.diet.id}', + complainedContentType: ComplainedContent.diet, + id: widget.diet.id, + ), + anchor: Aligned( + shiftToWithinBound: AxisFlag(x: true, y: true), + follower: Alignment.topRight, + target: Alignment.bottomRight, + ), + child: IconButton( + onPressed: () { + setState(() { + isMoreOptionsVisible = !isMoreOptionsVisible; + }); + }, + icon: SvgPicture.asset( + 'assets/icons/more_button.svg', + width: 20, + height: 20, + ), + ), + ), + ], + ), + body: DataErrorWidget( + onReload: () => context.read().getDiet(widget.diet.id, index: widget.index), + ), + ); + } return Scaffold( + appBar: AppBar( + centerTitle: true, + title: Text( + widget.diet.name, + maxLines: 2, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + softWrap: true, + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), + ), + actions: [ + PortalTarget( + visible: isMoreOptionsVisible, + portalFollower: MoreOptions( + path: 'diet/${widget.diet.id}', + complainedContentType: ComplainedContent.diet, + id: widget.diet.id, + ), + anchor: Aligned( + shiftToWithinBound: AxisFlag(x: true, y: true), + follower: Alignment.topRight, + target: Alignment.bottomRight, + ), + child: IconButton( + onPressed: () { + setState(() { + isMoreOptionsVisible = !isMoreOptionsVisible; + }); + }, + icon: SvgPicture.asset( + 'assets/icons/more_button.svg', + width: 20, + height: 20, + ), + ), + ), + ], + ), body: const Loader(), ); }, diff --git a/lib/features/diets/presentation/widgets/expandable_text.dart b/lib/features/diets/presentation/widgets/expandable_text.dart index ab490df4..37285909 100644 --- a/lib/features/diets/presentation/widgets/expandable_text.dart +++ b/lib/features/diets/presentation/widgets/expandable_text.dart @@ -40,6 +40,7 @@ class _ExpandableTextState extends State { data: widget.text, style: { "*": Style( + margin: Margins.all(0), color: Colors.black87, maxLines: isExpanded ? null : widget.maxLines, textOverflow: TextOverflow.clip, diff --git a/lib/features/diets/presentation/widgets/more_options.dart b/lib/features/diets/presentation/widgets/more_options.dart index 01da9a87..3347144c 100644 --- a/lib/features/diets/presentation/widgets/more_options.dart +++ b/lib/features/diets/presentation/widgets/more_options.dart @@ -1,5 +1,7 @@ import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/enums/complained_content.dart'; import 'package:eatvio_app/core/utils/toasts.dart'; +import 'package:eatvio_app/features/complaining/presentation/complaining_bottom_sheet.dart'; import 'package:eatvio_app/core/widgets/custom_divider.dart'; import 'package:eatvio_app/core/widgets/main_background_bouble.dart'; import 'package:eatvio_app/generated/l10n.dart'; @@ -8,12 +10,13 @@ import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:go_router/go_router.dart'; import 'package:share_plus/share_plus.dart'; class MoreOptions extends StatelessWidget { - const MoreOptions({super.key, this.disableActions, required this.path}); - + const MoreOptions({super.key, this.disableActions, required this.path, this.complainedContentType, this.slug, this.id}); + final ComplainedContent? complainedContentType; + final int? id; + final String? slug; final String path; final bool? disableActions; @@ -84,7 +87,16 @@ class MoreOptions extends StatelessWidget { onTap: disableActions == true ? null : () { - context.push('complainingScreen'); + if (complainedContentType != null) { + showModalBottomSheet( + context: context, + builder: (context) => ComplainingBottomSheet( + complainedContentType: complainedContentType!, + id: id, + slug: slug, + ), + ); + } }, child: Row( children: [ diff --git a/lib/features/feed/models/diets_pagination_model.dart b/lib/features/feed/models/diets_pagination_model.dart index c67b8e07..3907fe8a 100644 --- a/lib/features/feed/models/diets_pagination_model.dart +++ b/lib/features/feed/models/diets_pagination_model.dart @@ -96,7 +96,7 @@ class DailySchedule with _$DailySchedule { @JsonKey(name: "name") required String name, @JsonKey(name: "portion") required Portion portion, @JsonKey(name: "quantity", fromJson: _parseQuantity) required int quantity, - @JsonKey(includeToJson: false, includeFromJson: false) DailyNutrition? dailyNutrition, + @JsonKey(name: 'nutrition', includeToJson: false) DailyNutrition? dailyNutrition, @JsonKey(name: 'product_nutritional_value_id', includeToJson: true, includeFromJson: false, includeIfNull: false) String? productNutritionalValueId, }) = _DailySchedule; @@ -117,10 +117,21 @@ enum Portion { @JsonValue("milliliters") MILLILITERS, @JsonValue("pcs") - PCS -} + PCS; -final portionValues = EnumValues({"grams": Portion.GRAMS, "milliliters": Portion.MILLILITERS, "pcs": Portion.PCS}); + static Portion? getPortionFromValue(String value) { + switch (value) { + case 'gr': + return Portion.GRAMS; + case 'milliliters': + return Portion.MILLILITERS; + case 'pcs': + return Portion.PCS; + default: + return null; + } + } +} @freezed class DietType with _$DietType { diff --git a/lib/features/feed/models/diets_pagination_model.freezed.dart b/lib/features/feed/models/diets_pagination_model.freezed.dart index cc185651..fcbf3b63 100644 --- a/lib/features/feed/models/diets_pagination_model.freezed.dart +++ b/lib/features/feed/models/diets_pagination_model.freezed.dart @@ -1246,7 +1246,7 @@ mixin _$DailySchedule { Portion get portion => throw _privateConstructorUsedError; @JsonKey(name: "quantity", fromJson: _parseQuantity) int get quantity => throw _privateConstructorUsedError; - @JsonKey(includeToJson: false, includeFromJson: false) + @JsonKey(name: 'nutrition', includeToJson: false) DailyNutrition? get dailyNutrition => throw _privateConstructorUsedError; @JsonKey( name: 'product_nutritional_value_id', @@ -1275,7 +1275,7 @@ abstract class $DailyScheduleCopyWith<$Res> { {@JsonKey(name: "name") String name, @JsonKey(name: "portion") Portion portion, @JsonKey(name: "quantity", fromJson: _parseQuantity) int quantity, - @JsonKey(includeToJson: false, includeFromJson: false) + @JsonKey(name: 'nutrition', includeToJson: false) DailyNutrition? dailyNutrition, @JsonKey( name: 'product_nutritional_value_id', @@ -1359,7 +1359,7 @@ abstract class _$$DailyScheduleImplCopyWith<$Res> {@JsonKey(name: "name") String name, @JsonKey(name: "portion") Portion portion, @JsonKey(name: "quantity", fromJson: _parseQuantity) int quantity, - @JsonKey(includeToJson: false, includeFromJson: false) + @JsonKey(name: 'nutrition', includeToJson: false) DailyNutrition? dailyNutrition, @JsonKey( name: 'product_nutritional_value_id', @@ -1424,8 +1424,7 @@ class _$DailyScheduleImpl implements _DailySchedule { @JsonKey(name: "portion") required this.portion, @JsonKey(name: "quantity", fromJson: _parseQuantity) required this.quantity, - @JsonKey(includeToJson: false, includeFromJson: false) - this.dailyNutrition, + @JsonKey(name: 'nutrition', includeToJson: false) this.dailyNutrition, @JsonKey( name: 'product_nutritional_value_id', includeToJson: true, @@ -1446,7 +1445,7 @@ class _$DailyScheduleImpl implements _DailySchedule { @JsonKey(name: "quantity", fromJson: _parseQuantity) final int quantity; @override - @JsonKey(includeToJson: false, includeFromJson: false) + @JsonKey(name: 'nutrition', includeToJson: false) final DailyNutrition? dailyNutrition; @override @JsonKey( @@ -1504,7 +1503,7 @@ abstract class _DailySchedule implements DailySchedule { @JsonKey(name: "portion") required final Portion portion, @JsonKey(name: "quantity", fromJson: _parseQuantity) required final int quantity, - @JsonKey(includeToJson: false, includeFromJson: false) + @JsonKey(name: 'nutrition', includeToJson: false) final DailyNutrition? dailyNutrition, @JsonKey( name: 'product_nutritional_value_id', @@ -1526,7 +1525,7 @@ abstract class _DailySchedule implements DailySchedule { @JsonKey(name: "quantity", fromJson: _parseQuantity) int get quantity; @override - @JsonKey(includeToJson: false, includeFromJson: false) + @JsonKey(name: 'nutrition', includeToJson: false) DailyNutrition? get dailyNutrition; @override @JsonKey( diff --git a/lib/features/feed/models/diets_pagination_model.g.dart b/lib/features/feed/models/diets_pagination_model.g.dart index 021fbfcf..3832c00d 100644 --- a/lib/features/feed/models/diets_pagination_model.g.dart +++ b/lib/features/feed/models/diets_pagination_model.g.dart @@ -6,57 +6,49 @@ part of 'diets_pagination_model.dart'; // JsonSerializableGenerator // ************************************************************************** -_$DietsPaginationImpl _$$DietsPaginationImplFromJson( - Map json) => - _$DietsPaginationImpl( - data: (json['data'] as List) - .map((e) => Diet.fromJson(e as Map)) - .toList(), +_$DietsPaginationImpl _$$DietsPaginationImplFromJson(Map json) => _$DietsPaginationImpl( + data: (json['data'] as List).map((e) => Diet.fromJson(e as Map)).toList(), links: Links.fromJson(json['links'] as Map), meta: Meta.fromJson(json['meta'] as Map), ); -Map _$$DietsPaginationImplToJson( - _$DietsPaginationImpl instance) => - { +Map _$$DietsPaginationImplToJson(_$DietsPaginationImpl instance) => { 'data': instance.data, 'links': instance.links, 'meta': instance.meta, }; -_$DietImpl _$$DietImplFromJson(Map json) => _$DietImpl( - id: (json['id'] as num).toInt(), - name: json['name'] as String, - image: json['image'] as String, - duration: (json['duration'] as num).toInt(), - state: $enumDecode(_$DietStateEnumMap, json['state']), - type: DietType.fromJson(json['type'] as Map), - details: json['details'] as String, - dailySchedule: (json['daily_schedule'] as Map).map( - (k, e) => MapEntry( - int.parse(k), - (e as Map).map( - (k, e) => MapEntry( - int.parse(k), - (e as Map).map( - (k, e) => MapEntry(int.parse(k), - DailySchedule.fromJson(e as Map)), - )), - )), - ), - author: Author.fromJson(json['author'] as Map), - totalLikes: (json['total_likes'] as num).toInt(), - totalViews: (json['total_views'] as num).toInt(), - totalComments: (json['total_comments'] as num).toInt(), - wasLikedByMe: json['was_liked_by_me'] as bool, - isFavorite: json['is_favorite'] as bool, - dailyNutrition: (json['daily_nutrition'] as List) - .map((e) => DailyNutrition.fromJson(e as Map)) - .toList(), - ); +_$DietImpl _$$DietImplFromJson(Map json) { + return _$DietImpl( + id: (json['id'] as num).toInt(), + name: json['name'] as String, + image: json['image'] as String, + duration: (json['duration'] as num).toInt(), + state: $enumDecode(_$DietStateEnumMap, json['state']), + type: DietType.fromJson(json['type'] as Map), + details: json['details'] as String, + dailySchedule: (json['daily_schedule'] as Map).map( + (k, e) => MapEntry( + int.parse(k), + (e as Map).map( + (k, e) => MapEntry( + int.parse(k), + (e as Map).map( + (k, e) => MapEntry(int.parse(k), DailySchedule.fromJson(e as Map)), + )), + )), + ), + author: Author.fromJson(json['author'] as Map), + totalLikes: (json['total_likes'] as num).toInt(), + totalViews: (json['total_views'] as num).toInt(), + totalComments: (json['total_comments'] as num).toInt(), + wasLikedByMe: json['was_liked_by_me'] as bool, + isFavorite: json['is_favorite'] as bool, + dailyNutrition: (json['daily_nutrition'] as List).map((e) => DailyNutrition.fromJson(e as Map)).toList(), + ); +} -Map _$$DietImplToJson(_$DietImpl instance) => - { +Map _$$DietImplToJson(_$DietImpl instance) => { 'id': instance.id, 'name': instance.name, 'image': instance.image, @@ -64,10 +56,8 @@ Map _$$DietImplToJson(_$DietImpl instance) => 'state': _$DietStateEnumMap[instance.state]!, 'type': instance.type, 'details': instance.details, - 'daily_schedule': instance.dailySchedule.map((k, e) => MapEntry( - k.toString(), - e.map((k, e) => MapEntry( - k.toString(), e.map((k, e) => MapEntry(k.toString(), e)))))), + 'daily_schedule': + instance.dailySchedule.map((k, e) => MapEntry(k.toString(), e.map((k, e) => MapEntry(k.toString(), e.map((k, e) => MapEntry(k.toString(), e)))))), 'author': instance.author, 'total_likes': instance.totalLikes, 'total_views': instance.totalViews, @@ -83,13 +73,11 @@ const _$DietStateEnumMap = { DietState.personal: 'personal', }; -_$CreatedDietImpl _$$CreatedDietImplFromJson(Map json) => - _$CreatedDietImpl( +_$CreatedDietImpl _$$CreatedDietImplFromJson(Map json) => _$CreatedDietImpl( id: (json['id'] as num).toInt(), ); -Map _$$CreatedDietImplToJson(_$CreatedDietImpl instance) => - { +Map _$$CreatedDietImplToJson(_$CreatedDietImpl instance) => { 'id': instance.id, }; @@ -102,8 +90,7 @@ _$AuthorImpl _$$AuthorImplFromJson(Map json) => _$AuthorImpl( authorPlaceInRating: (json['author_place_in_rating'] as num?)?.toInt(), ); -Map _$$AuthorImplToJson(_$AuthorImpl instance) => - { +Map _$$AuthorImplToJson(_$AuthorImpl instance) => { 'author_slug': instance.authorSlug, 'author_name': instance.authorName, 'author_profile_icon': instance.authorProfileIcon, @@ -112,20 +99,18 @@ Map _$$AuthorImplToJson(_$AuthorImpl instance) => 'author_place_in_rating': instance.authorPlaceInRating, }; -_$DailyScheduleImpl _$$DailyScheduleImplFromJson(Map json) => - _$DailyScheduleImpl( +_$DailyScheduleImpl _$$DailyScheduleImplFromJson(Map json) => _$DailyScheduleImpl( name: json['name'] as String, portion: $enumDecode(_$PortionEnumMap, json['portion']), quantity: _parseQuantity(json['quantity']), + dailyNutrition: json['nutrition'] == null ? null : DailyNutrition.fromJson(json['nutrition'] as Map), ); -Map _$$DailyScheduleImplToJson(_$DailyScheduleImpl instance) => - { +Map _$$DailyScheduleImplToJson(_$DailyScheduleImpl instance) => { 'name': instance.name, 'portion': _$PortionEnumMap[instance.portion]!, 'quantity': instance.quantity, - if (instance.productNutritionalValueId case final value?) - 'product_nutritional_value_id': value, + if (instance.productNutritionalValueId case final value?) 'product_nutritional_value_id': value, }; const _$PortionEnumMap = { @@ -134,33 +119,28 @@ const _$PortionEnumMap = { Portion.PCS: 'pcs', }; -_$DietTypeImpl _$$DietTypeImplFromJson(Map json) => - _$DietTypeImpl( +_$DietTypeImpl _$$DietTypeImplFromJson(Map json) => _$DietTypeImpl( light: json['light'] as bool?, veg: json['veg'] as bool?, carb: json['carb'] as bool?, meat: json['meat'] as bool?, ); -Map _$$DietTypeImplToJson(_$DietTypeImpl instance) => - { +Map _$$DietTypeImplToJson(_$DietTypeImpl instance) => { 'light': instance.light, 'veg': instance.veg, 'carb': instance.carb, 'meat': instance.meat, }; -_$DailyNutritionImpl _$$DailyNutritionImplFromJson(Map json) => - _$DailyNutritionImpl( +_$DailyNutritionImpl _$$DailyNutritionImplFromJson(Map json) => _$DailyNutritionImpl( energy: (json['energy'] as num).toDouble(), proteins: (json['proteins'] as num).toDouble(), fats: (json['fats'] as num).toDouble(), carbohydrates: (json['carbohydrates'] as num).toDouble(), ); -Map _$$DailyNutritionImplToJson( - _$DailyNutritionImpl instance) => - { +Map _$$DailyNutritionImplToJson(_$DailyNutritionImpl instance) => { 'energy': instance.energy, 'proteins': instance.proteins, 'fats': instance.fats, diff --git a/lib/features/feed/models/recipes_model.dart b/lib/features/feed/models/recipes_model.dart index ff287211..f32fc011 100644 --- a/lib/features/feed/models/recipes_model.dart +++ b/lib/features/feed/models/recipes_model.dart @@ -5,12 +5,13 @@ part 'recipes_model.freezed.dart'; part 'recipes_model.g.dart'; @freezed -class Recipes with _$Recipes { - const factory Recipes({ +class Recipe with _$Recipe { + const factory Recipe({ required int id, required String image, required String state, @JsonKey(name: 'cooking_time') required int cookingTime, + @JsonKey(name: 'active_time') required int activeCookingTime, required List? ingredients, required String description, required List? steps, @@ -22,13 +23,14 @@ class Recipes with _$Recipes { required String slug, @JsonKey(name: 'total_comments') required int commentsCount, @JsonKey(name: 'was_liked_by_me') required bool wasLikedByMe, + @JsonKey(name: 'is_favorite') required bool isFavorite, @JsonKey(name: 'energy_per_serving') required double calories, @JsonKey(name: 'total_likes') required int likesCount, @JsonKey(name: 'total_views') required int viewsCount, required Author author, }) = _Recipes; - factory Recipes.fromJson(Map json) => _$RecipesFromJson(json); + factory Recipe.fromJson(Map json) => _$RecipeFromJson(json); } @freezed diff --git a/lib/features/feed/models/recipes_model.freezed.dart b/lib/features/feed/models/recipes_model.freezed.dart index 75d6ea7b..6123bc74 100644 --- a/lib/features/feed/models/recipes_model.freezed.dart +++ b/lib/features/feed/models/recipes_model.freezed.dart @@ -14,17 +14,19 @@ 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'); -Recipes _$RecipesFromJson(Map json) { +Recipe _$RecipeFromJson(Map json) { return _Recipes.fromJson(json); } /// @nodoc -mixin _$Recipes { +mixin _$Recipe { int get id => throw _privateConstructorUsedError; String get image => throw _privateConstructorUsedError; String get state => throw _privateConstructorUsedError; @JsonKey(name: 'cooking_time') int get cookingTime => throw _privateConstructorUsedError; + @JsonKey(name: 'active_time') + int get activeCookingTime => throw _privateConstructorUsedError; List? get ingredients => throw _privateConstructorUsedError; String get description => throw _privateConstructorUsedError; List? get steps => throw _privateConstructorUsedError; @@ -41,6 +43,8 @@ mixin _$Recipes { int get commentsCount => throw _privateConstructorUsedError; @JsonKey(name: 'was_liked_by_me') bool get wasLikedByMe => throw _privateConstructorUsedError; + @JsonKey(name: 'is_favorite') + bool get isFavorite => throw _privateConstructorUsedError; @JsonKey(name: 'energy_per_serving') double get calories => throw _privateConstructorUsedError; @JsonKey(name: 'total_likes') @@ -49,25 +53,26 @@ mixin _$Recipes { int get viewsCount => throw _privateConstructorUsedError; Author get author => throw _privateConstructorUsedError; - /// Serializes this Recipes to a JSON map. + /// Serializes this Recipe to a JSON map. Map toJson() => throw _privateConstructorUsedError; - /// Create a copy of Recipes + /// Create a copy of Recipe /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) - $RecipesCopyWith get copyWith => throw _privateConstructorUsedError; + $RecipeCopyWith get copyWith => throw _privateConstructorUsedError; } /// @nodoc -abstract class $RecipesCopyWith<$Res> { - factory $RecipesCopyWith(Recipes value, $Res Function(Recipes) then) = - _$RecipesCopyWithImpl<$Res, Recipes>; +abstract class $RecipeCopyWith<$Res> { + factory $RecipeCopyWith(Recipe value, $Res Function(Recipe) then) = + _$RecipeCopyWithImpl<$Res, Recipe>; @useResult $Res call( {int id, String image, String state, @JsonKey(name: 'cooking_time') int cookingTime, + @JsonKey(name: 'active_time') int activeCookingTime, List? ingredients, String description, List? steps, @@ -79,6 +84,7 @@ abstract class $RecipesCopyWith<$Res> { String slug, @JsonKey(name: 'total_comments') int commentsCount, @JsonKey(name: 'was_liked_by_me') bool wasLikedByMe, + @JsonKey(name: 'is_favorite') bool isFavorite, @JsonKey(name: 'energy_per_serving') double calories, @JsonKey(name: 'total_likes') int likesCount, @JsonKey(name: 'total_views') int viewsCount, @@ -88,16 +94,16 @@ abstract class $RecipesCopyWith<$Res> { } /// @nodoc -class _$RecipesCopyWithImpl<$Res, $Val extends Recipes> - implements $RecipesCopyWith<$Res> { - _$RecipesCopyWithImpl(this._value, this._then); +class _$RecipeCopyWithImpl<$Res, $Val extends Recipe> + implements $RecipeCopyWith<$Res> { + _$RecipeCopyWithImpl(this._value, this._then); // ignore: unused_field final $Val _value; // ignore: unused_field final $Res Function($Val) _then; - /// Create a copy of Recipes + /// Create a copy of Recipe /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override @@ -106,6 +112,7 @@ class _$RecipesCopyWithImpl<$Res, $Val extends Recipes> Object? image = null, Object? state = null, Object? cookingTime = null, + Object? activeCookingTime = null, Object? ingredients = freezed, Object? description = null, Object? steps = freezed, @@ -117,6 +124,7 @@ class _$RecipesCopyWithImpl<$Res, $Val extends Recipes> Object? slug = null, Object? commentsCount = null, Object? wasLikedByMe = null, + Object? isFavorite = null, Object? calories = null, Object? likesCount = null, Object? viewsCount = null, @@ -139,6 +147,10 @@ class _$RecipesCopyWithImpl<$Res, $Val extends Recipes> ? _value.cookingTime : cookingTime // ignore: cast_nullable_to_non_nullable as int, + activeCookingTime: null == activeCookingTime + ? _value.activeCookingTime + : activeCookingTime // ignore: cast_nullable_to_non_nullable + as int, ingredients: freezed == ingredients ? _value.ingredients : ingredients // ignore: cast_nullable_to_non_nullable @@ -183,6 +195,10 @@ class _$RecipesCopyWithImpl<$Res, $Val extends Recipes> ? _value.wasLikedByMe : wasLikedByMe // ignore: cast_nullable_to_non_nullable as bool, + isFavorite: null == isFavorite + ? _value.isFavorite + : isFavorite // ignore: cast_nullable_to_non_nullable + as bool, calories: null == calories ? _value.calories : calories // ignore: cast_nullable_to_non_nullable @@ -202,7 +218,7 @@ class _$RecipesCopyWithImpl<$Res, $Val extends Recipes> ) as $Val); } - /// Create a copy of Recipes + /// Create a copy of Recipe /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') @@ -214,7 +230,7 @@ class _$RecipesCopyWithImpl<$Res, $Val extends Recipes> } /// @nodoc -abstract class _$$RecipesImplCopyWith<$Res> implements $RecipesCopyWith<$Res> { +abstract class _$$RecipesImplCopyWith<$Res> implements $RecipeCopyWith<$Res> { factory _$$RecipesImplCopyWith( _$RecipesImpl value, $Res Function(_$RecipesImpl) then) = __$$RecipesImplCopyWithImpl<$Res>; @@ -225,6 +241,7 @@ abstract class _$$RecipesImplCopyWith<$Res> implements $RecipesCopyWith<$Res> { String image, String state, @JsonKey(name: 'cooking_time') int cookingTime, + @JsonKey(name: 'active_time') int activeCookingTime, List? ingredients, String description, List? steps, @@ -236,6 +253,7 @@ abstract class _$$RecipesImplCopyWith<$Res> implements $RecipesCopyWith<$Res> { String slug, @JsonKey(name: 'total_comments') int commentsCount, @JsonKey(name: 'was_liked_by_me') bool wasLikedByMe, + @JsonKey(name: 'is_favorite') bool isFavorite, @JsonKey(name: 'energy_per_serving') double calories, @JsonKey(name: 'total_likes') int likesCount, @JsonKey(name: 'total_views') int viewsCount, @@ -247,13 +265,13 @@ abstract class _$$RecipesImplCopyWith<$Res> implements $RecipesCopyWith<$Res> { /// @nodoc class __$$RecipesImplCopyWithImpl<$Res> - extends _$RecipesCopyWithImpl<$Res, _$RecipesImpl> + extends _$RecipeCopyWithImpl<$Res, _$RecipesImpl> implements _$$RecipesImplCopyWith<$Res> { __$$RecipesImplCopyWithImpl( _$RecipesImpl _value, $Res Function(_$RecipesImpl) _then) : super(_value, _then); - /// Create a copy of Recipes + /// Create a copy of Recipe /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override @@ -262,6 +280,7 @@ class __$$RecipesImplCopyWithImpl<$Res> Object? image = null, Object? state = null, Object? cookingTime = null, + Object? activeCookingTime = null, Object? ingredients = freezed, Object? description = null, Object? steps = freezed, @@ -273,6 +292,7 @@ class __$$RecipesImplCopyWithImpl<$Res> Object? slug = null, Object? commentsCount = null, Object? wasLikedByMe = null, + Object? isFavorite = null, Object? calories = null, Object? likesCount = null, Object? viewsCount = null, @@ -295,6 +315,10 @@ class __$$RecipesImplCopyWithImpl<$Res> ? _value.cookingTime : cookingTime // ignore: cast_nullable_to_non_nullable as int, + activeCookingTime: null == activeCookingTime + ? _value.activeCookingTime + : activeCookingTime // ignore: cast_nullable_to_non_nullable + as int, ingredients: freezed == ingredients ? _value._ingredients : ingredients // ignore: cast_nullable_to_non_nullable @@ -339,6 +363,10 @@ class __$$RecipesImplCopyWithImpl<$Res> ? _value.wasLikedByMe : wasLikedByMe // ignore: cast_nullable_to_non_nullable as bool, + isFavorite: null == isFavorite + ? _value.isFavorite + : isFavorite // ignore: cast_nullable_to_non_nullable + as bool, calories: null == calories ? _value.calories : calories // ignore: cast_nullable_to_non_nullable @@ -367,6 +395,7 @@ class _$RecipesImpl implements _Recipes { required this.image, required this.state, @JsonKey(name: 'cooking_time') required this.cookingTime, + @JsonKey(name: 'active_time') required this.activeCookingTime, required final List? ingredients, required this.description, required final List? steps, @@ -378,6 +407,7 @@ class _$RecipesImpl implements _Recipes { required this.slug, @JsonKey(name: 'total_comments') required this.commentsCount, @JsonKey(name: 'was_liked_by_me') required this.wasLikedByMe, + @JsonKey(name: 'is_favorite') required this.isFavorite, @JsonKey(name: 'energy_per_serving') required this.calories, @JsonKey(name: 'total_likes') required this.likesCount, @JsonKey(name: 'total_views') required this.viewsCount, @@ -397,6 +427,9 @@ class _$RecipesImpl implements _Recipes { @override @JsonKey(name: 'cooking_time') final int cookingTime; + @override + @JsonKey(name: 'active_time') + final int activeCookingTime; final List? _ingredients; @override List? get ingredients { @@ -441,6 +474,9 @@ class _$RecipesImpl implements _Recipes { @JsonKey(name: 'was_liked_by_me') final bool wasLikedByMe; @override + @JsonKey(name: 'is_favorite') + final bool isFavorite; + @override @JsonKey(name: 'energy_per_serving') final double calories; @override @@ -454,7 +490,7 @@ class _$RecipesImpl implements _Recipes { @override String toString() { - return 'Recipes(id: $id, image: $image, state: $state, cookingTime: $cookingTime, ingredients: $ingredients, description: $description, steps: $steps, videoUrl: $videoUrl, createdAt: $createdAt, updatedAt: $updatedAt, title: $title, type: $type, slug: $slug, commentsCount: $commentsCount, wasLikedByMe: $wasLikedByMe, calories: $calories, likesCount: $likesCount, viewsCount: $viewsCount, author: $author)'; + return 'Recipe(id: $id, image: $image, state: $state, cookingTime: $cookingTime, activeCookingTime: $activeCookingTime, ingredients: $ingredients, description: $description, steps: $steps, videoUrl: $videoUrl, createdAt: $createdAt, updatedAt: $updatedAt, title: $title, type: $type, slug: $slug, commentsCount: $commentsCount, wasLikedByMe: $wasLikedByMe, isFavorite: $isFavorite, calories: $calories, likesCount: $likesCount, viewsCount: $viewsCount, author: $author)'; } @override @@ -467,6 +503,8 @@ class _$RecipesImpl implements _Recipes { (identical(other.state, state) || other.state == state) && (identical(other.cookingTime, cookingTime) || other.cookingTime == cookingTime) && + (identical(other.activeCookingTime, activeCookingTime) || + other.activeCookingTime == activeCookingTime) && const DeepCollectionEquality() .equals(other._ingredients, _ingredients) && (identical(other.description, description) || @@ -485,6 +523,8 @@ class _$RecipesImpl implements _Recipes { other.commentsCount == commentsCount) && (identical(other.wasLikedByMe, wasLikedByMe) || other.wasLikedByMe == wasLikedByMe) && + (identical(other.isFavorite, isFavorite) || + other.isFavorite == isFavorite) && (identical(other.calories, calories) || other.calories == calories) && (identical(other.likesCount, likesCount) || @@ -502,6 +542,7 @@ class _$RecipesImpl implements _Recipes { image, state, cookingTime, + activeCookingTime, const DeepCollectionEquality().hash(_ingredients), description, const DeepCollectionEquality().hash(_steps), @@ -513,13 +554,14 @@ class _$RecipesImpl implements _Recipes { slug, commentsCount, wasLikedByMe, + isFavorite, calories, likesCount, viewsCount, author ]); - /// Create a copy of Recipes + /// Create a copy of Recipe /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @override @@ -535,12 +577,13 @@ class _$RecipesImpl implements _Recipes { } } -abstract class _Recipes implements Recipes { +abstract class _Recipes implements Recipe { const factory _Recipes( {required final int id, required final String image, required final String state, @JsonKey(name: 'cooking_time') required final int cookingTime, + @JsonKey(name: 'active_time') required final int activeCookingTime, required final List? ingredients, required final String description, required final List? steps, @@ -552,6 +595,7 @@ abstract class _Recipes implements Recipes { required final String slug, @JsonKey(name: 'total_comments') required final int commentsCount, @JsonKey(name: 'was_liked_by_me') required final bool wasLikedByMe, + @JsonKey(name: 'is_favorite') required final bool isFavorite, @JsonKey(name: 'energy_per_serving') required final double calories, @JsonKey(name: 'total_likes') required final int likesCount, @JsonKey(name: 'total_views') required final int viewsCount, @@ -569,6 +613,9 @@ abstract class _Recipes implements Recipes { @JsonKey(name: 'cooking_time') int get cookingTime; @override + @JsonKey(name: 'active_time') + int get activeCookingTime; + @override List? get ingredients; @override String get description; @@ -596,6 +643,9 @@ abstract class _Recipes implements Recipes { @JsonKey(name: 'was_liked_by_me') bool get wasLikedByMe; @override + @JsonKey(name: 'is_favorite') + bool get isFavorite; + @override @JsonKey(name: 'energy_per_serving') double get calories; @override @@ -607,7 +657,7 @@ abstract class _Recipes implements Recipes { @override Author get author; - /// Create a copy of Recipes + /// Create a copy of Recipe /// with the given fields replaced by the non-null parameter values. @override @JsonKey(includeFromJson: false, includeToJson: false) diff --git a/lib/features/feed/models/recipes_model.g.dart b/lib/features/feed/models/recipes_model.g.dart index 00da0654..c0a684d9 100644 --- a/lib/features/feed/models/recipes_model.g.dart +++ b/lib/features/feed/models/recipes_model.g.dart @@ -12,6 +12,7 @@ _$RecipesImpl _$$RecipesImplFromJson(Map json) => image: json['image'] as String, state: json['state'] as String, cookingTime: (json['cooking_time'] as num).toInt(), + activeCookingTime: (json['active_time'] as num).toInt(), ingredients: (json['ingredients'] as List?) ?.map((e) => Ingredient.fromJson(e as Map)) .toList(), @@ -27,6 +28,7 @@ _$RecipesImpl _$$RecipesImplFromJson(Map json) => slug: json['slug'] as String, commentsCount: (json['total_comments'] as num).toInt(), wasLikedByMe: json['was_liked_by_me'] as bool, + isFavorite: json['is_favorite'] as bool, calories: (json['energy_per_serving'] as num).toDouble(), likesCount: (json['total_likes'] as num).toInt(), viewsCount: (json['total_views'] as num).toInt(), @@ -39,6 +41,7 @@ Map _$$RecipesImplToJson(_$RecipesImpl instance) => 'image': instance.image, 'state': instance.state, 'cooking_time': instance.cookingTime, + 'active_time': instance.activeCookingTime, 'ingredients': instance.ingredients, 'description': instance.description, 'steps': instance.steps, @@ -50,6 +53,7 @@ Map _$$RecipesImplToJson(_$RecipesImpl instance) => 'slug': instance.slug, 'total_comments': instance.commentsCount, 'was_liked_by_me': instance.wasLikedByMe, + 'is_favorite': instance.isFavorite, 'energy_per_serving': instance.calories, 'total_likes': instance.likesCount, 'total_views': instance.viewsCount, diff --git a/lib/features/feed/models/recipes_pagination_model.dart b/lib/features/feed/models/recipes_pagination_model.dart index 25384082..315266a1 100644 --- a/lib/features/feed/models/recipes_pagination_model.dart +++ b/lib/features/feed/models/recipes_pagination_model.dart @@ -8,7 +8,7 @@ part 'recipes_pagination_model.g.dart'; @freezed class RecipesPagination with _$RecipesPagination { const factory RecipesPagination({ - @JsonKey(name: 'data') required List recipesList, + @JsonKey(name: 'data') required List recipesList, // @JsonKey(name: "current_page") required int currentPage, // @JsonKey(name: "first_page_url") required String firstPageUrl, // required int from, diff --git a/lib/features/feed/models/recipes_pagination_model.freezed.dart b/lib/features/feed/models/recipes_pagination_model.freezed.dart index ca8e9f92..4e05ed5a 100644 --- a/lib/features/feed/models/recipes_pagination_model.freezed.dart +++ b/lib/features/feed/models/recipes_pagination_model.freezed.dart @@ -21,7 +21,7 @@ RecipesPagination _$RecipesPaginationFromJson(Map json) { /// @nodoc mixin _$RecipesPagination { @JsonKey(name: 'data') - List get recipesList => throw _privateConstructorUsedError; + List get recipesList => throw _privateConstructorUsedError; /// Serializes this RecipesPagination to a JSON map. Map toJson() => throw _privateConstructorUsedError; @@ -39,7 +39,7 @@ abstract class $RecipesPaginationCopyWith<$Res> { RecipesPagination value, $Res Function(RecipesPagination) then) = _$RecipesPaginationCopyWithImpl<$Res, RecipesPagination>; @useResult - $Res call({@JsonKey(name: 'data') List recipesList}); + $Res call({@JsonKey(name: 'data') List recipesList}); } /// @nodoc @@ -63,7 +63,7 @@ class _$RecipesPaginationCopyWithImpl<$Res, $Val extends RecipesPagination> recipesList: null == recipesList ? _value.recipesList : recipesList // ignore: cast_nullable_to_non_nullable - as List, + as List, ) as $Val); } } @@ -76,7 +76,7 @@ abstract class _$$RecipesPaginationImplCopyWith<$Res> __$$RecipesPaginationImplCopyWithImpl<$Res>; @override @useResult - $Res call({@JsonKey(name: 'data') List recipesList}); + $Res call({@JsonKey(name: 'data') List recipesList}); } /// @nodoc @@ -98,7 +98,7 @@ class __$$RecipesPaginationImplCopyWithImpl<$Res> recipesList: null == recipesList ? _value._recipesList : recipesList // ignore: cast_nullable_to_non_nullable - as List, + as List, )); } } @@ -107,16 +107,16 @@ class __$$RecipesPaginationImplCopyWithImpl<$Res> @JsonSerializable() class _$RecipesPaginationImpl implements _RecipesPagination { const _$RecipesPaginationImpl( - {@JsonKey(name: 'data') required final List recipesList}) + {@JsonKey(name: 'data') required final List recipesList}) : _recipesList = recipesList; factory _$RecipesPaginationImpl.fromJson(Map json) => _$$RecipesPaginationImplFromJson(json); - final List _recipesList; + final List _recipesList; @override @JsonKey(name: 'data') - List get recipesList { + List get recipesList { if (_recipesList is EqualUnmodifiableListView) return _recipesList; // ignore: implicit_dynamic_type return EqualUnmodifiableListView(_recipesList); @@ -160,7 +160,7 @@ class _$RecipesPaginationImpl implements _RecipesPagination { abstract class _RecipesPagination implements RecipesPagination { const factory _RecipesPagination( - {@JsonKey(name: 'data') required final List recipesList}) = + {@JsonKey(name: 'data') required final List recipesList}) = _$RecipesPaginationImpl; factory _RecipesPagination.fromJson(Map json) = @@ -168,7 +168,7 @@ abstract class _RecipesPagination implements RecipesPagination { @override @JsonKey(name: 'data') - List get recipesList; + List get recipesList; /// Create a copy of RecipesPagination /// with the given fields replaced by the non-null parameter values. diff --git a/lib/features/feed/models/recipes_pagination_model.g.dart b/lib/features/feed/models/recipes_pagination_model.g.dart index 109f39d5..ab5e7326 100644 --- a/lib/features/feed/models/recipes_pagination_model.g.dart +++ b/lib/features/feed/models/recipes_pagination_model.g.dart @@ -10,7 +10,7 @@ _$RecipesPaginationImpl _$$RecipesPaginationImplFromJson( Map json) => _$RecipesPaginationImpl( recipesList: (json['data'] as List) - .map((e) => Recipes.fromJson(e as Map)) + .map((e) => Recipe.fromJson(e as Map)) .toList(), ); 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 de59de7e..1770e23d 100644 --- a/lib/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart +++ b/lib/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart @@ -110,7 +110,7 @@ class DietsCubit extends Cubit { } catch (e, st) { log(e.toString(), stackTrace: st); emit( - state.copyWith(isLoading: false), + state.copyWith(isLoading: false, errorMessage: e.toString()), ); } } @@ -144,13 +144,15 @@ class DietsCubit extends Cubit { ); } } catch (e) { - emit(state.copyWith(isLoading: false)); + emit( + state.copyWith(isLoading: false, errorMessage: e.toString()), + ); } } } Future likeDiet(int id, int index) async { - final currentDiets = state.dietsList; + final currentDiets = state.dietsList; final dietsForModification = [...currentDiets]; final currentDiet = currentDiets[index]; if (currentDiet.wasLikedByMe) { diff --git a/lib/features/feed/presentation/bloc/recipes_cubit/recipes_cubit.dart b/lib/features/feed/presentation/bloc/recipes_cubit/recipes_cubit.dart index ad9c01ad..0144fc00 100644 --- a/lib/features/feed/presentation/bloc/recipes_cubit/recipes_cubit.dart +++ b/lib/features/feed/presentation/bloc/recipes_cubit/recipes_cubit.dart @@ -1,23 +1,44 @@ +import 'dart:async'; + +import 'package:easy_debounce/easy_debounce.dart'; +import 'package:eatvio_app/core/utils/toasts.dart'; +import 'package:eatvio_app/features/diets/repositories/diet_repository.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes_state.dart'; import 'package:eatvio_app/features/feed/repositories/feed_repository.dart'; -import 'package:eatvio_app/features/recipes/models/recipe_tag_model.dart'; -import 'package:eatvio_app/features/recipes/reporitories/recipe_repository.dart'; +import 'package:eatvio_app/features/create_recipe/models/recipe_tag_model.dart'; +import 'package:eatvio_app/features/create_recipe/reporitories/recipe_repository.dart'; +import 'package:eatvio_app/features/recipe/repositories/about_recipe_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'dart:developer'; class RecipesCubit extends Cubit { final FeedRepository feedRepository; final RecipeRepository recipeRepository; + final AboutRecipeRepository aboutRecipeRepository; Set _selectedTags = {}; int currentPage = 1; late int lastPage; bool isLoading = false; String? _query; + late final StreamSubscription likesStateSubscription; + late final StreamSubscription favoriteStateSubscription; + late final StreamSubscription commentsStateSubscription; RecipesCubit({ required this.feedRepository, required this.recipeRepository, - }) : super(const RecipesState.loading()); + required this.aboutRecipeRepository, + }) : super(RecipesState.initial()) { + likesStateSubscription = aboutRecipeRepository.recipeLikeState.listen((likesState) { + _changeRecipeLikeState(likesState); + }); + favoriteStateSubscription = aboutRecipeRepository.recipeFavoriteState.listen((favoriteState) { + _changeRecipeFavoriteState(favoriteState); + }); + commentsStateSubscription = aboutRecipeRepository.recipeCommentsState.listen((commentsState) { + _changeRecipeCommentsState(commentsState); + }); + } loadRecipesList({ String? query, @@ -28,9 +49,10 @@ class RecipesCubit extends Cubit { tags: _selectedTags.toList(), query: query, ); + final recipesTags = (await recipeRepository.getRecipesTags()); emit( - RecipesState.loaded( + state.copyWith( recipesList: recipesList, recipesTags: recipesTags, selectedTags: _selectedTags, @@ -41,7 +63,7 @@ class RecipesCubit extends Cubit { lastPage = currentPage; } catch (e, st) { log(e.toString(), stackTrace: st); - emit(RecipesState.error(errorMessage: e.toString())); + emit(state.copyWith(errorMessage: e.toString())); } } @@ -49,7 +71,7 @@ class RecipesCubit extends Cubit { if (currentPage < lastPage && !isLoading) { isLoading = true; - emit((state as RecipesStateLoaded).copyWith(isLoading: isLoading)); + emit(state.copyWith(isLoading: isLoading)); try { if (currentPage < lastPage) { @@ -61,9 +83,9 @@ class RecipesCubit extends Cubit { ); isLoading = false; emit( - (state as RecipesStateLoaded).copyWith( + state.copyWith( recipesList: [ - ...(state as RecipesStateLoaded).recipesList, + ...state.recipesList, ...recipesList, ], isLoading: isLoading, @@ -72,7 +94,7 @@ class RecipesCubit extends Cubit { } lastPage = currentPage; } catch (e) { - emit(RecipesState.error(errorMessage: e.toString())); + emit(state.copyWith(errorMessage: e.toString())); } } } @@ -80,14 +102,202 @@ class RecipesCubit extends Cubit { updateTags(RecipeTag tag) { _selectedTags.contains(tag.id) ? _selectedTags.remove(tag.id) : _selectedTags.add(tag.id); emit( - (state as RecipesStateLoaded).copyWith( + state.copyWith( selectedTags: _selectedTags, selectedTagsCounter: _selectedTags.length, ), ); } - likeRecipe(String slug) async => await feedRepository.likeRecipe(slug: slug); + Future addRecipeToFavorite(String slug, int index) async { + final currentRecipes = state.recipesList; + final recipesForModification = [...currentRecipes]; + final currentRecipe = currentRecipes[index]; - unlikeRecipe(String slug) async => await feedRepository.unlikeRecipe(slug: slug); + recipesForModification[index] = currentRecipe.copyWith( + isFavorite: true, + ); + emit( + state.copyWith( + recipesList: [ + ...recipesForModification, + ], + isLoading: isLoading, + errorMessage: null, + ), + ); + EasyDebounce.debounce( + 'add_or_remove_plan_from_favorite', + Duration(milliseconds: 200), + () async { + try { + await aboutRecipeRepository.addToFavorite(slug); + } catch (e) { + log(e.toString()); + showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время'); + emit( + state.copyWith( + recipesList: [ + ...currentRecipes, + ], + isLoading: isLoading, + errorMessage: null, + ), + ); + } + }, + ); + } + + Future removeRecipeFromFavorite(String slug, int index) async { + final currentRecipes = state.recipesList; + final recipesForModification = [...currentRecipes]; + final currentRecipe = currentRecipes[index]; + + recipesForModification[index] = currentRecipe.copyWith( + isFavorite: false, + ); + emit( + state.copyWith( + recipesList: [ + ...recipesForModification, + ], + isLoading: isLoading, + errorMessage: null, + ), + ); + EasyDebounce.debounce( + 'add_or_remove_plan_from_favorite', + Duration(milliseconds: 200), + () async { + try { + await aboutRecipeRepository.removeFromFavorite(slug); + } catch (e) { + log(e.toString()); + showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время'); + emit( + state.copyWith( + recipesList: [ + ...currentRecipes, + ], + isLoading: isLoading, + errorMessage: null, + ), + ); + } + }, + ); + } + + Future likeRecipe(String slug, int index) async { + final currentRecipes = state.recipesList; + final recipesForModification = [...currentRecipes]; + final currentRecipe = currentRecipes[index]; + if (currentRecipe.wasLikedByMe) { + recipesForModification[index] = currentRecipe.copyWith( + likesCount: currentRecipe.likesCount - 1, + wasLikedByMe: false, + ); + emit( + state.copyWith( + recipesList: [ + ...recipesForModification, + ], + isLoading: isLoading, + errorMessage: null, + ), + ); + } else { + recipesForModification[index] = currentRecipe.copyWith( + likesCount: currentRecipe.likesCount + 1, + wasLikedByMe: true, + ); + emit( + state.copyWith( + recipesList: [ + ...recipesForModification, + ], + isLoading: isLoading, + errorMessage: null, + ), + ); + } + EasyDebounce.debounce( + 'like_plan', + Duration(milliseconds: 200), + () async { + try { + await aboutRecipeRepository.likeRecipe(slug); + } catch (e) { + log(e.toString()); + emit( + state.copyWith( + recipesList: [ + ...currentRecipes, + ], + isLoading: isLoading, + ), + ); + } + }, + ); + } + + void _changeRecipeLikeState(LikesState likesState) { + final currentRecipes = state.recipesList; + final recipesForModification = [...currentRecipes]; + final currentRecipe = currentRecipes[likesState.index]; + + recipesForModification[likesState.index] = currentRecipe.copyWith( + likesCount: likesState.totalLikes, + wasLikedByMe: likesState.wasLikedByMe, + ); + emit( + state.copyWith( + recipesList: [ + ...recipesForModification, + ], + isLoading: isLoading, + errorMessage: null, + ), + ); + } + + void _changeRecipeFavoriteState(FavoriteState favoriteState) { + final currentRecipes = state.recipesList; + final recipesForModification = [...currentRecipes]; + final currentRecipe = currentRecipes[favoriteState.index]; + + recipesForModification[favoriteState.index] = currentRecipe.copyWith( + isFavorite: favoriteState.isFavorite, + ); + emit( + state.copyWith( + recipesList: [ + ...recipesForModification, + ], + isLoading: isLoading, + errorMessage: null, + ), + ); + } + + void _changeRecipeCommentsState(CommentsState commentsState) { + final currentRecipes = state.recipesList; + final recipesForModification = [...currentRecipes]; + final currentRecipe = currentRecipes[commentsState.index]; + + recipesForModification[commentsState.index] = currentRecipe.copyWith( + commentsCount: commentsState.totalComments, + ); + emit( + state.copyWith( + recipesList: [ + ...recipesForModification, + ], + isLoading: isLoading, + errorMessage: null, + ), + ); + } } diff --git a/lib/features/feed/presentation/bloc/recipes_cubit/recipes_state.dart b/lib/features/feed/presentation/bloc/recipes_cubit/recipes_state.dart index dc6dcb66..74db94f7 100644 --- a/lib/features/feed/presentation/bloc/recipes_cubit/recipes_state.dart +++ b/lib/features/feed/presentation/bloc/recipes_cubit/recipes_state.dart @@ -1,24 +1,26 @@ import 'package:eatvio_app/features/feed/models/recipes_model.dart'; -import 'package:eatvio_app/features/recipes/models/recipe_tag_model.dart'; +import 'package:eatvio_app/features/create_recipe/models/recipe_tag_model.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; part 'recipes_state.freezed.dart'; @freezed abstract class RecipesState with _$RecipesState { - const factory RecipesState() = _RecipesState; - - const factory RecipesState.loading() = RecipesStateLoading; - - const factory RecipesState.loaded({ - required List recipesList, + const factory RecipesState({ + required List recipesList, required List recipesTags, required Set selectedTags, required int selectedTagsCounter, required bool isLoading, - }) = RecipesStateLoaded; + String? errorMessage, + }) = _RecipesState; - const factory RecipesState.error({ - required String errorMessage, - }) = RecipesStateError; + factory RecipesState.initial() => RecipesState( + recipesList: [], + recipesTags: [], + selectedTags: {}, + selectedTagsCounter: 0, + isLoading: true, + errorMessage: null, + ); } diff --git a/lib/features/feed/presentation/bloc/recipes_cubit/recipes_state.freezed.dart b/lib/features/feed/presentation/bloc/recipes_cubit/recipes_state.freezed.dart index 34100adb..b5afc8b1 100644 --- a/lib/features/feed/presentation/bloc/recipes_cubit/recipes_state.freezed.dart +++ b/lib/features/feed/presentation/bloc/recipes_cubit/recipes_state.freezed.dart @@ -16,65 +16,17 @@ final _privateConstructorUsedError = UnsupportedError( /// @nodoc mixin _$RecipesState { - @optionalTypeArgs - TResult when( - TResult Function() $default, { - required TResult Function() loading, - required TResult Function( - List recipesList, - List recipesTags, - Set selectedTags, - int selectedTagsCounter, - bool isLoading) - loaded, - required TResult Function(String errorMessage) error, - }) => - throw _privateConstructorUsedError; - @optionalTypeArgs - TResult? whenOrNull( - TResult? Function()? $default, { - TResult? Function()? loading, - TResult? Function(List recipesList, List recipesTags, - Set selectedTags, int selectedTagsCounter, bool isLoading)? - loaded, - TResult? Function(String errorMessage)? error, - }) => - throw _privateConstructorUsedError; - @optionalTypeArgs - TResult maybeWhen( - TResult Function()? $default, { - TResult Function()? loading, - TResult Function(List recipesList, List recipesTags, - Set selectedTags, int selectedTagsCounter, bool isLoading)? - loaded, - TResult Function(String errorMessage)? error, - required TResult orElse(), - }) => - throw _privateConstructorUsedError; - @optionalTypeArgs - TResult map( - TResult Function(_RecipesState value) $default, { - required TResult Function(RecipesStateLoading value) loading, - required TResult Function(RecipesStateLoaded value) loaded, - required TResult Function(RecipesStateError value) error, - }) => - throw _privateConstructorUsedError; - @optionalTypeArgs - TResult? mapOrNull( - TResult? Function(_RecipesState value)? $default, { - TResult? Function(RecipesStateLoading value)? loading, - TResult? Function(RecipesStateLoaded value)? loaded, - TResult? Function(RecipesStateError value)? error, - }) => - throw _privateConstructorUsedError; - @optionalTypeArgs - TResult maybeMap( - TResult Function(_RecipesState value)? $default, { - TResult Function(RecipesStateLoading value)? loading, - TResult Function(RecipesStateLoaded value)? loaded, - TResult Function(RecipesStateError value)? error, - required TResult orElse(), - }) => + List get recipesList => throw _privateConstructorUsedError; + List get recipesTags => throw _privateConstructorUsedError; + Set get selectedTags => throw _privateConstructorUsedError; + int get selectedTagsCounter => throw _privateConstructorUsedError; + bool get isLoading => throw _privateConstructorUsedError; + String? get errorMessage => throw _privateConstructorUsedError; + + /// Create a copy of RecipesState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $RecipesStateCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -83,6 +35,14 @@ abstract class $RecipesStateCopyWith<$Res> { factory $RecipesStateCopyWith( RecipesState value, $Res Function(RecipesState) then) = _$RecipesStateCopyWithImpl<$Res, RecipesState>; + @useResult + $Res call( + {List recipesList, + List recipesTags, + Set selectedTags, + int selectedTagsCounter, + bool isLoading, + String? errorMessage}); } /// @nodoc @@ -97,13 +57,60 @@ class _$RecipesStateCopyWithImpl<$Res, $Val extends RecipesState> /// Create a copy of RecipesState /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? recipesList = null, + Object? recipesTags = null, + Object? selectedTags = null, + Object? selectedTagsCounter = null, + Object? isLoading = null, + Object? errorMessage = freezed, + }) { + return _then(_value.copyWith( + recipesList: null == recipesList + ? _value.recipesList + : recipesList // ignore: cast_nullable_to_non_nullable + as List, + recipesTags: null == recipesTags + ? _value.recipesTags + : recipesTags // ignore: cast_nullable_to_non_nullable + as List, + selectedTags: null == selectedTags + ? _value.selectedTags + : selectedTags // ignore: cast_nullable_to_non_nullable + as Set, + selectedTagsCounter: null == selectedTagsCounter + ? _value.selectedTagsCounter + : selectedTagsCounter // ignore: cast_nullable_to_non_nullable + as int, + isLoading: null == isLoading + ? _value.isLoading + : isLoading // ignore: cast_nullable_to_non_nullable + as bool, + errorMessage: freezed == errorMessage + ? _value.errorMessage + : errorMessage // ignore: cast_nullable_to_non_nullable + as String?, + ) as $Val); + } } /// @nodoc -abstract class _$$RecipesStateImplCopyWith<$Res> { +abstract class _$$RecipesStateImplCopyWith<$Res> + implements $RecipesStateCopyWith<$Res> { factory _$$RecipesStateImplCopyWith( _$RecipesStateImpl value, $Res Function(_$RecipesStateImpl) then) = __$$RecipesStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {List recipesList, + List recipesTags, + Set selectedTags, + int selectedTagsCounter, + bool isLoading, + String? errorMessage}); } /// @nodoc @@ -116,268 +123,6 @@ class __$$RecipesStateImplCopyWithImpl<$Res> /// Create a copy of RecipesState /// with the given fields replaced by the non-null parameter values. -} - -/// @nodoc - -class _$RecipesStateImpl implements _RecipesState { - const _$RecipesStateImpl(); - - @override - String toString() { - return 'RecipesState()'; - } - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && other is _$RecipesStateImpl); - } - - @override - int get hashCode => runtimeType.hashCode; - - @override - @optionalTypeArgs - TResult when( - TResult Function() $default, { - required TResult Function() loading, - required TResult Function( - List recipesList, - List recipesTags, - Set selectedTags, - int selectedTagsCounter, - bool isLoading) - loaded, - required TResult Function(String errorMessage) error, - }) { - return $default(); - } - - @override - @optionalTypeArgs - TResult? whenOrNull( - TResult? Function()? $default, { - TResult? Function()? loading, - TResult? Function(List recipesList, List recipesTags, - Set selectedTags, int selectedTagsCounter, bool isLoading)? - loaded, - TResult? Function(String errorMessage)? error, - }) { - return $default?.call(); - } - - @override - @optionalTypeArgs - TResult maybeWhen( - TResult Function()? $default, { - TResult Function()? loading, - TResult Function(List recipesList, List recipesTags, - Set selectedTags, int selectedTagsCounter, bool isLoading)? - loaded, - TResult Function(String errorMessage)? error, - required TResult orElse(), - }) { - if ($default != null) { - return $default(); - } - return orElse(); - } - - @override - @optionalTypeArgs - TResult map( - TResult Function(_RecipesState value) $default, { - required TResult Function(RecipesStateLoading value) loading, - required TResult Function(RecipesStateLoaded value) loaded, - required TResult Function(RecipesStateError value) error, - }) { - return $default(this); - } - - @override - @optionalTypeArgs - TResult? mapOrNull( - TResult? Function(_RecipesState value)? $default, { - TResult? Function(RecipesStateLoading value)? loading, - TResult? Function(RecipesStateLoaded value)? loaded, - TResult? Function(RecipesStateError value)? error, - }) { - return $default?.call(this); - } - - @override - @optionalTypeArgs - TResult maybeMap( - TResult Function(_RecipesState value)? $default, { - TResult Function(RecipesStateLoading value)? loading, - TResult Function(RecipesStateLoaded value)? loaded, - TResult Function(RecipesStateError value)? error, - required TResult orElse(), - }) { - if ($default != null) { - return $default(this); - } - return orElse(); - } -} - -abstract class _RecipesState implements RecipesState { - const factory _RecipesState() = _$RecipesStateImpl; -} - -/// @nodoc -abstract class _$$RecipesStateLoadingImplCopyWith<$Res> { - factory _$$RecipesStateLoadingImplCopyWith(_$RecipesStateLoadingImpl value, - $Res Function(_$RecipesStateLoadingImpl) then) = - __$$RecipesStateLoadingImplCopyWithImpl<$Res>; -} - -/// @nodoc -class __$$RecipesStateLoadingImplCopyWithImpl<$Res> - extends _$RecipesStateCopyWithImpl<$Res, _$RecipesStateLoadingImpl> - implements _$$RecipesStateLoadingImplCopyWith<$Res> { - __$$RecipesStateLoadingImplCopyWithImpl(_$RecipesStateLoadingImpl _value, - $Res Function(_$RecipesStateLoadingImpl) _then) - : super(_value, _then); - - /// Create a copy of RecipesState - /// with the given fields replaced by the non-null parameter values. -} - -/// @nodoc - -class _$RecipesStateLoadingImpl implements RecipesStateLoading { - const _$RecipesStateLoadingImpl(); - - @override - String toString() { - return 'RecipesState.loading()'; - } - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$RecipesStateLoadingImpl); - } - - @override - int get hashCode => runtimeType.hashCode; - - @override - @optionalTypeArgs - TResult when( - TResult Function() $default, { - required TResult Function() loading, - required TResult Function( - List recipesList, - List recipesTags, - Set selectedTags, - int selectedTagsCounter, - bool isLoading) - loaded, - required TResult Function(String errorMessage) error, - }) { - return loading(); - } - - @override - @optionalTypeArgs - TResult? whenOrNull( - TResult? Function()? $default, { - TResult? Function()? loading, - TResult? Function(List recipesList, List recipesTags, - Set selectedTags, int selectedTagsCounter, bool isLoading)? - loaded, - TResult? Function(String errorMessage)? error, - }) { - return loading?.call(); - } - - @override - @optionalTypeArgs - TResult maybeWhen( - TResult Function()? $default, { - TResult Function()? loading, - TResult Function(List recipesList, List recipesTags, - Set selectedTags, int selectedTagsCounter, bool isLoading)? - loaded, - TResult Function(String errorMessage)? error, - required TResult orElse(), - }) { - if (loading != null) { - return loading(); - } - return orElse(); - } - - @override - @optionalTypeArgs - TResult map( - TResult Function(_RecipesState value) $default, { - required TResult Function(RecipesStateLoading value) loading, - required TResult Function(RecipesStateLoaded value) loaded, - required TResult Function(RecipesStateError value) error, - }) { - return loading(this); - } - - @override - @optionalTypeArgs - TResult? mapOrNull( - TResult? Function(_RecipesState value)? $default, { - TResult? Function(RecipesStateLoading value)? loading, - TResult? Function(RecipesStateLoaded value)? loaded, - TResult? Function(RecipesStateError value)? error, - }) { - return loading?.call(this); - } - - @override - @optionalTypeArgs - TResult maybeMap( - TResult Function(_RecipesState value)? $default, { - TResult Function(RecipesStateLoading value)? loading, - TResult Function(RecipesStateLoaded value)? loaded, - TResult Function(RecipesStateError value)? error, - required TResult orElse(), - }) { - if (loading != null) { - return loading(this); - } - return orElse(); - } -} - -abstract class RecipesStateLoading implements RecipesState { - const factory RecipesStateLoading() = _$RecipesStateLoadingImpl; -} - -/// @nodoc -abstract class _$$RecipesStateLoadedImplCopyWith<$Res> { - factory _$$RecipesStateLoadedImplCopyWith(_$RecipesStateLoadedImpl value, - $Res Function(_$RecipesStateLoadedImpl) then) = - __$$RecipesStateLoadedImplCopyWithImpl<$Res>; - @useResult - $Res call( - {List recipesList, - List recipesTags, - Set selectedTags, - int selectedTagsCounter, - bool isLoading}); -} - -/// @nodoc -class __$$RecipesStateLoadedImplCopyWithImpl<$Res> - extends _$RecipesStateCopyWithImpl<$Res, _$RecipesStateLoadedImpl> - implements _$$RecipesStateLoadedImplCopyWith<$Res> { - __$$RecipesStateLoadedImplCopyWithImpl(_$RecipesStateLoadedImpl _value, - $Res Function(_$RecipesStateLoadedImpl) _then) - : super(_value, _then); - - /// Create a copy of RecipesState - /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -386,12 +131,13 @@ class __$$RecipesStateLoadedImplCopyWithImpl<$Res> Object? selectedTags = null, Object? selectedTagsCounter = null, Object? isLoading = null, + Object? errorMessage = freezed, }) { - return _then(_$RecipesStateLoadedImpl( + return _then(_$RecipesStateImpl( recipesList: null == recipesList ? _value._recipesList : recipesList // ignore: cast_nullable_to_non_nullable - as List, + as List, recipesTags: null == recipesTags ? _value._recipesTags : recipesTags // ignore: cast_nullable_to_non_nullable @@ -408,26 +154,31 @@ class __$$RecipesStateLoadedImplCopyWithImpl<$Res> ? _value.isLoading : isLoading // ignore: cast_nullable_to_non_nullable as bool, + errorMessage: freezed == errorMessage + ? _value.errorMessage + : errorMessage // ignore: cast_nullable_to_non_nullable + as String?, )); } } /// @nodoc -class _$RecipesStateLoadedImpl implements RecipesStateLoaded { - const _$RecipesStateLoadedImpl( - {required final List recipesList, +class _$RecipesStateImpl implements _RecipesState { + const _$RecipesStateImpl( + {required final List recipesList, required final List recipesTags, required final Set selectedTags, required this.selectedTagsCounter, - required this.isLoading}) + required this.isLoading, + this.errorMessage}) : _recipesList = recipesList, _recipesTags = recipesTags, _selectedTags = selectedTags; - final List _recipesList; + final List _recipesList; @override - List get recipesList { + List get recipesList { if (_recipesList is EqualUnmodifiableListView) return _recipesList; // ignore: implicit_dynamic_type return EqualUnmodifiableListView(_recipesList); @@ -453,17 +204,19 @@ class _$RecipesStateLoadedImpl implements RecipesStateLoaded { final int selectedTagsCounter; @override final bool isLoading; + @override + final String? errorMessage; @override String toString() { - return 'RecipesState.loaded(recipesList: $recipesList, recipesTags: $recipesTags, selectedTags: $selectedTags, selectedTagsCounter: $selectedTagsCounter, isLoading: $isLoading)'; + return 'RecipesState(recipesList: $recipesList, recipesTags: $recipesTags, selectedTags: $selectedTags, selectedTagsCounter: $selectedTagsCounter, isLoading: $isLoading, errorMessage: $errorMessage)'; } @override bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && - other is _$RecipesStateLoadedImpl && + other is _$RecipesStateImpl && const DeepCollectionEquality() .equals(other._recipesList, _recipesList) && const DeepCollectionEquality() @@ -473,7 +226,9 @@ class _$RecipesStateLoadedImpl implements RecipesStateLoaded { (identical(other.selectedTagsCounter, selectedTagsCounter) || other.selectedTagsCounter == selectedTagsCounter) && (identical(other.isLoading, isLoading) || - other.isLoading == isLoading)); + other.isLoading == isLoading) && + (identical(other.errorMessage, errorMessage) || + other.errorMessage == errorMessage)); } @override @@ -483,287 +238,44 @@ class _$RecipesStateLoadedImpl implements RecipesStateLoaded { const DeepCollectionEquality().hash(_recipesTags), const DeepCollectionEquality().hash(_selectedTags), selectedTagsCounter, - isLoading); + isLoading, + errorMessage); /// Create a copy of RecipesState /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') - _$$RecipesStateLoadedImplCopyWith<_$RecipesStateLoadedImpl> get copyWith => - __$$RecipesStateLoadedImplCopyWithImpl<_$RecipesStateLoadedImpl>( - this, _$identity); - - @override - @optionalTypeArgs - TResult when( - TResult Function() $default, { - required TResult Function() loading, - required TResult Function( - List recipesList, - List recipesTags, - Set selectedTags, - int selectedTagsCounter, - bool isLoading) - loaded, - required TResult Function(String errorMessage) error, - }) { - return loaded( - recipesList, recipesTags, selectedTags, selectedTagsCounter, isLoading); - } - - @override - @optionalTypeArgs - TResult? whenOrNull( - TResult? Function()? $default, { - TResult? Function()? loading, - TResult? Function(List recipesList, List recipesTags, - Set selectedTags, int selectedTagsCounter, bool isLoading)? - loaded, - TResult? Function(String errorMessage)? error, - }) { - return loaded?.call( - recipesList, recipesTags, selectedTags, selectedTagsCounter, isLoading); - } - - @override - @optionalTypeArgs - TResult maybeWhen( - TResult Function()? $default, { - TResult Function()? loading, - TResult Function(List recipesList, List recipesTags, - Set selectedTags, int selectedTagsCounter, bool isLoading)? - loaded, - TResult Function(String errorMessage)? error, - required TResult orElse(), - }) { - if (loaded != null) { - return loaded(recipesList, recipesTags, selectedTags, selectedTagsCounter, - isLoading); - } - return orElse(); - } - - @override - @optionalTypeArgs - TResult map( - TResult Function(_RecipesState value) $default, { - required TResult Function(RecipesStateLoading value) loading, - required TResult Function(RecipesStateLoaded value) loaded, - required TResult Function(RecipesStateError value) error, - }) { - return loaded(this); - } - - @override - @optionalTypeArgs - TResult? mapOrNull( - TResult? Function(_RecipesState value)? $default, { - TResult? Function(RecipesStateLoading value)? loading, - TResult? Function(RecipesStateLoaded value)? loaded, - TResult? Function(RecipesStateError value)? error, - }) { - return loaded?.call(this); - } - - @override - @optionalTypeArgs - TResult maybeMap( - TResult Function(_RecipesState value)? $default, { - TResult Function(RecipesStateLoading value)? loading, - TResult Function(RecipesStateLoaded value)? loaded, - TResult Function(RecipesStateError value)? error, - required TResult orElse(), - }) { - if (loaded != null) { - return loaded(this); - } - return orElse(); - } + _$$RecipesStateImplCopyWith<_$RecipesStateImpl> get copyWith => + __$$RecipesStateImplCopyWithImpl<_$RecipesStateImpl>(this, _$identity); } -abstract class RecipesStateLoaded implements RecipesState { - const factory RecipesStateLoaded( - {required final List recipesList, +abstract class _RecipesState implements RecipesState { + const factory _RecipesState( + {required final List recipesList, required final List recipesTags, required final Set selectedTags, required final int selectedTagsCounter, - required final bool isLoading}) = _$RecipesStateLoadedImpl; + required final bool isLoading, + final String? errorMessage}) = _$RecipesStateImpl; - List get recipesList; + @override + List get recipesList; + @override List get recipesTags; + @override Set get selectedTags; + @override int get selectedTagsCounter; + @override bool get isLoading; + @override + String? get errorMessage; /// Create a copy of RecipesState /// with the given fields replaced by the non-null parameter values. + @override @JsonKey(includeFromJson: false, includeToJson: false) - _$$RecipesStateLoadedImplCopyWith<_$RecipesStateLoadedImpl> get copyWith => - throw _privateConstructorUsedError; -} - -/// @nodoc -abstract class _$$RecipesStateErrorImplCopyWith<$Res> { - factory _$$RecipesStateErrorImplCopyWith(_$RecipesStateErrorImpl value, - $Res Function(_$RecipesStateErrorImpl) then) = - __$$RecipesStateErrorImplCopyWithImpl<$Res>; - @useResult - $Res call({String errorMessage}); -} - -/// @nodoc -class __$$RecipesStateErrorImplCopyWithImpl<$Res> - extends _$RecipesStateCopyWithImpl<$Res, _$RecipesStateErrorImpl> - implements _$$RecipesStateErrorImplCopyWith<$Res> { - __$$RecipesStateErrorImplCopyWithImpl(_$RecipesStateErrorImpl _value, - $Res Function(_$RecipesStateErrorImpl) _then) - : super(_value, _then); - - /// Create a copy of RecipesState - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? errorMessage = null, - }) { - return _then(_$RecipesStateErrorImpl( - errorMessage: null == errorMessage - ? _value.errorMessage - : errorMessage // ignore: cast_nullable_to_non_nullable - as String, - )); - } -} - -/// @nodoc - -class _$RecipesStateErrorImpl implements RecipesStateError { - const _$RecipesStateErrorImpl({required this.errorMessage}); - - @override - final String errorMessage; - - @override - String toString() { - return 'RecipesState.error(errorMessage: $errorMessage)'; - } - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$RecipesStateErrorImpl && - (identical(other.errorMessage, errorMessage) || - other.errorMessage == errorMessage)); - } - - @override - int get hashCode => Object.hash(runtimeType, errorMessage); - - /// Create a copy of RecipesState - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - @override - @pragma('vm:prefer-inline') - _$$RecipesStateErrorImplCopyWith<_$RecipesStateErrorImpl> get copyWith => - __$$RecipesStateErrorImplCopyWithImpl<_$RecipesStateErrorImpl>( - this, _$identity); - - @override - @optionalTypeArgs - TResult when( - TResult Function() $default, { - required TResult Function() loading, - required TResult Function( - List recipesList, - List recipesTags, - Set selectedTags, - int selectedTagsCounter, - bool isLoading) - loaded, - required TResult Function(String errorMessage) error, - }) { - return error(errorMessage); - } - - @override - @optionalTypeArgs - TResult? whenOrNull( - TResult? Function()? $default, { - TResult? Function()? loading, - TResult? Function(List recipesList, List recipesTags, - Set selectedTags, int selectedTagsCounter, bool isLoading)? - loaded, - TResult? Function(String errorMessage)? error, - }) { - return error?.call(errorMessage); - } - - @override - @optionalTypeArgs - TResult maybeWhen( - TResult Function()? $default, { - TResult Function()? loading, - TResult Function(List recipesList, List recipesTags, - Set selectedTags, int selectedTagsCounter, bool isLoading)? - loaded, - TResult Function(String errorMessage)? error, - required TResult orElse(), - }) { - if (error != null) { - return error(errorMessage); - } - return orElse(); - } - - @override - @optionalTypeArgs - TResult map( - TResult Function(_RecipesState value) $default, { - required TResult Function(RecipesStateLoading value) loading, - required TResult Function(RecipesStateLoaded value) loaded, - required TResult Function(RecipesStateError value) error, - }) { - return error(this); - } - - @override - @optionalTypeArgs - TResult? mapOrNull( - TResult? Function(_RecipesState value)? $default, { - TResult? Function(RecipesStateLoading value)? loading, - TResult? Function(RecipesStateLoaded value)? loaded, - TResult? Function(RecipesStateError value)? error, - }) { - return error?.call(this); - } - - @override - @optionalTypeArgs - TResult maybeMap( - TResult Function(_RecipesState value)? $default, { - TResult Function(RecipesStateLoading value)? loading, - TResult Function(RecipesStateLoaded value)? loaded, - TResult Function(RecipesStateError value)? error, - required TResult orElse(), - }) { - if (error != null) { - return error(this); - } - return orElse(); - } -} - -abstract class RecipesStateError implements RecipesState { - const factory RecipesStateError({required final String errorMessage}) = - _$RecipesStateErrorImpl; - - String get errorMessage; - - /// Create a copy of RecipesState - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - _$$RecipesStateErrorImplCopyWith<_$RecipesStateErrorImpl> get copyWith => + _$$RecipesStateImplCopyWith<_$RecipesStateImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/features/feed/presentation/pages/feed_screen.dart b/lib/features/feed/presentation/pages/feed_screen.dart index 4527c0af..faf90370 100644 --- a/lib/features/feed/presentation/pages/feed_screen.dart +++ b/lib/features/feed/presentation/pages/feed_screen.dart @@ -1,6 +1,5 @@ import 'package:easy_debounce/easy_debounce.dart'; import 'package:eatvio_app/core/widgets/add_item_button.dart'; -import 'package:eatvio_app/core/widgets/bottom_sheet_with_blur.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/features/create_plan/presentation/create_plan_screen.dart'; @@ -15,16 +14,14 @@ import 'package:eatvio_app/features/feed/presentation/widgets/chip_group.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/consultants.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/diets.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/plans.dart'; -import 'package:eatvio_app/features/feed/presentation/widgets/success_published_diet_bottom_sheet.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/tags_modal_widget.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_cubit.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; -import 'package:eatvio_app/features/recipes/presentation/pages/recipe_create_screen.dart'; +import 'package:eatvio_app/features/create_recipe/presentation/pages/recipe_create_screen.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; - import '../widgets/recipes.dart'; class FeedScreen extends StatelessWidget { @@ -263,16 +260,7 @@ class _FeedScreenContentState extends State { 2 => AddItemButton( onTap: state.profile?.roles.contains('consultant') ?? false ? () async { - final result = await GoRouter.of(context).pushNamed(CreateDietScreen.name); - if (result != null) { - showBottomSheetWithBlur( - userRootNavigator: true, - context: context, - child: SuccessPublishedDietBottomSheet( - title: result, - ), - ); - } + await GoRouter.of(context).pushNamed(CreateDietScreen.name); } : null, ), diff --git a/lib/features/feed/presentation/widgets/blog_posts.dart b/lib/features/feed/presentation/widgets/blog_posts.dart index f76e35ce..da5363fe 100644 --- a/lib/features/feed/presentation/widgets/blog_posts.dart +++ b/lib/features/feed/presentation/widgets/blog_posts.dart @@ -1,3 +1,4 @@ +import 'package:eatvio_app/core/widgets/data_error_widget.dart'; import 'package:eatvio_app/core/widgets/loader.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_state.dart'; @@ -31,14 +32,8 @@ class BlogPosts extends StatelessWidget { itemCount: state.isLoading ? state.feedBlogPostList.length + 1 : state.feedBlogPostList.length, ); } else if (state is FeedStateError) { - return Scaffold( - backgroundColor: Colors.grey.shade200, - body: Padding( - padding: const EdgeInsets.all(16), - child: Center( - child: Text(state.errorMessage), - ), - ), + return DataErrorWidget( + onReload: () => context.read().loadFeedBlogPostList(), ); } else { return Scaffold( @@ -50,4 +45,4 @@ class BlogPosts extends StatelessWidget { ), ); } -} \ No newline at end of file +} diff --git a/lib/features/feed/presentation/widgets/consultants.dart b/lib/features/feed/presentation/widgets/consultants.dart index 01c4e816..4e505e40 100644 --- a/lib/features/feed/presentation/widgets/consultants.dart +++ b/lib/features/feed/presentation/widgets/consultants.dart @@ -1,4 +1,5 @@ - import 'package:eatvio_app/core/widgets/loader.dart'; +import 'package:eatvio_app/core/widgets/data_error_widget.dart'; +import 'package:eatvio_app/core/widgets/loader.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/consultants_cubit/consultants_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/consultants_cubit/consultants_state.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/consultant_widget.dart'; @@ -12,7 +13,7 @@ class Consultants extends StatelessWidget { Widget build(BuildContext context) { return RefreshIndicator( onRefresh: () => context.read().getConsultants(), - child: BlocBuilder( + child: BlocBuilder( builder: (BuildContext context, ConsultantsState state) { if (state is ConsultantsStateLoaded) { return Padding( @@ -38,15 +39,7 @@ class Consultants extends StatelessWidget { ), ); } else if (state is ConsultantsStateError) { - return Scaffold( - backgroundColor: Colors.grey.shade200, - body: Padding( - padding: const EdgeInsets.all(16), - child: Center( - child: Text(state.errorMessage), - ), - ), - ); + return DataErrorWidget(onReload: () => context.read().getConsultants()); } else { return Scaffold( backgroundColor: Colors.grey.shade200, @@ -58,5 +51,3 @@ class Consultants extends StatelessWidget { ); } } - - \ No newline at end of file diff --git a/lib/features/feed/presentation/widgets/diets.dart b/lib/features/feed/presentation/widgets/diets.dart index b698065b..254a9567 100644 --- a/lib/features/feed/presentation/widgets/diets.dart +++ b/lib/features/feed/presentation/widgets/diets.dart @@ -1,3 +1,4 @@ +import 'package:eatvio_app/core/widgets/data_error_widget.dart'; import 'package:eatvio_app/core/widgets/loader.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'; @@ -22,15 +23,7 @@ class Diets extends StatelessWidget { body: const Loader(), ); } else if (state.errorMessage != null) { - return Scaffold( - backgroundColor: Colors.grey.shade200, - body: Padding( - padding: const EdgeInsets.all(16), - child: Center( - child: Text(state.errorMessage ?? ''), - ), - ), - ); + return DataErrorWidget(onReload: () => context.read().loadDiets()); } else { return Stack( children: [ diff --git a/lib/features/feed/presentation/widgets/diets_widget.dart b/lib/features/feed/presentation/widgets/diets_widget.dart index d42d7da3..46b93d18 100644 --- a/lib/features/feed/presentation/widgets/diets_widget.dart +++ b/lib/features/feed/presentation/widgets/diets_widget.dart @@ -1,5 +1,6 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/enums/product_type.dart'; import 'package:eatvio_app/core/widgets/bookmark_button.dart'; import 'package:eatvio_app/core/widgets/info_row.dart'; import 'package:eatvio_app/core/widgets/feed_icon_button.dart'; @@ -30,21 +31,27 @@ class DietsWidget extends StatefulWidget { class _DietsWidgetState extends State { @override Widget build(BuildContext context) { - return DecoratedBox( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - color: Colors.white, + return InkWell( + onTap: () => GoRouter.of(context).pushNamed( + 'aboutDietScreen', + extra: { + 'diet': widget.diet, + 'index': widget.index, + 'navigateToComments': false, + }, ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Stack( - children: [ - InkWell( - onTap: () => - GoRouter.of(context).pushNamed('aboutDietScreen', extra: {'diet': widget.diet, 'index': widget.index, 'navigateToComments': false}), - child: ClipRRect( + child: Ink( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: Colors.white, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Stack( + children: [ + ClipRRect( borderRadius: BorderRadius.vertical(top: Radius.circular(12)), child: CachedNetworkImage( imageUrl: baseUrl + '/storage/' + widget.diet.image, @@ -52,123 +59,124 @@ class _DietsWidgetState extends State { width: MediaQuery.of(context).size.width, ), ), - ), - Positioned( - right: 14, - child: BookmarkButton( - onTap: () async { - if (widget.diet.isFavorite) { - await context.read().removeDietFromFavorite(widget.diet.id, widget.index); - } else { - await context.read().addDietToFavorite(widget.diet.id, widget.index); - } - }, - color: appTheme.whiteA700, - isFavorite: widget.diet.isFavorite, - borderRadius: BorderRadius.vertical(bottom: Radius.circular(12)), - width: 40, - height: 44, - size: 19, - ), - ), - 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, - ), + Positioned( + right: 14, + child: BookmarkButton( + onTap: () async { + if (widget.diet.isFavorite) { + await context.read().removeDietFromFavorite(widget.diet.id, widget.index); + } else { + await context.read().addDietToFavorite(widget.diet.id, widget.index); + } }, + color: appTheme.whiteA700, + isFavorite: widget.diet.isFavorite, + borderRadius: BorderRadius.vertical(bottom: Radius.circular(12)), + width: 40, + height: 44, + size: 19, ), ), - ], - ], - ), - Padding( - padding: const EdgeInsets.only(left: 16, top: 12), - child: Row( - children: [ - FeedIconButton( - icon: widget.diet.wasLikedByMe ? Icons.favorite_rounded : Icons.favorite_border_rounded, - text: widget.diet.totalLikes.toString(), - iconColor: widget.diet.wasLikedByMe ? appTheme.blueA400 : null, - backgroundColor: widget.diet.wasLikedByMe ? appTheme.transparentBlue : null, - onTap: () async { - await context.read().likeDiet(widget.diet.id, widget.index); - }, - ), - SizedBox( - width: 12, - ), - FeedIconButton( - icon: Icons.chat_bubble_outline, - text: widget.diet.totalComments.toString(), - onTap: () => - GoRouter.of(context).pushNamed('aboutDietScreen', extra: {'diet': widget.diet, 'index': widget.index, 'navigateToComments': true}), - ), - SizedBox( - width: 12, - ), - FeedIconButton( - svgIcon: SvgPicture.asset('assets/icons/repost.svg'), - onTap: () async { - final box = context.findRenderObject() as RenderBox?; - final result = await Share.shareUri( - Uri.parse( - baseUrl + '/ru/diet/' + widget.diet.id.toString(), - ), - sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, - ); - switch (result.status) { - case ShareResultStatus.success: - case ShareResultStatus.dismissed: - case ShareResultStatus.unavailable: - } - }, - ), - Spacer(), - FeedIconButton( - svgIcon: SvgPicture.asset('assets/icons/eye2.svg'), - text: widget.diet.totalViews.toString(), - backgroundColor: Colors.transparent, + Positioned( + bottom: 16, + child: SizedBox( + width: MediaQuery.sizeOf(context).width, + child: InfoRow( + diet: widget.diet, + itemsBackgroundColor: appTheme.whiteA700, + disableDescriptionOfTypes: true, + type: ProductType.diet, + ), + ), ), ], ), - ), - SizedBox(height: 12), - ], + 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, + ), + }, + ), + ), + ], + ], + ), + Padding( + padding: const EdgeInsets.only(left: 16, top: 12), + child: Row( + children: [ + FeedIconButton( + icon: widget.diet.wasLikedByMe ? Icons.favorite_rounded : Icons.favorite_border_rounded, + text: widget.diet.totalLikes.toString(), + iconColor: widget.diet.wasLikedByMe ? appTheme.blueA400 : null, + backgroundColor: widget.diet.wasLikedByMe ? appTheme.transparentBlue : null, + onTap: () async { + await context.read().likeDiet(widget.diet.id, widget.index); + }, + ), + SizedBox( + width: 12, + ), + FeedIconButton( + icon: Icons.chat_bubble_outline, + text: widget.diet.totalComments.toString(), + onTap: () => + GoRouter.of(context).pushNamed('aboutDietScreen', extra: {'diet': widget.diet, 'index': widget.index, 'navigateToComments': true}), + ), + SizedBox( + width: 12, + ), + FeedIconButton( + svgIcon: SvgPicture.asset('assets/icons/repost.svg'), + onTap: () async { + final box = context.findRenderObject() as RenderBox?; + final result = await Share.shareUri( + Uri.parse( + baseUrl + '/ru/diet/' + widget.diet.id.toString(), + ), + sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, + ); + switch (result.status) { + case ShareResultStatus.success: + case ShareResultStatus.dismissed: + case ShareResultStatus.unavailable: + } + }, + ), + Spacer(), + FeedIconButton( + svgIcon: SvgPicture.asset('assets/icons/eye2.svg'), + text: widget.diet.totalViews.toString(), + backgroundColor: Colors.transparent, + ), + ], + ), + ), + SizedBox(height: 12), + ], + ), ), ); } diff --git a/lib/features/feed/presentation/widgets/plan_widget.dart b/lib/features/feed/presentation/widgets/plan_widget.dart index a77c9b3e..3166aa96 100644 --- a/lib/features/feed/presentation/widgets/plan_widget.dart +++ b/lib/features/feed/presentation/widgets/plan_widget.dart @@ -1,5 +1,6 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/enums/product_type.dart'; import 'package:eatvio_app/core/widgets/bookmark_button.dart'; import 'package:eatvio_app/core/widgets/info_row.dart'; import 'package:eatvio_app/core/widgets/feed_icon_button.dart'; @@ -33,21 +34,27 @@ class _PlanWidgetState extends State { @override Widget build(BuildContext context) { final s = S.of(context); - return DecoratedBox( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - color: Colors.white, + return InkWell( + onTap: () => GoRouter.of(context).pushNamed( + 'aboutPlanScreen', + extra: { + 'plan': widget.plan, + 'index': widget.index, + 'navigateToComments': false, + }, ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Stack( - children: [ - InkWell( - onTap: () => - GoRouter.of(context).pushNamed('aboutPlanScreen', extra: {'plan': widget.plan, 'index': widget.index, 'navigateToComments': false}), - child: ClipRRect( + child: Ink( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: Colors.white, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Stack( + children: [ + ClipRRect( borderRadius: BorderRadius.vertical(top: Radius.circular(12)), child: CachedNetworkImage( imageUrl: baseUrl + '/storage/' + widget.plan.image, @@ -55,176 +62,177 @@ class _PlanWidgetState extends State { width: MediaQuery.of(context).size.width, ), ), - ), - Positioned( - right: 14, - child: BookmarkButton( - onTap: () async { - if (widget.plan.isFavorite) { - await context.read().removePlanFromFavorite(widget.plan.slug, widget.index); - } else { - await context.read().addPlanToFavorite(widget.plan.slug, widget.index); - } - }, - color: appTheme.whiteA700, - isFavorite: widget.plan.isFavorite, - borderRadius: BorderRadius.vertical(bottom: Radius.circular(12)), - width: 40, - height: 44, - size: 19, - ), - ), - Positioned( - bottom: 16, - child: SizedBox( - width: MediaQuery.sizeOf(context).width, - child: InfoRow( - plan: widget.plan, - itemsBackgroundColor: appTheme.whiteA700, - disableDescriptionOfTypes: true, + Positioned( + right: 14, + child: BookmarkButton( + onTap: () async { + if (widget.plan.isFavorite) { + await context.read().removePlanFromFavorite(widget.plan.slug, widget.index); + } else { + await context.read().addPlanToFavorite(widget.plan.slug, widget.index); + } + }, + color: appTheme.whiteA700, + isFavorite: widget.plan.isFavorite, + borderRadius: BorderRadius.vertical(bottom: Radius.circular(12)), + width: 40, + height: 44, + size: 19, ), ), - ), - ], - ), - 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, + Positioned( + bottom: 16, + child: SizedBox( + width: MediaQuery.sizeOf(context).width, + child: InfoRow( + plan: widget.plan, + itemsBackgroundColor: appTheme.whiteA700, + disableDescriptionOfTypes: true, + type: ProductType.plan, + ), + ), ), + ], + ), + SizedBox(height: 12), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ 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, - ), + 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: 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( + icon: widget.plan.wasLikedByMe ? Icons.favorite_rounded : Icons.favorite_border_rounded, + text: widget.plan.totalLikes.toString(), + iconColor: widget.plan.wasLikedByMe ? appTheme.blueA400 : null, + backgroundColor: widget.plan.wasLikedByMe ? appTheme.transparentBlue : null, + onTap: () async { + await context.read().likePlan(widget.plan.slug, widget.index); }, ), - ), - ], - ], - ), - SizedBox(height: 16), - Padding( - 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, + SizedBox( + width: 12, ), - ), - ), - 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, + FeedIconButton( + icon: Icons.chat_bubble_outline, + text: widget.plan.totalComments.toString(), + onTap: () => + GoRouter.of(context).pushNamed('aboutPlanScreen', extra: {'plan': widget.plan, 'index': widget.index, 'navigateToComments': true}), + ), + SizedBox( + width: 12, + ), + FeedIconButton( + svgIcon: SvgPicture.asset('assets/icons/repost.svg'), + onTap: () async { + final box = context.findRenderObject() as RenderBox?; + final result = await Share.shareUri( + Uri.parse( + baseUrl + '/ru/diet/' + widget.plan.id.toString(), ), - ), - ], + sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, + ); + switch (result.status) { + case ShareResultStatus.success: + case ShareResultStatus.dismissed: + case ShareResultStatus.unavailable: + } + }, ), Spacer(), - Text( - '${widget.plan.packetsCount == 1 ? widget.plan.packet1Options?.price : '${s.from} ${widget.plan.minPriceOfOptions.toStringAsFixed(0)}'}₽', - style: TextStyles.headingXLSemibold.copyWith(color: appTheme.greenA700), + FeedIconButton( + svgIcon: SvgPicture.asset('assets/icons/eye2.svg'), + text: widget.plan.totalViews.toString(), + backgroundColor: Colors.transparent, ), - 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( - icon: widget.plan.wasLikedByMe ? Icons.favorite_rounded : Icons.favorite_border_rounded, - text: widget.plan.totalLikes.toString(), - iconColor: widget.plan.wasLikedByMe ? appTheme.blueA400 : null, - backgroundColor: widget.plan.wasLikedByMe ? appTheme.transparentBlue : null, - onTap: () async { - await context.read().likePlan(widget.plan.slug, widget.index); - }, - ), - SizedBox( - width: 12, - ), - FeedIconButton( - icon: Icons.chat_bubble_outline, - text: widget.plan.totalComments.toString(), - onTap: () => - GoRouter.of(context).pushNamed('aboutPlanScreen', extra: {'plan': widget.plan, 'index': widget.index, 'navigateToComments': true}), - ), - SizedBox( - width: 12, - ), - FeedIconButton( - svgIcon: SvgPicture.asset('assets/icons/repost.svg'), - onTap: () async { - final box = context.findRenderObject() as RenderBox?; - final result = await Share.shareUri( - Uri.parse( - baseUrl + '/ru/diet/' + widget.plan.id.toString(), - ), - sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, - ); - switch (result.status) { - case ShareResultStatus.success: - case ShareResultStatus.dismissed: - case ShareResultStatus.unavailable: - } - }, - ), - Spacer(), - FeedIconButton( - svgIcon: SvgPicture.asset('assets/icons/eye2.svg'), - text: widget.plan.totalViews.toString(), - backgroundColor: Colors.transparent, - ), - ], - ), - ), - SizedBox(height: 12), - ], + SizedBox(height: 12), + ], + ), ), ); } diff --git a/lib/features/feed/presentation/widgets/plans.dart b/lib/features/feed/presentation/widgets/plans.dart index 940e677b..cc3a1a63 100644 --- a/lib/features/feed/presentation/widgets/plans.dart +++ b/lib/features/feed/presentation/widgets/plans.dart @@ -1,3 +1,4 @@ +import 'package:eatvio_app/core/widgets/data_error_widget.dart'; import 'package:eatvio_app/core/widgets/loader.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'; @@ -22,15 +23,7 @@ class Plans extends StatelessWidget { body: const Loader(), ); } else if (state.errorMessage != null) { - return Scaffold( - backgroundColor: Colors.grey.shade200, - body: Padding( - padding: const EdgeInsets.all(16), - child: Center( - child: Text(state.errorMessage ?? ''), - ), - ), - ); + return DataErrorWidget(onReload: () => context.read().loadPlans()); } return Stack( children: [ @@ -65,4 +58,4 @@ class Plans extends StatelessWidget { ), ); } -} \ No newline at end of file +} diff --git a/lib/features/feed/presentation/widgets/recipes.dart b/lib/features/feed/presentation/widgets/recipes.dart index e5dc0fa5..b75d50ee 100644 --- a/lib/features/feed/presentation/widgets/recipes.dart +++ b/lib/features/feed/presentation/widgets/recipes.dart @@ -1,3 +1,4 @@ +import 'package:eatvio_app/core/widgets/data_error_widget.dart'; import 'package:eatvio_app/core/widgets/loader.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes_state.dart'; @@ -14,38 +15,35 @@ class Recipes extends StatelessWidget { onRefresh: () => context.read().loadRecipesList(), child: BlocBuilder( builder: (BuildContext context, RecipesState state) { - if (state is RecipesStateLoaded) { - return ListView.separated( - padding: EdgeInsets.symmetric(vertical: 8), - shrinkWrap: true, - physics: BouncingScrollPhysics(), - itemBuilder: (context, index) { - return RecipesWidget( - recipes: state.recipesList[index], - index: index, - ); - }, - separatorBuilder: (context, index) { - return SizedBox(height: 8); - }, - itemCount: state.recipesList.length, - ); - } else if (state is RecipesStateError) { - return Scaffold( - backgroundColor: Colors.grey.shade200, - body: Padding( - padding: const EdgeInsets.all(16), - child: Center( - child: Text(state.errorMessage), - ), - ), - ); - } else { + if (state.isLoading) { return Scaffold( backgroundColor: Colors.grey.shade200, body: const Loader(), ); } + if (state.errorMessage != null) { + return DataErrorWidget( + onReload: () async { + context.read().loadRecipesList(); + }, + ); + } + + return ListView.separated( + padding: EdgeInsets.symmetric(vertical: 8), + shrinkWrap: true, + physics: BouncingScrollPhysics(), + itemBuilder: (context, index) { + return RecipesWidget( + recipe: state.recipesList[index], + index: index, + ); + }, + separatorBuilder: (context, index) { + return SizedBox(height: 8); + }, + itemCount: state.recipesList.length, + ); }, ), ); diff --git a/lib/features/feed/presentation/widgets/recipes_widget.dart b/lib/features/feed/presentation/widgets/recipes_widget.dart index 16681a77..8e08c05a 100644 --- a/lib/features/feed/presentation/widgets/recipes_widget.dart +++ b/lib/features/feed/presentation/widgets/recipes_widget.dart @@ -2,11 +2,15 @@ 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/utils/localization_utils.dart'; +import 'package:eatvio_app/core/widgets/bookmark_button.dart'; import 'package:eatvio_app/core/widgets/feed_icon_button.dart'; import 'package:eatvio_app/features/feed/models/recipes_model.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes_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'; @@ -16,11 +20,11 @@ import 'package:share_plus/share_plus.dart'; class RecipesWidget extends StatefulWidget { final int index; - final Recipes recipes; + final Recipe recipe; const RecipesWidget({ super.key, - required this.recipes, + required this.recipe, required this.index, }); @@ -29,177 +33,207 @@ class RecipesWidget extends StatefulWidget { } class _RecipesWidgetState extends State { - late bool isLiked; - late int totalLikes; - - @override - void initState() { - super.initState(); - isLiked = false; - } - @override Widget build(BuildContext context) { + final s = S.of(context); return BlocBuilder( builder: (BuildContext context, RecipesState state) { - if (state is RecipesStateLoaded) { - isLiked = state.recipesList[widget.index].wasLikedByMe; - totalLikes = state.recipesList[widget.index].likesCount; - return InkWell( - borderRadius: BorderRadius.circular(12), - onTap: () => GoRouter.of(context).pushNamed('aboutRecipesScreen', extra: widget.recipes), - child: Ink( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - color: Colors.white, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Stack( - children: [ - ClipRRect( - borderRadius: BorderRadius.vertical(top: Radius.circular(12)), - child: CachedNetworkImage( - imageUrl: baseUrl + '/storage/' + widget.recipes.image, - fit: BoxFit.fitWidth, - width: MediaQuery.of(context).size.width, - ), - ), - Positioned( - left: 8, - bottom: 8, - child: Wrap( - runSpacing: 8, - children: [ - _iconButton('assets/icons/clock.svg', widget.recipes.cookingTime.toString() + ' минут'), - SizedBox(width: 8), - _iconButton('assets/icons/steps.svg', '${widget.recipes.steps?.length.toString()}' + ' этапов'), - SizedBox(width: 8), - _iconButton('assets/icons/calories.svg', widget.recipes.calories.toStringAsFixed(0) + ' ккал'), - ], - ), - ), - ], - ), - SizedBox(height: 12), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 12), - child: Text( - widget.recipes.title ?? '', - style: TextStyle( - fontWeight: FontWeight.w600, - color: Colors.black87, + final isLiked = widget.recipe.wasLikedByMe; + final totalLikes = widget.recipe.likesCount; + final isFavorite = widget.recipe.isFavorite; + final hours = widget.recipe.cookingTime ~/ 60; + final minutes = widget.recipe.cookingTime % 60; + final String timeTitle; + if (hours > 0) { + timeTitle = '$hours ${getHourForm(hours)} $minutes ${getMinuteForm(minutes)}'; + } else { + timeTitle = '$minutes ${getMinuteForm(minutes)}'; + } + return InkWell( + borderRadius: BorderRadius.circular(12), + onTap: () => GoRouter.of(context).pushNamed( + 'aboutRecipesScreen', + extra: { + 'recipe': widget.recipe, + 'index': widget.index, + 'navigateToComments': false, + }, + ), + child: Ink( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: Colors.white, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Stack( + children: [ + ClipRRect( + borderRadius: BorderRadius.vertical(top: Radius.circular(12)), + child: CachedNetworkImage( + imageUrl: baseUrl + '/storage/' + widget.recipe.image, + fit: BoxFit.fitWidth, + width: MediaQuery.of(context).size.width, ), ), - ), - if (widget.recipes.description.isNotEmpty) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 4), - child: Html( - data: widget.recipes.description.length > 200 ? widget.recipes.description.substring(0, 200) : widget.recipes.description, - style: { - "*": Style( - color: Colors.black87, - ), + Positioned( + left: 8, + bottom: 8, + child: Wrap( + runSpacing: 8, + children: [ + _iconButton('assets/icons/clock.svg', timeTitle), + SizedBox(width: 8), + _iconButton('assets/icons/steps.svg', '${widget.recipe.steps?.length ?? 0} ${getStepForm(widget.recipe.steps?.length ?? 0)}'), + SizedBox(width: 8), + _iconButton('assets/icons/calories.svg', widget.recipe.calories.toStringAsFixed(0) + ' ккал'), + ], + ), + ), + Positioned( + right: 14, + child: BookmarkButton( + onTap: () async { + if (isFavorite) { + await context.read().removeRecipeFromFavorite(widget.recipe.slug, widget.index); + } else { + await context.read().addRecipeToFavorite(widget.recipe.slug, widget.index); + } }, + color: appTheme.whiteA700, + isFavorite: isFavorite, + borderRadius: BorderRadius.vertical(bottom: Radius.circular(12)), + width: 40, + height: 44, + size: 19, ), ), - if (widget.recipes.description.length > 200) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Text( - 'Читать полностью...', - style: TextStyle( - height: 1, - color: appTheme.blueA400, - decoration: TextDecoration.underline, - ), - ), - ), + ], + ), + SizedBox(height: 12), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Text( + widget.recipe.title ?? '', + style: TextStyles.textMSemibold.copyWith(color: appTheme.black), + ), + ), + if (widget.recipe.videoUrl != null && widget.recipe.videoUrl!.isNotEmpty) ...[ SizedBox(height: 12), Padding( - padding: const EdgeInsets.symmetric(horizontal: 12), + padding: const EdgeInsets.only(left: 15), child: Row( children: [ - FeedIconButton( - icon: isLiked ? Icons.favorite_rounded : Icons.favorite_border_rounded, - text: widget.recipes.likesCount.toString(), - iconColor: isLiked ? Colors.red.withValues(alpha: 0.8) : null, - backgroundColor: isLiked ? Colors.red.withValues(alpha: 0.1) : null, - textColor: isLiked ? Colors.red.withValues(alpha: 0.8) : null, - onTap: () async { - setState(() { - isLiked = !isLiked; - isLiked ? totalLikes++ : totalLikes--; - }); - isLiked - ? await context.read().likeRecipe(state.recipesList[widget.index].slug) - : await context.read().unlikeRecipe(state.recipesList[widget.index].slug); - await context.read().loadRecipesList(); - }, - ), + SvgPicture.asset('assets/icons/play_icon.svg', width: 16, height: 16), SizedBox(width: 8), - FeedIconButton( - svgIcon: SvgPicture.asset( - 'assets/icons/comment.svg', - height: 20, - width: 20, - colorFilter: ColorFilter.mode( - appTheme.gray600, - BlendMode.srcIn, - ), - ), - text: widget.recipes.commentsCount.toString(), - onTap: () => GoRouter.of(context).pushNamed( - 'aboutRecipesScreen', - extra: widget.recipes, - ), - ), - SizedBox(width: 8), - FeedIconButton( - svgIcon: SvgPicture.asset( - height: 20, - width: 20, - 'assets/icons/share.svg', - colorFilter: ColorFilter.mode(appTheme.gray600, BlendMode.srcIn), - ), - onTap: () { - final box = context.findRenderObject() as RenderBox?; - Share.shareUri( - Uri.parse( - baseUrl + '/ru/blog_post/' + widget.recipes.id.toString(), - ), - sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, - ); - }, - ), - Spacer(), - Icon( - Icons.visibility_rounded, - size: 16, - color: appTheme.gray500, - ), - SizedBox(width: 4), Text( - widget.recipes.viewsCount.toString(), - style: TextStyle( - fontSize: 12, - color: appTheme.gray500, - fontWeight: FontWeight.w400, - ), + s.there_is_a_video, + style: TextStyles.textXSSemibold.copyWith(color: appTheme.black), ), ], ), ), - SizedBox(height: 12), ], - ), + SizedBox(height: 12), + if (widget.recipe.description.isNotEmpty) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Html( + data: widget.recipe.description.length > 200 ? widget.recipe.description.substring(0, 200) : widget.recipe.description, + style: { + "*": Style( + color: appTheme.black, + margin: Margins.all(0), + fontFamily: TextStyles.textSMedium.fontFamily, + fontSize: FontSize(TextStyles.textSMedium.fontSize ?? 14), + fontWeight: TextStyles.textSMedium.fontWeight, + ), + }, + ), + ), + if (widget.recipe.description.length > 200) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Text( + 'Читать полностью...', + style: TextStyle( + height: 1, + color: appTheme.blueA400, + decoration: TextDecoration.underline, + ), + ), + ), + SizedBox(height: 12), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Row( + children: [ + FeedIconButton( + icon: isLiked ? Icons.favorite_rounded : Icons.favorite_border_rounded, + text: totalLikes.toString(), + iconColor: isLiked ? Colors.red.withValues(alpha: 0.8) : null, + backgroundColor: isLiked ? Colors.red.withValues(alpha: 0.1) : null, + textColor: isLiked ? Colors.red.withValues(alpha: 0.8) : null, + onTap: () async { + await context.read().likeRecipe(widget.recipe.slug, widget.index); + }, + ), + SizedBox(width: 8), + FeedIconButton( + icon: Icons.chat_bubble_outline, + text: widget.recipe.commentsCount.toString(), + onTap: () => GoRouter.of(context).pushNamed( + 'aboutRecipesScreen', + extra: { + 'recipe': widget.recipe, + 'index': widget.index, + 'navigateToComments': true, + }, + ), + ), + SizedBox(width: 8), + FeedIconButton( + svgIcon: SvgPicture.asset( + height: 15, + width: 12, + 'assets/icons/repost.svg', + colorFilter: ColorFilter.mode(appTheme.gray600, BlendMode.srcIn), + ), + onTap: () { + final box = context.findRenderObject() as RenderBox?; + Share.shareUri( + Uri.parse( + baseUrl + '/ru/blog_post/' + widget.recipe.id.toString(), + ), + sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, + ); + }, + ), + Spacer(), + Icon( + Icons.visibility_rounded, + size: 16, + color: appTheme.gray500, + ), + SizedBox(width: 4), + Text( + widget.recipe.viewsCount.toString(), + style: TextStyle( + fontSize: 12, + color: appTheme.gray500, + fontWeight: FontWeight.w400, + ), + ), + ], + ), + ), + SizedBox(height: 12), + ], ), - ); - } - return Container(); + ), + ); }, ); } @@ -208,7 +242,7 @@ class _RecipesWidgetState extends State { return Container( padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4), decoration: BoxDecoration( - color: Colors.white.withValues(alpha: 0.8), + color: appTheme.whiteA700, borderRadius: BorderRadius.circular(16), ), height: 32, diff --git a/lib/features/feed/presentation/widgets/tags_modal_widget.dart b/lib/features/feed/presentation/widgets/tags_modal_widget.dart index 51b2fca0..f9382909 100644 --- a/lib/features/feed/presentation/widgets/tags_modal_widget.dart +++ b/lib/features/feed/presentation/widgets/tags_modal_widget.dart @@ -1,6 +1,6 @@ import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes_state.dart'; -import 'package:eatvio_app/features/recipes/models/recipe_tag_model.dart'; +import 'package:eatvio_app/features/create_recipe/models/recipe_tag_model.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -23,25 +23,21 @@ class _TagsModalWidgetState extends State { return BlocBuilder( bloc: widget.recipesCubit, builder: (BuildContext context, RecipesState state) { - if (state is RecipesStateLoaded) { - return Container( - padding: EdgeInsets.all(16), - width: MediaQuery.of(context).size.width, - child: Wrap( - spacing: 8, - runSpacing: -8, - children: [ - for (int i = 0; i < state.recipesTags.length; i++) - _tagChip( - tag: state.recipesTags[i], - isSelected: state.selectedTags.contains(state.recipesTags[i].id), - ), - ], - ), - ); - } else { - return Container(); - } + return Container( + padding: EdgeInsets.all(16), + width: MediaQuery.of(context).size.width, + child: Wrap( + spacing: 8, + runSpacing: -8, + children: [ + for (int i = 0; i < state.recipesTags.length; i++) + _tagChip( + tag: state.recipesTags[i], + isSelected: state.selectedTags.contains(state.recipesTags[i].id), + ), + ], + ), + ); }, ); } diff --git a/lib/features/feed/repositories/feed_repository.dart b/lib/features/feed/repositories/feed_repository.dart index 1f13becf..854ce4d3 100644 --- a/lib/features/feed/repositories/feed_repository.dart +++ b/lib/features/feed/repositories/feed_repository.dart @@ -27,7 +27,7 @@ class FeedRepository { } } - Future> getRecipes({ + Future> getRecipes({ String? query, List? tags, int page = 1, @@ -64,7 +64,7 @@ class FeedRepository { } } - Future getPlans({ + Future getPlans({ int page = 1, String? query, String? types, @@ -85,7 +85,7 @@ class FeedRepository { rethrow; } } - + Future getBlogPostComments({ required String id, }) async { @@ -217,24 +217,4 @@ class FeedRepository { rethrow; } } - - Future likeRecipe({ - required String slug, - }) async { - try { - await feedService.likeRecipe(slug: slug); - } catch (e) { - rethrow; - } - } - - Future unlikeRecipe({ - required String slug, - }) async { - try { - await feedService.unlikeRecipe(slug: slug); - } catch (e) { - rethrow; - } - } } diff --git a/lib/features/feed/services/feed_service.dart b/lib/features/feed/services/feed_service.dart index 73ec0bcb..9551f16b 100644 --- a/lib/features/feed/services/feed_service.dart +++ b/lib/features/feed/services/feed_service.dart @@ -103,16 +103,6 @@ abstract class FeedService { @Path() required int id, }); - @POST('/api/recipes/{slug}/like') - Future likeRecipe({ - @Path() required String slug, - }); - - @POST('/api/recipes/{slug}/unliked') - Future unlikeRecipe({ - @Path() required String slug, - }); - @GET('/api/consultants/index') Future getConsultants(); } diff --git a/lib/features/feed/services/feed_service.g.dart b/lib/features/feed/services/feed_service.g.dart index 951463e3..17825316 100644 --- a/lib/features/feed/services/feed_service.g.dart +++ b/lib/features/feed/services/feed_service.g.dart @@ -427,44 +427,6 @@ class _FeedService implements FeedService { await _dio.fetch(_options); } - @override - Future likeRecipe({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/recipes/${slug}/like', - queryParameters: queryParameters, - data: _data, - ) - .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), - ); - await _dio.fetch(_options); - } - - @override - Future unlikeRecipe({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/recipes/${slug}/unliked', - queryParameters: queryParameters, - data: _data, - ) - .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), - ); - await _dio.fetch(_options); - } - @override Future getConsultants() async { final _extra = {}; diff --git a/lib/features/internet_connection_checker/internet_connection_checker_screen.dart b/lib/features/internet_connection_checker/internet_connection_checker_screen.dart index f8d258b1..dc6ed06b 100644 --- a/lib/features/internet_connection_checker/internet_connection_checker_screen.dart +++ b/lib/features/internet_connection_checker/internet_connection_checker_screen.dart @@ -9,6 +9,7 @@ import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/widgets/custom_outlined_button.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_svg/svg.dart'; import 'package:http/http.dart' as http; class InternetConnectionCheckerScreen extends StatefulWidget { @@ -128,11 +129,12 @@ class NoInternetScreen extends StatelessWidget { body: Column( children: [ Spacer(), + SvgPicture.asset('assets/icons/no_internet.svg'), SizedBox(height: 28), Padding( padding: const EdgeInsets.symmetric(horizontal: 52), child: Text( - 'Отсутствует подключение к интернету', + 'Отсутствует подключение \nк интернету', textAlign: TextAlign.center, style: TextStyles.headingXLSemibold.copyWith( color: appTheme.black, diff --git a/lib/features/my_diets/presentation/cubit/my_diets_cubit.dart b/lib/features/my_diets/presentation/cubit/my_diets_cubit.dart index 09753d80..73f3459a 100644 --- a/lib/features/my_diets/presentation/cubit/my_diets_cubit.dart +++ b/lib/features/my_diets/presentation/cubit/my_diets_cubit.dart @@ -2,6 +2,7 @@ import 'dart:developer'; import 'package:easy_debounce/easy_debounce.dart'; import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/features/create_diet/repositories/create_diet_repository.dart'; import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; import 'package:eatvio_app/features/my_diets/presentation/cubit/my_diets_state.dart'; import 'package:eatvio_app/features/my_diets/repositories/my_diets_repository.dart'; @@ -9,6 +10,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; class MyDietsCubit extends Cubit { final MyDietsRepository myDietsRepository; + final CreateDietRepository createDietRepository; int currentDraftPage = 1; int currentPersonalPage = 1; @@ -35,7 +37,10 @@ class MyDietsCubit extends Cubit { static const List _durationValues = [null, ...durationValues]; static const List _types = [null, ...dietTypeValues]; - MyDietsCubit({required this.myDietsRepository}) : super(MyDietsState()); + MyDietsCubit({ + required this.myDietsRepository, + required this.createDietRepository, + }) : super(MyDietsState()); void changeTypeFilter(int index) { if (state.selectedStateIndex == 0) { @@ -202,13 +207,14 @@ class MyDietsCubit extends Cubit { state.copyWith( activeDiets: dietsPagination.data, isLoading: isLoadingActive, + activeDietsError: null, error: null, ), ); } catch (e, st) { log(e.toString(), stackTrace: st); emit( - state.copyWith(error: e.toString()), + state.copyWith(activeDietsError: e.toString()), ); } } @@ -232,12 +238,13 @@ class MyDietsCubit extends Cubit { state.copyWith( draftDiets: dietsPagination.data, isLoading: isLoadingDraft, + draftDietsError: null, error: null, ), ); } catch (e) { emit( - state.copyWith(error: e.toString()), + state.copyWith(draftDietsError: e.toString()), ); } } @@ -261,12 +268,13 @@ class MyDietsCubit extends Cubit { state.copyWith( personalDiets: dietsPagination.data, isLoading: isLoadingPersonal, + personalDietsError: null, error: null, ), ); } catch (e) { emit( - state.copyWith(error: e.toString()), + state.copyWith(personalDietsError: e.toString()), ); } } @@ -274,7 +282,6 @@ class MyDietsCubit extends Cubit { Future deleteDiet(int dietId) async { try { await myDietsRepository.deleteDiet(dietId); - await loadAllDiets(); return true; } catch (e) { emit( @@ -283,4 +290,30 @@ class MyDietsCubit extends Cubit { return false; } } + + Future publishDiet(int dietId) async { + try { + await createDietRepository.updateDiet( + dietId: dietId, + state: DietState.active, + ); + return true; + } catch (e, st) { + log(e.toString(), stackTrace: st); + return false; + } + } + + Future unpublishDiet(int dietId) async { + try { + await createDietRepository.updateDiet( + dietId: dietId, + state: DietState.draft, + ); + return true; + } catch (e, st) { + log(e.toString(), stackTrace: st); + 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 index 36b0ec38..b4cf5492 100644 --- a/lib/features/my_diets/presentation/cubit/my_diets_state.dart +++ b/lib/features/my_diets/presentation/cubit/my_diets_state.dart @@ -11,6 +11,9 @@ class MyDietsState with _$MyDietsState { @Default([]) List draftDiets, @Default([]) List personalDiets, @Default(false) bool isLoading, + @Default(null) String? activeDietsError, + @Default(null) String? draftDietsError, + @Default(null) String? personalDietsError, @Default(null) String? error, @Default(0) int selectedStateIndex, @Default( 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 index b15a73ae..91782100 100644 --- a/lib/features/my_diets/presentation/cubit/my_diets_state.freezed.dart +++ b/lib/features/my_diets/presentation/cubit/my_diets_state.freezed.dart @@ -20,6 +20,9 @@ mixin _$MyDietsState { List get draftDiets => throw _privateConstructorUsedError; List get personalDiets => throw _privateConstructorUsedError; bool get isLoading => throw _privateConstructorUsedError; + String? get activeDietsError => throw _privateConstructorUsedError; + String? get draftDietsError => throw _privateConstructorUsedError; + String? get personalDietsError => throw _privateConstructorUsedError; String? get error => throw _privateConstructorUsedError; int get selectedStateIndex => throw _privateConstructorUsedError; DietsFilterState get activeFilterState => throw _privateConstructorUsedError; @@ -45,6 +48,9 @@ abstract class $MyDietsStateCopyWith<$Res> { List draftDiets, List personalDiets, bool isLoading, + String? activeDietsError, + String? draftDietsError, + String? personalDietsError, String? error, int selectedStateIndex, DietsFilterState activeFilterState, @@ -75,6 +81,9 @@ class _$MyDietsStateCopyWithImpl<$Res, $Val extends MyDietsState> Object? draftDiets = null, Object? personalDiets = null, Object? isLoading = null, + Object? activeDietsError = freezed, + Object? draftDietsError = freezed, + Object? personalDietsError = freezed, Object? error = freezed, Object? selectedStateIndex = null, Object? activeFilterState = null, @@ -98,6 +107,18 @@ class _$MyDietsStateCopyWithImpl<$Res, $Val extends MyDietsState> ? _value.isLoading : isLoading // ignore: cast_nullable_to_non_nullable as bool, + activeDietsError: freezed == activeDietsError + ? _value.activeDietsError + : activeDietsError // ignore: cast_nullable_to_non_nullable + as String?, + draftDietsError: freezed == draftDietsError + ? _value.draftDietsError + : draftDietsError // ignore: cast_nullable_to_non_nullable + as String?, + personalDietsError: freezed == personalDietsError + ? _value.personalDietsError + : personalDietsError // ignore: cast_nullable_to_non_nullable + as String?, error: freezed == error ? _value.error : error // ignore: cast_nullable_to_non_nullable @@ -165,6 +186,9 @@ abstract class _$$MyDietsStateImplCopyWith<$Res> List draftDiets, List personalDiets, bool isLoading, + String? activeDietsError, + String? draftDietsError, + String? personalDietsError, String? error, int selectedStateIndex, DietsFilterState activeFilterState, @@ -196,6 +220,9 @@ class __$$MyDietsStateImplCopyWithImpl<$Res> Object? draftDiets = null, Object? personalDiets = null, Object? isLoading = null, + Object? activeDietsError = freezed, + Object? draftDietsError = freezed, + Object? personalDietsError = freezed, Object? error = freezed, Object? selectedStateIndex = null, Object? activeFilterState = null, @@ -219,6 +246,18 @@ class __$$MyDietsStateImplCopyWithImpl<$Res> ? _value.isLoading : isLoading // ignore: cast_nullable_to_non_nullable as bool, + activeDietsError: freezed == activeDietsError + ? _value.activeDietsError + : activeDietsError // ignore: cast_nullable_to_non_nullable + as String?, + draftDietsError: freezed == draftDietsError + ? _value.draftDietsError + : draftDietsError // ignore: cast_nullable_to_non_nullable + as String?, + personalDietsError: freezed == personalDietsError + ? _value.personalDietsError + : personalDietsError // ignore: cast_nullable_to_non_nullable + as String?, error: freezed == error ? _value.error : error // ignore: cast_nullable_to_non_nullable @@ -251,6 +290,9 @@ class _$MyDietsStateImpl implements _MyDietsState { final List draftDiets = const [], final List personalDiets = const [], this.isLoading = false, + this.activeDietsError = null, + this.draftDietsError = null, + this.personalDietsError = null, this.error = null, this.selectedStateIndex = 0, this.activeFilterState = const DietsFilterState( @@ -295,6 +337,15 @@ class _$MyDietsStateImpl implements _MyDietsState { final bool isLoading; @override @JsonKey() + final String? activeDietsError; + @override + @JsonKey() + final String? draftDietsError; + @override + @JsonKey() + final String? personalDietsError; + @override + @JsonKey() final String? error; @override @JsonKey() @@ -311,7 +362,7 @@ class _$MyDietsStateImpl implements _MyDietsState { @override String toString() { - return 'MyDietsState(activeDiets: $activeDiets, draftDiets: $draftDiets, personalDiets: $personalDiets, isLoading: $isLoading, error: $error, selectedStateIndex: $selectedStateIndex, activeFilterState: $activeFilterState, draftFilterState: $draftFilterState, personalFilterState: $personalFilterState)'; + return 'MyDietsState(activeDiets: $activeDiets, draftDiets: $draftDiets, personalDiets: $personalDiets, isLoading: $isLoading, activeDietsError: $activeDietsError, draftDietsError: $draftDietsError, personalDietsError: $personalDietsError, error: $error, selectedStateIndex: $selectedStateIndex, activeFilterState: $activeFilterState, draftFilterState: $draftFilterState, personalFilterState: $personalFilterState)'; } @override @@ -327,6 +378,12 @@ class _$MyDietsStateImpl implements _MyDietsState { .equals(other._personalDiets, _personalDiets) && (identical(other.isLoading, isLoading) || other.isLoading == isLoading) && + (identical(other.activeDietsError, activeDietsError) || + other.activeDietsError == activeDietsError) && + (identical(other.draftDietsError, draftDietsError) || + other.draftDietsError == draftDietsError) && + (identical(other.personalDietsError, personalDietsError) || + other.personalDietsError == personalDietsError) && (identical(other.error, error) || other.error == error) && (identical(other.selectedStateIndex, selectedStateIndex) || other.selectedStateIndex == selectedStateIndex) && @@ -345,6 +402,9 @@ class _$MyDietsStateImpl implements _MyDietsState { const DeepCollectionEquality().hash(_draftDiets), const DeepCollectionEquality().hash(_personalDiets), isLoading, + activeDietsError, + draftDietsError, + personalDietsError, error, selectedStateIndex, activeFilterState, @@ -366,6 +426,9 @@ abstract class _MyDietsState implements MyDietsState { final List draftDiets, final List personalDiets, final bool isLoading, + final String? activeDietsError, + final String? draftDietsError, + final String? personalDietsError, final String? error, final int selectedStateIndex, final DietsFilterState activeFilterState, @@ -381,6 +444,12 @@ abstract class _MyDietsState implements MyDietsState { @override bool get isLoading; @override + String? get activeDietsError; + @override + String? get draftDietsError; + @override + String? get personalDietsError; + @override String? get error; @override int get selectedStateIndex; diff --git a/lib/features/my_diets/presentation/my_diets_screen.dart b/lib/features/my_diets/presentation/my_diets_screen.dart index a58f3bdc..36fe1d31 100644 --- a/lib/features/my_diets/presentation/my_diets_screen.dart +++ b/lib/features/my_diets/presentation/my_diets_screen.dart @@ -4,6 +4,7 @@ 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/data_error_widget.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'; @@ -142,21 +143,26 @@ class MyDietsScreenContent extends StatelessWidget { 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 ?? ''), - ), - ), - ); + return DataErrorWidget(onReload: () => context.read().loadAllDiets()); } else { + final gotDietLoadingError = state.activeDietsError != null && state.selectedStateIndex == 0 || + state.draftDietsError != null && state.selectedStateIndex == 1 || + state.personalDietsError != null && state.selectedStateIndex == 2; + + if (gotDietLoadingError) { + return DataErrorWidget(onReload: () => context.read().loadCurrentDiets()); + } + + if (state.isLoading) { + return Loader(); + } + final diets = switch (state.selectedStateIndex) { 1 => state.draftDiets, 2 => state.personalDiets, _ => state.activeDiets, }; + return Stack( children: [ SizedBox.expand(), @@ -164,7 +170,10 @@ class MyDietsScreenContent extends StatelessWidget { shrinkWrap: true, physics: BouncingScrollPhysics(), itemBuilder: (context, index) { - return MyDietWidget(diet: diets[index]); + return MyDietWidget( + diet: diets[index], + key: ValueKey(diets[index].id), + ); }, separatorBuilder: (context, index) { return SizedBox(height: 20); diff --git a/lib/features/my_diets/presentation/widgets/diet_deleted_bottom_sheet.dart b/lib/features/my_diets/presentation/widgets/diet_deleted_bottom_sheet.dart new file mode 100644 index 00000000..e9561def --- /dev/null +++ b/lib/features/my_diets/presentation/widgets/diet_deleted_bottom_sheet.dart @@ -0,0 +1,57 @@ +import 'package:eatvio_app/core/app_router.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/custom_outlined_button.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +class DietDeletedBottomSheet extends StatelessWidget { + const DietDeletedBottomSheet({super.key, required this.title}); + final String title; + @override + Widget build(BuildContext context) { + final s = S.of(context); + return Column( + children: [ + SizedBox( + height: 20, + ), + BottomSheetHandlerBar(), + SizedBox( + height: 20, + ), + Text( + title, + 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: () { + AppRouter.navigatorKey.currentContext?.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/my_diets/presentation/widgets/diet_drafetd_bottom_sheet.dart b/lib/features/my_diets/presentation/widgets/diet_drafetd_bottom_sheet.dart new file mode 100644 index 00000000..4bcffd1c --- /dev/null +++ b/lib/features/my_diets/presentation/widgets/diet_drafetd_bottom_sheet.dart @@ -0,0 +1,59 @@ +import 'package:eatvio_app/core/app_router.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/custom_outlined_button.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +class DietDraftedBottomSheet extends StatelessWidget { + const DietDraftedBottomSheet({super.key, required this.title}); + final String title; + @override + Widget build(BuildContext context) { + final s = S.of(context); + return Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + height: 20, + ), + BottomSheetHandlerBar(), + SizedBox( + height: 20, + ), + Text( + title, + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), + ), + Text( + s.diet_has_been_sent_to_draft, + textAlign: TextAlign.center, + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), + ), + SizedBox( + height: 20, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: CustomOutlinedButton( + onPressed: () { + AppRouter.navigatorKey.currentContext?.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/my_diets/presentation/widgets/my_diet_widget.dart b/lib/features/my_diets/presentation/widgets/my_diet_widget.dart index ff8d060a..ec36cc6b 100644 --- a/lib/features/my_diets/presentation/widgets/my_diet_widget.dart +++ b/lib/features/my_diets/presentation/widgets/my_diet_widget.dart @@ -2,14 +2,18 @@ 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/enums/product_type.dart'; +import 'package:eatvio_app/core/utils/toasts.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/core/widgets/success_published_diet_bottom_sheet.dart'; import 'package:eatvio_app/features/create_diet/presentation/create_diet_screen.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/features/my_diets/presentation/widgets/diet_deleted_bottom_sheet.dart'; +import 'package:eatvio_app/features/my_diets/presentation/widgets/diet_drafetd_bottom_sheet.dart'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -59,11 +63,7 @@ class _MyDietWidgetState extends State { bottom: 16, child: SizedBox( width: MediaQuery.sizeOf(context).width, - child: InfoRow( - diet: widget.diet, - itemsBackgroundColor: appTheme.whiteA700, - disableDescriptionOfTypes: true, - ), + child: InfoRow(diet: widget.diet, itemsBackgroundColor: appTheme.whiteA700, disableDescriptionOfTypes: true, type: ProductType.diet), ), ), ], @@ -111,8 +111,18 @@ class _MyDietWidgetState extends State { children: [ Expanded( child: CustomOutlinedButton( - onPressed: () { + onPressed: () async { if (state.selectedStateIndex == 1) { + final result = await context.read().publishDiet(widget.diet.id); + if (result) { + showBottomSheetWithBlur( + context: context, + child: SuccessPublishedDietBottomSheet(title: '${widget.diet.name}'), + ); + await context.read().loadAllDiets(); + } else { + showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время'); + } } else if (state.selectedStateIndex == 2) { } else { GoRouter.of(context).pushNamed(CreateDietScreen.name, extra: widget.diet); @@ -168,8 +178,19 @@ class _MyDietWidgetState extends State { children: [ Expanded( child: CustomOutlinedButton( - onPressed: () { + onPressed: () async { if (state.selectedStateIndex == 0) { + final result = await context.read().unpublishDiet(widget.diet.id); + if (result) { + showBottomSheetWithBlur( + context: context, + height: 250, + child: DietDraftedBottomSheet(title: '${widget.diet.name}'), + ); + await context.read().loadAllDiets(); + } else { + showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время'); + } } else { GoRouter.of(context).pushNamed(CreateDietScreen.name, extra: widget.diet); } @@ -191,50 +212,13 @@ class _MyDietWidgetState extends State { CustomOutlinedButton( onPressed: () async { final result = await context.read().deleteDiet(widget.diet.id); - if (result) + if (result) { 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, - ), - ], - ), + child: DietDeletedBottomSheet(title: widget.diet.name), ); + await context.read().loadAllDiets(); + } }, height: 52, width: 52, diff --git a/lib/features/my_plans/presentation/my_plans_screen.dart b/lib/features/my_plans/presentation/my_plans_screen.dart index efa423e0..29bf9587 100644 --- a/lib/features/my_plans/presentation/my_plans_screen.dart +++ b/lib/features/my_plans/presentation/my_plans_screen.dart @@ -4,6 +4,7 @@ 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/data_error_widget.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'; @@ -138,15 +139,7 @@ class MyPlansScreenContent extends StatelessWidget { 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 ?? ''), - ), - ), - ); + return DataErrorWidget(onReload: () => context.read().loadPlans()); } else { final plans = switch (state.selectedStateIndex) { 1 => state.draftPlans, diff --git a/lib/features/my_plans/presentation/widgets/my_plan_widget.dart b/lib/features/my_plans/presentation/widgets/my_plan_widget.dart index 9132624d..5aa7e63a 100644 --- a/lib/features/my_plans/presentation/widgets/my_plan_widget.dart +++ b/lib/features/my_plans/presentation/widgets/my_plan_widget.dart @@ -2,6 +2,7 @@ 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/enums/product_type.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'; @@ -62,6 +63,7 @@ class _MyPlanWidgetState extends State { plan: widget.plan, itemsBackgroundColor: appTheme.whiteA700, disableDescriptionOfTypes: true, + type: ProductType.plan, ), ), ), diff --git a/lib/features/plans/presentation/cubit/about_plan_cubit.dart b/lib/features/plans/presentation/cubit/about_plan_cubit.dart index ad60723a..20ca860b 100644 --- a/lib/features/plans/presentation/cubit/about_plan_cubit.dart +++ b/lib/features/plans/presentation/cubit/about_plan_cubit.dart @@ -17,7 +17,7 @@ class AboutPlanCubit extends Cubit { emit(state.copyWith(planModel: plan, error: null)); } catch (e, stackTrace) { log(e.toString(), stackTrace: stackTrace); - emit(state.copyWith(error: e.toString())); + emit(state.copyWith(error: e.toString(), isLoading: false)); } } diff --git a/lib/features/plans/presentation/pages/about_plan_screen.dart b/lib/features/plans/presentation/pages/about_plan_screen.dart index db415e15..0022a4f2 100644 --- a/lib/features/plans/presentation/pages/about_plan_screen.dart +++ b/lib/features/plans/presentation/pages/about_plan_screen.dart @@ -2,9 +2,12 @@ import 'dart:io'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/enums/complained_content.dart'; +import 'package:eatvio_app/core/enums/product_type.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/data_error_widget.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'; @@ -93,12 +96,96 @@ class _AboutPlanScreenState extends State { builder: (BuildContext context, CommentsState commentsState) { return BlocBuilder( builder: (context, state) { - if (state.isLoading) return Scaffold(body: Loader()); + if (state.isLoading) + return Scaffold( + appBar: AppBar( + centerTitle: true, + title: Text( + widget.plan.name, + maxLines: 2, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + softWrap: true, + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), + ), + actions: [ + PortalTarget( + visible: isMoreOptionsVisible, + portalFollower: MoreOptions( + complainedContentType: ComplainedContent.plan, + path: 'plan/${widget.plan.id}', + slug: widget.plan.slug, + ), + anchor: Aligned( + shiftToWithinBound: AxisFlag(x: true, y: true), + follower: Alignment.topRight, + target: Alignment.bottomRight, + ), + child: IconButton( + onPressed: () { + setState(() { + isMoreOptionsVisible = !isMoreOptionsVisible; + }); + }, + icon: SvgPicture.asset( + 'assets/icons/more_button.svg', + width: 20, + height: 20, + ), + ), + ), + ], + ), + body: Loader(), + ); WidgetsBinding.instance.addPostFrameCallback((_) { if (widget.navigateToComments && !_jumped) { _scrollToBottom(); } }); + if (state.error != null) { + return Scaffold( + appBar: AppBar( + centerTitle: true, + title: Text( + widget.plan.name, + maxLines: 2, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + softWrap: true, + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), + ), + actions: [ + PortalTarget( + visible: isMoreOptionsVisible, + portalFollower: MoreOptions( + complainedContentType: ComplainedContent.plan, + path: 'plan/${widget.plan.id}', + slug: widget.plan.slug, + ), + anchor: Aligned( + shiftToWithinBound: AxisFlag(x: true, y: true), + follower: Alignment.topRight, + target: Alignment.bottomRight, + ), + child: IconButton( + onPressed: () { + setState(() { + isMoreOptionsVisible = !isMoreOptionsVisible; + }); + }, + icon: SvgPicture.asset( + 'assets/icons/more_button.svg', + width: 20, + height: 20, + ), + ), + ), + ], + ), + body: DataErrorWidget(onReload: () => context.read().getPlan(widget.plan.slug)), + ); + } return Scaffold( backgroundColor: appTheme.gray100, resizeToAvoidBottomInset: true, @@ -293,7 +380,9 @@ class _AboutPlanScreenState extends State { PortalTarget( visible: isMoreOptionsVisible, portalFollower: MoreOptions( + complainedContentType: ComplainedContent.plan, path: 'plan/${widget.plan.id}', + slug: widget.plan.slug, ), anchor: Aligned( shiftToWithinBound: AxisFlag(x: true, y: true), @@ -400,9 +489,7 @@ class _AboutPlanScreenState extends State { ), ), SizedBox(height: 12), - - /// TODO глянуть как заменить - InfoRow(plan: state.planModel), + InfoRow(plan: state.planModel, type: ProductType.plan), ExpandableText( text: widget.plan.details, ), diff --git a/lib/features/plans/presentation/widgets/item_widget.dart b/lib/features/plans/presentation/widgets/item_widget.dart index 4cf41e70..5ec3b288 100644 --- a/lib/features/plans/presentation/widgets/item_widget.dart +++ b/lib/features/plans/presentation/widgets/item_widget.dart @@ -10,11 +10,12 @@ class ItemWidget extends StatelessWidget { Widget build(BuildContext context) { return Row( children: [ - Text( - text, - style: TextStyles.textMMedium.copyWith(color: appTheme.black), + Expanded( + child: Text( + text, + style: TextStyles.textMMedium.copyWith(color: appTheme.black), + ), ), - Spacer(), SvgPicture.asset( 'assets/icons/ok.svg', width: 20, diff --git a/lib/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart b/lib/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart index ce0065c8..6b89e051 100644 --- a/lib/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart +++ b/lib/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart @@ -17,7 +17,7 @@ class ProfileCubit extends Cubit { required this.chatRepository, }) : super(const ProfileState.loading()); - getProfileBySlug(String slug) async { + Future getProfileBySlug(String slug) async { try { emit( ProfileState.loaded( @@ -31,7 +31,7 @@ class ProfileCubit extends Cubit { } } - getMyProfile({ + Future getMyProfile({ String period = '1', }) async { _currentPeriod = period; diff --git a/lib/features/profile/presentation/pages/profile_screen.dart b/lib/features/profile/presentation/pages/profile_screen.dart index 0b8b7af6..7aba6e23 100644 --- a/lib/features/profile/presentation/pages/profile_screen.dart +++ b/lib/features/profile/presentation/pages/profile_screen.dart @@ -3,10 +3,13 @@ import 'dart:io'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:dismissible_page/dismissible_page.dart'; import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/enums/complained_content.dart'; import 'package:eatvio_app/core/utils/date_time_utils.dart'; +import 'package:eatvio_app/core/widgets/data_error_widget.dart'; import 'package:eatvio_app/core/widgets/loader.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; import 'package:eatvio_app/features/chat/presentation/pages/photo_view_screen.dart'; +import 'package:eatvio_app/features/complaining/presentation/complaining_bottom_sheet.dart'; import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/plan_widget.dart'; import 'package:eatvio_app/features/profile/models/circumference_model.dart'; @@ -88,14 +91,10 @@ class _ProfileScreenState extends State { } return widget.author == null ? _myProfileScreen(state) : _profileScreen(state); } else if (state is ProfileStateError) { - return Scaffold( - appBar: AppBar(), - body: Center( - child: Padding( - padding: const EdgeInsets.all(16), - child: Text(state.errorMessage), - ), - ), + return DataErrorWidget( + onReload: () async { + widget.author == null ? await profileCubit.getMyProfile() : await profileCubit.getProfileBySlug(widget.author!.authorSlug); + }, ); } else { return Scaffold( @@ -567,6 +566,7 @@ class _ProfileScreenState extends State { Uri.parse( state.profile.educationResource?.vkLink ?? '', ), + mode: LaunchMode.externalApplication, ); }, 'assets/icons/vk.svg', @@ -579,6 +579,7 @@ class _ProfileScreenState extends State { Uri.parse( state.profile.educationResource?.vkLink ?? '', ), + mode: LaunchMode.externalApplication, ); }, 'assets/icons/telegram.svg', @@ -591,6 +592,7 @@ class _ProfileScreenState extends State { Uri.parse( state.profile.educationResource?.personalSiteLink ?? '', ), + mode: LaunchMode.externalApplication, ); }, 'assets/icons/web.svg', @@ -1008,19 +1010,34 @@ class _ProfileScreenState extends State { const Spacer(), PopupMenuButton( onSelected: (value) { - final box = context.findRenderObject() as RenderBox?; - Share.shareUri( - Uri.parse( - baseUrl + '/ru/profile/' + state.profile.slug, - ), - sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, - ); + if (value == 'share') { + final box = context.findRenderObject() as RenderBox?; + Share.shareUri( + Uri.parse( + baseUrl + '/ru/profile/' + state.profile.slug, + ), + sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, + ); + } else { + showModalBottomSheet( + context: context, + builder: (context) => ComplainingBottomSheet( + complainedContentType: ComplainedContent.profile, + slug: widget.author?.authorSlug, + ), + ); + } }, itemBuilder: (context) => [ PopupMenuItem( value: 'share', child: Text('Поделиться'), ), + if (widget.author != null) + PopupMenuItem( + value: 'complain', + child: Text('Пожаловаться'), + ), ], child: Icon(Icons.more_horiz_rounded), ), diff --git a/lib/features/recipe/presentation/about_recipe_screen.dart b/lib/features/recipe/presentation/about_recipe_screen.dart new file mode 100644 index 00000000..029b9c0f --- /dev/null +++ b/lib/features/recipe/presentation/about_recipe_screen.dart @@ -0,0 +1,560 @@ +import 'dart:developer'; +import 'dart:io'; + +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/enums/complained_content.dart'; +import 'package:eatvio_app/core/enums/product_type.dart'; +import 'package:eatvio_app/core/utils/localization_utils.dart'; +import 'package:eatvio_app/core/utils/string_extentions.dart'; +import 'package:eatvio_app/core/utils/toasts.dart'; +import 'package:eatvio_app/core/widgets/bookmark_button.dart'; +import 'package:eatvio_app/core/widgets/data_error_widget.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'; +import 'package:eatvio_app/features/diets/presentation/widgets/info_bouble.dart'; +import 'package:eatvio_app/core/widgets/main_background_bouble.dart'; +import 'package:eatvio_app/features/diets/presentation/widgets/more_options.dart'; +import 'package:eatvio_app/features/feed/models/recipes_model.dart'; +import 'package:eatvio_app/features/feed/presentation/bloc/comments_cubit/comments_cubit.dart'; +import 'package:eatvio_app/features/feed/presentation/bloc/comments_cubit/comments_state.dart'; +import 'package:eatvio_app/features/feed/presentation/widgets/comments_widget.dart'; +import 'package:eatvio_app/features/recipe/presentation/cubit/about_recipe_cubit.dart'; +import 'package:eatvio_app/features/recipe/presentation/cubit/about_recipe_state.dart'; +import 'package:eatvio_app/features/recipe/presentation/widgets/ingredients_widget.dart'; +import 'package:eatvio_app/features/recipe/presentation/widgets/steps_widget.dart'; +import 'package:eatvio_app/generated/l10n.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:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_portal/flutter_portal.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class AboutRecipeScreen extends StatefulWidget { + final Recipe recipe; + final int? index; + final bool navigateToComments; + + AboutRecipeScreen({super.key, required this.recipe, required this.index, required this.navigateToComments}); + + @override + State createState() => _AboutRecipeScreenState(); +} + +class _AboutRecipeScreenState extends State { + final CommentsCubit commentsCubit = sl(); + final AboutRecipeCubit recipeCubit = sl(); + int? replyId; + final ScrollController scrollController = ScrollController(); + final textEditingController = TextEditingController(); + bool isMoreOptionsVisible = false; + bool _jumped = false; + @override + void initState() { + super.initState(); + scrollController.addListener(() { + if (isMoreOptionsVisible) { + setState(() { + isMoreOptionsVisible = false; + }); + } + }); + } + + void _scrollToBottom() { + _jumped = true; + scrollController.jumpTo( + scrollController.position.maxScrollExtent, + ); + } + + @override + Widget build(BuildContext context) { + final s = S.of(context); + return BlocProvider( + create: (context) => recipeCubit..getRecipe(widget.recipe.slug), + child: BlocProvider( + create: (BuildContext context) => commentsCubit + ..getComments( + widget.recipe.slug.toString(), + CommentSourceType.recipe, + true, + ), + child: BlocConsumer( + listener: (BuildContext context, CommentsState commentsState) { + if (commentsState is CommentsStateLoaded) { + setState(() { + replyId = commentsState.replyId; + }); + } + }, + builder: (BuildContext context, CommentsState state) { + return BlocBuilder( + builder: (context, recipeState) { + if (recipeState is AboutRecipeStateLoaded) { + Future.delayed(Duration(milliseconds: 500), () { + if (widget.navigateToComments && !_jumped) { + _scrollToBottom(); + } + }); + return Scaffold( + backgroundColor: appTheme.gray100, + resizeToAvoidBottomInset: true, + bottomSheet: Container( + padding: EdgeInsets.symmetric(vertical: 12) + (Platform.isIOS ? const EdgeInsets.only(bottom: 16) : EdgeInsets.zero), + decoration: BoxDecoration( + color: Colors.white, + border: Border( + top: BorderSide( + color: Colors.black.withValues(alpha: 0.1), + ), + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (replyId != null) + GestureDetector( + onTap: () { + context.read().setReplyId(null); + setState(() { + replyId = null; + textEditingController.clear(); + }); + FocusManager.instance.primaryFocus?.unfocus(); + }, + child: Container( + decoration: BoxDecoration( + color: appTheme.blueA400, + shape: BoxShape.circle, + ), + height: 32, + width: 32, + child: Icon( + Icons.close_rounded, + size: 18, + color: Colors.white, + ), + ), + ), + if (replyId != null) SizedBox(width: 8), + Expanded( + child: SizedBox( + child: TextFormField( + onChanged: (_) { + setState(() {}); + }, + controller: textEditingController, + maxLines: 5, + minLines: 1, + keyboardType: TextInputType.multiline, + style: TextStyles.textMMedium.copyWith(color: appTheme.black), + decoration: InputDecoration( + isDense: true, + contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), + hintText: s.comment.capitalize(), + hintStyle: TextStyles.textMMedium.copyWith(color: appTheme.lightGray600), + filled: true, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + borderSide: BorderSide( + color: Color(0xFFD1D1D6), + width: 1, + ), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + borderSide: BorderSide( + color: Color(0xFFD1D1D6), + width: 1, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + borderSide: BorderSide( + color: Color(0xFFD1D1D6), + width: 1, + ), + ), + ), + ), + ), + ), + SizedBox(width: 8), + GestureDetector( + onTap: () { + if (textEditingController.text.length > 2) { + commentsCubit.sendComment( + id: widget.recipe.slug, + commentText: textEditingController.text, + parentCommentId: replyId, + commentSourceType: CommentSourceType.recipe, + ); + context.read().setReplyId(null); + setState(() { + textEditingController.clear(); + replyId = null; + }); + Future.delayed(Duration(milliseconds: 500), () { + recipeCubit.getRecipe(widget.recipe.slug, index: widget.index); + }); + FocusManager.instance.primaryFocus?.unfocus(); + } + }, + child: Container( + decoration: BoxDecoration( + color: textEditingController.text.length > 2 ? appTheme.blueA400 : appTheme.gray50, + shape: BoxShape.circle, + ), + height: 32, + width: 32, + child: Icon( + Icons.send_rounded, + size: 18, + color: textEditingController.text.length > 2 ? appTheme.whiteA700 : appTheme.black, + ), + ), + ), + ], + ), + ], + ), + ), + ), + appBar: AppBar( + centerTitle: true, + title: Text( + widget.recipe.title ?? '', + maxLines: 2, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + softWrap: true, + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), + ), + actions: [ + PortalTarget( + visible: isMoreOptionsVisible, + portalFollower: MoreOptions( + complainedContentType: ComplainedContent.recipe, + path: 'recipe/${widget.recipe.slug}', + slug: widget.recipe.slug, + ), + anchor: Aligned( + shiftToWithinBound: AxisFlag(x: true, y: true), + follower: Alignment.topRight, + target: Alignment.bottomRight, + ), + child: IconButton( + onPressed: () { + setState(() { + isMoreOptionsVisible = !isMoreOptionsVisible; + }); + }, + icon: SvgPicture.asset( + 'assets/icons/more_button.svg', + width: 20, + height: 20, + ), + ), + ), + ], + ), + body: SingleChildScrollView( + controller: scrollController, + padding: EdgeInsets.only(bottom: 160), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 8), + MainBackgroundBouble( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Stack( + children: [ + CachedNetworkImage( + imageUrl: baseUrl + '/storage/' + widget.recipe.image, + height: 300, + width: MediaQuery.sizeOf(context).width, + fit: BoxFit.fill, + ), + Positioned( + bottom: 16, + left: 0, + right: 0, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + InfoBouble( + children: [ + SvgPicture.asset('assets/icons/eye.svg', width: 18, height: 12), + SizedBox(width: 8), + Text( + widget.recipe.viewsCount.toString(), + style: TextStyles.textXSSemibold.copyWith(color: appTheme.black), + ), + ], + backgroundColor: appTheme.whiteA700, + ), + SizedBox(width: 12), + GestureDetector( + onTap: () { + recipeCubit.likeRecipe(widget.recipe.slug, widget.index); + }, + child: InfoBouble( + children: [ + SvgPicture.asset( + 'assets/icons/heart.svg', + width: 14, + height: 12, + colorFilter: recipeState.recipe.wasLikedByMe ? ColorFilter.mode(appTheme.blueA400, BlendMode.srcIn) : null, + ), + SizedBox(width: 8), + Text( + recipeState.recipe.likesCount.toString(), + style: TextStyles.textXSSemibold.copyWith(color: appTheme.black), + ), + ], + backgroundColor: appTheme.whiteA700, + ), + ), + SizedBox(width: 12), + InfoBouble( + children: [ + SvgPicture.asset('assets/icons/comment2.svg', width: 13, height: 12), + SizedBox(width: 8), + Text( + recipeState.recipe.commentsCount.toString(), + style: TextStyles.textXSSemibold.copyWith(color: appTheme.black), + ), + ], + backgroundColor: appTheme.whiteA700, + ), + ], + ), + ), + Positioned( + right: 14, + child: BookmarkButton( + onTap: () async { + if (recipeState.recipe.isFavorite) { + await context.read().removeFromFavorite(widget.recipe.slug, widget.index); + } else { + await context.read().addToFavorite(widget.recipe.slug, widget.index); + } + }, + color: appTheme.whiteA700, + isFavorite: recipeState.recipe.isFavorite, + borderRadius: BorderRadius.vertical(bottom: Radius.circular(12)), + width: 40, + height: 44, + size: 19, + ), + ), + ], + ), + SizedBox(height: 16), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Text( + s.about_recipe, + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), + ), + ), + SizedBox(height: 12), + InfoRow( + recipe: widget.recipe, + type: ProductType.recipe, + ), + if (widget.recipe.videoUrl != null) + Padding( + padding: const EdgeInsets.only(left: 16, bottom: 12, top: 12), + child: GestureDetector( + onTap: () async { + try { + final uri = Uri.parse(widget.recipe.videoUrl!); + if (await canLaunchUrl(uri)) { + await launchUrl(uri, mode: LaunchMode.externalApplication); + } + } catch (e) { + log(e.toString()); + showErrorToast(s.could_not_launch_video); + } + }, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + s.check_out_video_recipe, + style: TextStyles.textMSemibold.copyWith( + color: appTheme.black, + ), + ), + SizedBox(width: 12), + SvgPicture.asset('assets/icons/play_icon.svg', width: 16, height: 16), + ], + ), + ), + ), + ExpandableText( + text: widget.recipe.description, + ), + SizedBox(height: 20), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: AuthorCard( + author: widget.recipe.author, + title: s.recipe_author, + ), + ), + SizedBox(height: 16), + ], + ), + ), + SizedBox(height: 12), + if (widget.recipe.ingredients?.isNotEmpty == true) + IngredientsWidget( + recipe: widget.recipe, + ), + if (widget.recipe.steps?.isNotEmpty == true) ...[ + SizedBox(height: 12), + StepsWidget( + recipe: widget.recipe, + ), + ], + SizedBox(height: 8), + MainBackgroundBouble( + padding: EdgeInsets.symmetric(vertical: 16), + child: Column( + children: [ + Container( + padding: EdgeInsets.symmetric(horizontal: 16), + alignment: Alignment.centerLeft, + child: Text( + recipeState.recipe.commentsCount.toString() + ' ' + getCommentForm(widget.recipe.commentsCount, s), + style: TextStyles.textMMedium.copyWith( + color: appTheme.gray500, + ), + ), + ), + SizedBox(height: 16), + if (state is CommentsStateLoading) const Loader(), + if (state is CommentsStateLoaded) + CommentsWidget( + id: widget.recipe.slug, + commentSourceType: CommentSourceType.recipe, + comment: state.comment, + replyId: replyId, + ), + ], + ), + ), + ], + ), + ), + ); + } + if (recipeState is RecipeStateError) { + return Scaffold( + appBar: AppBar( + centerTitle: true, + title: Text( + widget.recipe.title ?? '', + maxLines: 2, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + softWrap: true, + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), + ), + actions: [ + PortalTarget( + visible: isMoreOptionsVisible, + portalFollower: MoreOptions( + complainedContentType: ComplainedContent.recipe, + path: 'recipe/${widget.recipe.slug}', + slug: widget.recipe.slug, + ), + anchor: Aligned( + shiftToWithinBound: AxisFlag(x: true, y: true), + follower: Alignment.topRight, + target: Alignment.bottomRight, + ), + child: IconButton( + onPressed: () { + setState(() { + isMoreOptionsVisible = !isMoreOptionsVisible; + }); + }, + icon: SvgPicture.asset( + 'assets/icons/more_button.svg', + width: 20, + height: 20, + ), + ), + ), + ], + ), + body: DataErrorWidget( + onReload: () => context.read().getRecipe(widget.recipe.slug, index: widget.index), + ), + ); + } + return Scaffold( + appBar: AppBar( + centerTitle: true, + title: Text( + widget.recipe.title ?? '', + maxLines: 2, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + softWrap: true, + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), + ), + actions: [ + PortalTarget( + visible: isMoreOptionsVisible, + portalFollower: MoreOptions( + complainedContentType: ComplainedContent.recipe, + path: 'recipe/${widget.recipe.slug}', + slug: widget.recipe.slug, + ), + anchor: Aligned( + shiftToWithinBound: AxisFlag(x: true, y: true), + follower: Alignment.topRight, + target: Alignment.bottomRight, + ), + child: IconButton( + onPressed: () { + setState(() { + isMoreOptionsVisible = !isMoreOptionsVisible; + }); + }, + icon: SvgPicture.asset( + 'assets/icons/more_button.svg', + width: 20, + height: 20, + ), + ), + ), + ], + ), + body: const Loader(), + ); + }, + ); + }, + ), + ), + ); + } +} diff --git a/lib/features/recipe/presentation/cubit/about_recipe_cubit.dart b/lib/features/recipe/presentation/cubit/about_recipe_cubit.dart new file mode 100644 index 00000000..24480a29 --- /dev/null +++ b/lib/features/recipe/presentation/cubit/about_recipe_cubit.dart @@ -0,0 +1,152 @@ +import 'dart:async'; +import 'dart:developer'; +import 'package:easy_debounce/easy_debounce.dart'; +import 'package:eatvio_app/core/utils/toasts.dart'; +import 'package:eatvio_app/features/diets/repositories/diet_repository.dart'; +import 'package:eatvio_app/features/recipe/repositories/about_recipe_repository.dart'; +import 'package:eatvio_app/features/profile/repositories/profile_repository.dart'; +import 'package:eatvio_app/features/recipe/presentation/cubit/about_recipe_state.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class AboutRecipeCubit extends Cubit { + AboutRecipeCubit({required this.recipeRepository, required this.profileRepository}) : super(AboutRecipeState.initial()); + final AboutRecipeRepository recipeRepository; + final ProfileRepository profileRepository; + bool isCurrentDietStarted = false; + bool hasStartedDiet = false; + String? userStartedDietName; + + Future getRecipe(String id, {int? index}) async { + try { + final recipe = await recipeRepository.getRecipe(id); + if (index != null) { + recipeRepository.emitCommentsStateToFeed(CommentsState(index: index, totalComments: recipe.commentsCount)); + } + emit(AboutRecipeState.loaded(recipe: recipe)); + } catch (e) { + log(e.toString()); + emit(AboutRecipeState.error(message: e.toString())); + } + } + + Future initRecipe(String id, {int? index}) async { + try { + emit(AboutRecipeState.loading()); + final recipe = await recipeRepository.getRecipe(id); + if (index != null) { + recipeRepository.emitCommentsStateToFeed(CommentsState(index: index, totalComments: recipe.commentsCount)); + } + emit(AboutRecipeState.loaded(recipe: recipe)); + } catch (e) { + log(e.toString()); + emit(AboutRecipeState.error(message: e.toString())); + } + } + + Future likeRecipe(String id, int? recipeIndexInFeed) async { + if (state is! AboutRecipeStateLoaded) return; + + final currentRecipe = (state as AboutRecipeStateLoaded).recipe; + + if (currentRecipe.wasLikedByMe) { + final updatedRecipe = currentRecipe.copyWith( + likesCount: currentRecipe.likesCount - 1, + wasLikedByMe: false, + ); + if (recipeIndexInFeed != null) { + recipeRepository + .emitLikesStateToFeed(LikesState(wasLikedByMe: updatedRecipe.wasLikedByMe, totalLikes: updatedRecipe.likesCount, index: recipeIndexInFeed)); + } + emit(AboutRecipeState.loaded(recipe: updatedRecipe)); + } else { + final updatedRecipe = currentRecipe.copyWith( + likesCount: currentRecipe.likesCount + 1, + wasLikedByMe: true, + ); + if (recipeIndexInFeed != null) { + recipeRepository + .emitLikesStateToFeed(LikesState(wasLikedByMe: updatedRecipe.wasLikedByMe, totalLikes: updatedRecipe.likesCount, index: recipeIndexInFeed)); + } + emit(AboutRecipeState.loaded(recipe: updatedRecipe)); + } + + EasyDebounce.debounce( + 'like_recipe', + Duration(seconds: 1), + () async { + try { + await recipeRepository.likeRecipe(id); + } catch (e) { + log(e.toString()); + if (recipeIndexInFeed != null) { + recipeRepository + .emitLikesStateToFeed(LikesState(wasLikedByMe: currentRecipe.wasLikedByMe, totalLikes: currentRecipe.likesCount, index: recipeIndexInFeed)); + } + emit(AboutRecipeState.loaded(recipe: currentRecipe)); + } + }, + ); + } + + Future addToFavorite(String id, int? recipeIndex) async { + if (state is! AboutRecipeStateLoaded) return; + + final currentRecipe = (state as AboutRecipeStateLoaded).recipe; + + final updatedRecipe = currentRecipe.copyWith( + isFavorite: true, + ); + if (recipeIndex != null) { + recipeRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: updatedRecipe.isFavorite, index: recipeIndex)); + } + emit(AboutRecipeState.loaded(recipe: updatedRecipe)); + + EasyDebounce.debounce( + 'add_or_remove_diet_to_favorites', + Duration(seconds: 1), + () async { + try { + await recipeRepository.addToFavorite(id); + } catch (e) { + log(e.toString()); + showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время'); + if (recipeIndex != null) { + recipeRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: currentRecipe.isFavorite, index: recipeIndex)); + } + emit(AboutRecipeState.loaded(recipe: currentRecipe)); + } + }, + ); + } + + Future removeFromFavorite(String id, int? recipeIndex) async { + if (state is! AboutRecipeStateLoaded) return; + + final currentRecipe = (state as AboutRecipeStateLoaded).recipe; + + final updatedRecipe = currentRecipe.copyWith( + isFavorite: false, + ); + if (recipeIndex != null) { + recipeRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: updatedRecipe.isFavorite, index: recipeIndex)); + } + emit(AboutRecipeState.loaded(recipe: updatedRecipe)); + + EasyDebounce.debounce( + 'add_or_remove_diet_to_favorites', + Duration(seconds: 1), + () async { + try { + await recipeRepository.removeFromFavorite(id); + } catch (e) { + log(e.toString()); + showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время'); + if (recipeIndex != null) { + recipeRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: currentRecipe.isFavorite, index: recipeIndex)); + } + emit(AboutRecipeState.loaded(recipe: currentRecipe)); + } + }, + ); + } +} diff --git a/lib/features/recipe/presentation/cubit/about_recipe_state.dart b/lib/features/recipe/presentation/cubit/about_recipe_state.dart new file mode 100644 index 00000000..9a96b569 --- /dev/null +++ b/lib/features/recipe/presentation/cubit/about_recipe_state.dart @@ -0,0 +1,16 @@ +import 'package:eatvio_app/features/feed/models/recipes_model.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'about_recipe_state.freezed.dart'; + +@freezed +class AboutRecipeState with _$AboutRecipeState { + const factory AboutRecipeState() = _AboutRecipeState; + + const factory AboutRecipeState.initial() = AboutRecipeStateInitial; + const factory AboutRecipeState.loading() = AboutRecipeStateLoading; + const factory AboutRecipeState.loaded({ + required Recipe recipe, + }) = AboutRecipeStateLoaded; + const factory AboutRecipeState.error({required String message}) = RecipeStateError; +} diff --git a/lib/features/recipe/presentation/cubit/about_recipe_state.freezed.dart b/lib/features/recipe/presentation/cubit/about_recipe_state.freezed.dart new file mode 100644 index 00000000..850c5386 --- /dev/null +++ b/lib/features/recipe/presentation/cubit/about_recipe_state.freezed.dart @@ -0,0 +1,805 @@ +// 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 'about_recipe_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 _$AboutRecipeState { + @optionalTypeArgs + TResult when( + TResult Function() $default, { + required TResult Function() initial, + required TResult Function() loading, + required TResult Function(Recipe recipe) loaded, + required TResult Function(String message) error, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull( + TResult? Function()? $default, { + TResult? Function()? initial, + TResult? Function()? loading, + TResult? Function(Recipe recipe)? loaded, + TResult? Function(String message)? error, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen( + TResult Function()? $default, { + TResult Function()? initial, + TResult Function()? loading, + TResult Function(Recipe recipe)? loaded, + TResult Function(String message)? error, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map( + TResult Function(_AboutRecipeState value) $default, { + required TResult Function(AboutRecipeStateInitial value) initial, + required TResult Function(AboutRecipeStateLoading value) loading, + required TResult Function(AboutRecipeStateLoaded value) loaded, + required TResult Function(RecipeStateError value) error, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull( + TResult? Function(_AboutRecipeState value)? $default, { + TResult? Function(AboutRecipeStateInitial value)? initial, + TResult? Function(AboutRecipeStateLoading value)? loading, + TResult? Function(AboutRecipeStateLoaded value)? loaded, + TResult? Function(RecipeStateError value)? error, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap( + TResult Function(_AboutRecipeState value)? $default, { + TResult Function(AboutRecipeStateInitial value)? initial, + TResult Function(AboutRecipeStateLoading value)? loading, + TResult Function(AboutRecipeStateLoaded value)? loaded, + TResult Function(RecipeStateError value)? error, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $AboutRecipeStateCopyWith<$Res> { + factory $AboutRecipeStateCopyWith( + AboutRecipeState value, $Res Function(AboutRecipeState) then) = + _$AboutRecipeStateCopyWithImpl<$Res, AboutRecipeState>; +} + +/// @nodoc +class _$AboutRecipeStateCopyWithImpl<$Res, $Val extends AboutRecipeState> + implements $AboutRecipeStateCopyWith<$Res> { + _$AboutRecipeStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of AboutRecipeState + /// with the given fields replaced by the non-null parameter values. +} + +/// @nodoc +abstract class _$$AboutRecipeStateImplCopyWith<$Res> { + factory _$$AboutRecipeStateImplCopyWith(_$AboutRecipeStateImpl value, + $Res Function(_$AboutRecipeStateImpl) then) = + __$$AboutRecipeStateImplCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$AboutRecipeStateImplCopyWithImpl<$Res> + extends _$AboutRecipeStateCopyWithImpl<$Res, _$AboutRecipeStateImpl> + implements _$$AboutRecipeStateImplCopyWith<$Res> { + __$$AboutRecipeStateImplCopyWithImpl(_$AboutRecipeStateImpl _value, + $Res Function(_$AboutRecipeStateImpl) _then) + : super(_value, _then); + + /// Create a copy of AboutRecipeState + /// with the given fields replaced by the non-null parameter values. +} + +/// @nodoc + +class _$AboutRecipeStateImpl implements _AboutRecipeState { + const _$AboutRecipeStateImpl(); + + @override + String toString() { + return 'AboutRecipeState()'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is _$AboutRecipeStateImpl); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when( + TResult Function() $default, { + required TResult Function() initial, + required TResult Function() loading, + required TResult Function(Recipe recipe) loaded, + required TResult Function(String message) error, + }) { + return $default(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull( + TResult? Function()? $default, { + TResult? Function()? initial, + TResult? Function()? loading, + TResult? Function(Recipe recipe)? loaded, + TResult? Function(String message)? error, + }) { + return $default?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen( + TResult Function()? $default, { + TResult Function()? initial, + TResult Function()? loading, + TResult Function(Recipe recipe)? loaded, + TResult Function(String message)? error, + required TResult orElse(), + }) { + if ($default != null) { + return $default(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map( + TResult Function(_AboutRecipeState value) $default, { + required TResult Function(AboutRecipeStateInitial value) initial, + required TResult Function(AboutRecipeStateLoading value) loading, + required TResult Function(AboutRecipeStateLoaded value) loaded, + required TResult Function(RecipeStateError value) error, + }) { + return $default(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull( + TResult? Function(_AboutRecipeState value)? $default, { + TResult? Function(AboutRecipeStateInitial value)? initial, + TResult? Function(AboutRecipeStateLoading value)? loading, + TResult? Function(AboutRecipeStateLoaded value)? loaded, + TResult? Function(RecipeStateError value)? error, + }) { + return $default?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap( + TResult Function(_AboutRecipeState value)? $default, { + TResult Function(AboutRecipeStateInitial value)? initial, + TResult Function(AboutRecipeStateLoading value)? loading, + TResult Function(AboutRecipeStateLoaded value)? loaded, + TResult Function(RecipeStateError value)? error, + required TResult orElse(), + }) { + if ($default != null) { + return $default(this); + } + return orElse(); + } +} + +abstract class _AboutRecipeState implements AboutRecipeState { + const factory _AboutRecipeState() = _$AboutRecipeStateImpl; +} + +/// @nodoc +abstract class _$$AboutRecipeStateInitialImplCopyWith<$Res> { + factory _$$AboutRecipeStateInitialImplCopyWith( + _$AboutRecipeStateInitialImpl value, + $Res Function(_$AboutRecipeStateInitialImpl) then) = + __$$AboutRecipeStateInitialImplCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$AboutRecipeStateInitialImplCopyWithImpl<$Res> + extends _$AboutRecipeStateCopyWithImpl<$Res, _$AboutRecipeStateInitialImpl> + implements _$$AboutRecipeStateInitialImplCopyWith<$Res> { + __$$AboutRecipeStateInitialImplCopyWithImpl( + _$AboutRecipeStateInitialImpl _value, + $Res Function(_$AboutRecipeStateInitialImpl) _then) + : super(_value, _then); + + /// Create a copy of AboutRecipeState + /// with the given fields replaced by the non-null parameter values. +} + +/// @nodoc + +class _$AboutRecipeStateInitialImpl implements AboutRecipeStateInitial { + const _$AboutRecipeStateInitialImpl(); + + @override + String toString() { + return 'AboutRecipeState.initial()'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$AboutRecipeStateInitialImpl); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when( + TResult Function() $default, { + required TResult Function() initial, + required TResult Function() loading, + required TResult Function(Recipe recipe) loaded, + required TResult Function(String message) error, + }) { + return initial(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull( + TResult? Function()? $default, { + TResult? Function()? initial, + TResult? Function()? loading, + TResult? Function(Recipe recipe)? loaded, + TResult? Function(String message)? error, + }) { + return initial?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen( + TResult Function()? $default, { + TResult Function()? initial, + TResult Function()? loading, + TResult Function(Recipe recipe)? loaded, + TResult Function(String message)? error, + required TResult orElse(), + }) { + if (initial != null) { + return initial(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map( + TResult Function(_AboutRecipeState value) $default, { + required TResult Function(AboutRecipeStateInitial value) initial, + required TResult Function(AboutRecipeStateLoading value) loading, + required TResult Function(AboutRecipeStateLoaded value) loaded, + required TResult Function(RecipeStateError value) error, + }) { + return initial(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull( + TResult? Function(_AboutRecipeState value)? $default, { + TResult? Function(AboutRecipeStateInitial value)? initial, + TResult? Function(AboutRecipeStateLoading value)? loading, + TResult? Function(AboutRecipeStateLoaded value)? loaded, + TResult? Function(RecipeStateError value)? error, + }) { + return initial?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap( + TResult Function(_AboutRecipeState value)? $default, { + TResult Function(AboutRecipeStateInitial value)? initial, + TResult Function(AboutRecipeStateLoading value)? loading, + TResult Function(AboutRecipeStateLoaded value)? loaded, + TResult Function(RecipeStateError value)? error, + required TResult orElse(), + }) { + if (initial != null) { + return initial(this); + } + return orElse(); + } +} + +abstract class AboutRecipeStateInitial implements AboutRecipeState { + const factory AboutRecipeStateInitial() = _$AboutRecipeStateInitialImpl; +} + +/// @nodoc +abstract class _$$AboutRecipeStateLoadingImplCopyWith<$Res> { + factory _$$AboutRecipeStateLoadingImplCopyWith( + _$AboutRecipeStateLoadingImpl value, + $Res Function(_$AboutRecipeStateLoadingImpl) then) = + __$$AboutRecipeStateLoadingImplCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$AboutRecipeStateLoadingImplCopyWithImpl<$Res> + extends _$AboutRecipeStateCopyWithImpl<$Res, _$AboutRecipeStateLoadingImpl> + implements _$$AboutRecipeStateLoadingImplCopyWith<$Res> { + __$$AboutRecipeStateLoadingImplCopyWithImpl( + _$AboutRecipeStateLoadingImpl _value, + $Res Function(_$AboutRecipeStateLoadingImpl) _then) + : super(_value, _then); + + /// Create a copy of AboutRecipeState + /// with the given fields replaced by the non-null parameter values. +} + +/// @nodoc + +class _$AboutRecipeStateLoadingImpl implements AboutRecipeStateLoading { + const _$AboutRecipeStateLoadingImpl(); + + @override + String toString() { + return 'AboutRecipeState.loading()'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$AboutRecipeStateLoadingImpl); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when( + TResult Function() $default, { + required TResult Function() initial, + required TResult Function() loading, + required TResult Function(Recipe recipe) loaded, + required TResult Function(String message) error, + }) { + return loading(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull( + TResult? Function()? $default, { + TResult? Function()? initial, + TResult? Function()? loading, + TResult? Function(Recipe recipe)? loaded, + TResult? Function(String message)? error, + }) { + return loading?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen( + TResult Function()? $default, { + TResult Function()? initial, + TResult Function()? loading, + TResult Function(Recipe recipe)? loaded, + TResult Function(String message)? error, + required TResult orElse(), + }) { + if (loading != null) { + return loading(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map( + TResult Function(_AboutRecipeState value) $default, { + required TResult Function(AboutRecipeStateInitial value) initial, + required TResult Function(AboutRecipeStateLoading value) loading, + required TResult Function(AboutRecipeStateLoaded value) loaded, + required TResult Function(RecipeStateError value) error, + }) { + return loading(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull( + TResult? Function(_AboutRecipeState value)? $default, { + TResult? Function(AboutRecipeStateInitial value)? initial, + TResult? Function(AboutRecipeStateLoading value)? loading, + TResult? Function(AboutRecipeStateLoaded value)? loaded, + TResult? Function(RecipeStateError value)? error, + }) { + return loading?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap( + TResult Function(_AboutRecipeState value)? $default, { + TResult Function(AboutRecipeStateInitial value)? initial, + TResult Function(AboutRecipeStateLoading value)? loading, + TResult Function(AboutRecipeStateLoaded value)? loaded, + TResult Function(RecipeStateError value)? error, + required TResult orElse(), + }) { + if (loading != null) { + return loading(this); + } + return orElse(); + } +} + +abstract class AboutRecipeStateLoading implements AboutRecipeState { + const factory AboutRecipeStateLoading() = _$AboutRecipeStateLoadingImpl; +} + +/// @nodoc +abstract class _$$AboutRecipeStateLoadedImplCopyWith<$Res> { + factory _$$AboutRecipeStateLoadedImplCopyWith( + _$AboutRecipeStateLoadedImpl value, + $Res Function(_$AboutRecipeStateLoadedImpl) then) = + __$$AboutRecipeStateLoadedImplCopyWithImpl<$Res>; + @useResult + $Res call({Recipe recipe}); + + $RecipeCopyWith<$Res> get recipe; +} + +/// @nodoc +class __$$AboutRecipeStateLoadedImplCopyWithImpl<$Res> + extends _$AboutRecipeStateCopyWithImpl<$Res, _$AboutRecipeStateLoadedImpl> + implements _$$AboutRecipeStateLoadedImplCopyWith<$Res> { + __$$AboutRecipeStateLoadedImplCopyWithImpl( + _$AboutRecipeStateLoadedImpl _value, + $Res Function(_$AboutRecipeStateLoadedImpl) _then) + : super(_value, _then); + + /// Create a copy of AboutRecipeState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? recipe = null, + }) { + return _then(_$AboutRecipeStateLoadedImpl( + recipe: null == recipe + ? _value.recipe + : recipe // ignore: cast_nullable_to_non_nullable + as Recipe, + )); + } + + /// Create a copy of AboutRecipeState + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $RecipeCopyWith<$Res> get recipe { + return $RecipeCopyWith<$Res>(_value.recipe, (value) { + return _then(_value.copyWith(recipe: value)); + }); + } +} + +/// @nodoc + +class _$AboutRecipeStateLoadedImpl implements AboutRecipeStateLoaded { + const _$AboutRecipeStateLoadedImpl({required this.recipe}); + + @override + final Recipe recipe; + + @override + String toString() { + return 'AboutRecipeState.loaded(recipe: $recipe)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$AboutRecipeStateLoadedImpl && + (identical(other.recipe, recipe) || other.recipe == recipe)); + } + + @override + int get hashCode => Object.hash(runtimeType, recipe); + + /// Create a copy of AboutRecipeState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$AboutRecipeStateLoadedImplCopyWith<_$AboutRecipeStateLoadedImpl> + get copyWith => __$$AboutRecipeStateLoadedImplCopyWithImpl< + _$AboutRecipeStateLoadedImpl>(this, _$identity); + + @override + @optionalTypeArgs + TResult when( + TResult Function() $default, { + required TResult Function() initial, + required TResult Function() loading, + required TResult Function(Recipe recipe) loaded, + required TResult Function(String message) error, + }) { + return loaded(recipe); + } + + @override + @optionalTypeArgs + TResult? whenOrNull( + TResult? Function()? $default, { + TResult? Function()? initial, + TResult? Function()? loading, + TResult? Function(Recipe recipe)? loaded, + TResult? Function(String message)? error, + }) { + return loaded?.call(recipe); + } + + @override + @optionalTypeArgs + TResult maybeWhen( + TResult Function()? $default, { + TResult Function()? initial, + TResult Function()? loading, + TResult Function(Recipe recipe)? loaded, + TResult Function(String message)? error, + required TResult orElse(), + }) { + if (loaded != null) { + return loaded(recipe); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map( + TResult Function(_AboutRecipeState value) $default, { + required TResult Function(AboutRecipeStateInitial value) initial, + required TResult Function(AboutRecipeStateLoading value) loading, + required TResult Function(AboutRecipeStateLoaded value) loaded, + required TResult Function(RecipeStateError value) error, + }) { + return loaded(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull( + TResult? Function(_AboutRecipeState value)? $default, { + TResult? Function(AboutRecipeStateInitial value)? initial, + TResult? Function(AboutRecipeStateLoading value)? loading, + TResult? Function(AboutRecipeStateLoaded value)? loaded, + TResult? Function(RecipeStateError value)? error, + }) { + return loaded?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap( + TResult Function(_AboutRecipeState value)? $default, { + TResult Function(AboutRecipeStateInitial value)? initial, + TResult Function(AboutRecipeStateLoading value)? loading, + TResult Function(AboutRecipeStateLoaded value)? loaded, + TResult Function(RecipeStateError value)? error, + required TResult orElse(), + }) { + if (loaded != null) { + return loaded(this); + } + return orElse(); + } +} + +abstract class AboutRecipeStateLoaded implements AboutRecipeState { + const factory AboutRecipeStateLoaded({required final Recipe recipe}) = + _$AboutRecipeStateLoadedImpl; + + Recipe get recipe; + + /// Create a copy of AboutRecipeState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + _$$AboutRecipeStateLoadedImplCopyWith<_$AboutRecipeStateLoadedImpl> + get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class _$$RecipeStateErrorImplCopyWith<$Res> { + factory _$$RecipeStateErrorImplCopyWith(_$RecipeStateErrorImpl value, + $Res Function(_$RecipeStateErrorImpl) then) = + __$$RecipeStateErrorImplCopyWithImpl<$Res>; + @useResult + $Res call({String message}); +} + +/// @nodoc +class __$$RecipeStateErrorImplCopyWithImpl<$Res> + extends _$AboutRecipeStateCopyWithImpl<$Res, _$RecipeStateErrorImpl> + implements _$$RecipeStateErrorImplCopyWith<$Res> { + __$$RecipeStateErrorImplCopyWithImpl(_$RecipeStateErrorImpl _value, + $Res Function(_$RecipeStateErrorImpl) _then) + : super(_value, _then); + + /// Create a copy of AboutRecipeState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? message = null, + }) { + return _then(_$RecipeStateErrorImpl( + message: null == message + ? _value.message + : message // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc + +class _$RecipeStateErrorImpl implements RecipeStateError { + const _$RecipeStateErrorImpl({required this.message}); + + @override + final String message; + + @override + String toString() { + return 'AboutRecipeState.error(message: $message)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$RecipeStateErrorImpl && + (identical(other.message, message) || other.message == message)); + } + + @override + int get hashCode => Object.hash(runtimeType, message); + + /// Create a copy of AboutRecipeState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$RecipeStateErrorImplCopyWith<_$RecipeStateErrorImpl> get copyWith => + __$$RecipeStateErrorImplCopyWithImpl<_$RecipeStateErrorImpl>( + this, _$identity); + + @override + @optionalTypeArgs + TResult when( + TResult Function() $default, { + required TResult Function() initial, + required TResult Function() loading, + required TResult Function(Recipe recipe) loaded, + required TResult Function(String message) error, + }) { + return error(message); + } + + @override + @optionalTypeArgs + TResult? whenOrNull( + TResult? Function()? $default, { + TResult? Function()? initial, + TResult? Function()? loading, + TResult? Function(Recipe recipe)? loaded, + TResult? Function(String message)? error, + }) { + return error?.call(message); + } + + @override + @optionalTypeArgs + TResult maybeWhen( + TResult Function()? $default, { + TResult Function()? initial, + TResult Function()? loading, + TResult Function(Recipe recipe)? loaded, + TResult Function(String message)? error, + required TResult orElse(), + }) { + if (error != null) { + return error(message); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map( + TResult Function(_AboutRecipeState value) $default, { + required TResult Function(AboutRecipeStateInitial value) initial, + required TResult Function(AboutRecipeStateLoading value) loading, + required TResult Function(AboutRecipeStateLoaded value) loaded, + required TResult Function(RecipeStateError value) error, + }) { + return error(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull( + TResult? Function(_AboutRecipeState value)? $default, { + TResult? Function(AboutRecipeStateInitial value)? initial, + TResult? Function(AboutRecipeStateLoading value)? loading, + TResult? Function(AboutRecipeStateLoaded value)? loaded, + TResult? Function(RecipeStateError value)? error, + }) { + return error?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap( + TResult Function(_AboutRecipeState value)? $default, { + TResult Function(AboutRecipeStateInitial value)? initial, + TResult Function(AboutRecipeStateLoading value)? loading, + TResult Function(AboutRecipeStateLoaded value)? loaded, + TResult Function(RecipeStateError value)? error, + required TResult orElse(), + }) { + if (error != null) { + return error(this); + } + return orElse(); + } +} + +abstract class RecipeStateError implements AboutRecipeState { + const factory RecipeStateError({required final String message}) = + _$RecipeStateErrorImpl; + + String get message; + + /// Create a copy of AboutRecipeState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + _$$RecipeStateErrorImplCopyWith<_$RecipeStateErrorImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/features/recipe/presentation/widgets/ingredients_widget.dart b/lib/features/recipe/presentation/widgets/ingredients_widget.dart new file mode 100644 index 00000000..6f83d6c1 --- /dev/null +++ b/lib/features/recipe/presentation/widgets/ingredients_widget.dart @@ -0,0 +1,59 @@ +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/main_background_bouble.dart'; +import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; +import 'package:eatvio_app/features/feed/models/recipes_model.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; + +class IngredientsWidget extends StatelessWidget { + const IngredientsWidget({super.key, required this.recipe}); + final Recipe recipe; + @override + Widget build(BuildContext context) { + final s = S.of(context); + return MainBackgroundBouble( + child: ExpansionTile( + initiallyExpanded: true, + shape: Border(), + title: Text( + s.ingredients, + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), + ), + children: List.generate(recipe.ingredients!.length, (index) { + return Padding( + padding: const EdgeInsets.only(left: 16, bottom: 8), + child: Row( + children: [ + Container( + width: 4, + height: 4, + decoration: BoxDecoration( + color: appTheme.black, + shape: BoxShape.circle, + ), + ), + SizedBox(width: 4), + Builder( + builder: (context) { + final portion = Portion.getPortionFromValue(recipe.ingredients![index].unit ?? ''); + + String? portionName; + if (portion != null) { + portionName = getLocalizedMeasurementUnits(s, portion); + } + return Text( + '${recipe.ingredients![index].name} - ${recipe.ingredients![index].portion} ${portionName}', + style: TextStyles.textMMedium.copyWith(color: appTheme.black), + ); + }, + ), + ], + ), + ); + }), + ), + ); + } +} diff --git a/lib/features/recipe/presentation/widgets/steps_widget.dart b/lib/features/recipe/presentation/widgets/steps_widget.dart new file mode 100644 index 00000000..3e41164d --- /dev/null +++ b/lib/features/recipe/presentation/widgets/steps_widget.dart @@ -0,0 +1,63 @@ +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/main_background_bouble.dart'; +import 'package:eatvio_app/features/feed/models/recipes_model.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/widgets.dart'; + +class StepsWidget extends StatelessWidget { + const StepsWidget({super.key, required this.recipe}); + final Recipe recipe; + @override + Widget build(BuildContext context) { + final s = S.of(context); + return MainBackgroundBouble( + padding: EdgeInsets.all(16), + child: Column( + spacing: 16, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + s.step_by_step_photo_recipe, + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), + ), + ...List.generate(recipe.steps?.length ?? 0, (index) { + return Column( + spacing: 8, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + decoration: BoxDecoration( + color: appTheme.blueA400, + borderRadius: BorderRadius.circular(8), + ), + padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8), + child: Text( + '${s.step} ${index + 1}', + style: TextStyles.textSSemibold.copyWith(color: appTheme.whiteA700), + ), + ), + if (recipe.steps?[index].image != null && recipe.steps![index].image.isNotEmpty) + AspectRatio( + aspectRatio: 16 / 12, + child: ClipRRect( + borderRadius: BorderRadius.circular(16), + child: Image.network( + baseUrl + '/storage/' + recipe.steps![index].image, + fit: BoxFit.cover, + ), + ), + ), + Text( + recipe.steps![index].description, + style: TextStyles.textMMedium.copyWith(color: appTheme.black), + ), + ], + ); + }), + ], + ), + ); + } +} diff --git a/lib/features/recipe/repositories/about_recipe_repository.dart b/lib/features/recipe/repositories/about_recipe_repository.dart new file mode 100644 index 00000000..a9178a65 --- /dev/null +++ b/lib/features/recipe/repositories/about_recipe_repository.dart @@ -0,0 +1,53 @@ +// ignore_for_file: public_member_api_docs, sort_constructors_first +import 'dart:async'; +import 'package:eatvio_app/features/diets/repositories/diet_repository.dart'; +import 'package:eatvio_app/features/feed/models/recipes_model.dart'; +import 'package:eatvio_app/features/recipe/services/about_recipe_service.dart'; + +class AboutRecipeRepository { + final AboutRecipeService recipeService; + final _recipeLikeStateStreamController = StreamController.broadcast(); + final _recipeFavoriteStateStreamController = StreamController.broadcast(); + final _recipeCommentsStateStreamController = StreamController.broadcast(); + Stream get recipeLikeState => _recipeLikeStateStreamController.stream; + Stream get recipeFavoriteState => _recipeFavoriteStateStreamController.stream; + Stream get recipeCommentsState => _recipeCommentsStateStreamController.stream; + + AboutRecipeRepository({required this.recipeService}); + + Future getRecipe(String recipeID) async { + try { + return await recipeService.getRecipe(recipeID); + } catch (e) { + rethrow; + } + } + + Future likeRecipe(String slug) async { + try { + await recipeService.likeRecipe(slug: slug); + } catch (e) { + rethrow; + } + } + + Future addToFavorite(String slug) async { + try { + await recipeService.addToFavorite(slug: slug); + } catch (e) { + rethrow; + } + } + + Future removeFromFavorite(String slug) async { + try { + await recipeService.removeFromFavorite(slug: slug); + } catch (e) { + rethrow; + } + } + + void emitLikesStateToFeed(LikesState state) => _recipeLikeStateStreamController.add(state); + void emitFavoriteStateToFeed(FavoriteState state) => _recipeFavoriteStateStreamController.add(state); + void emitCommentsStateToFeed(CommentsState state) => _recipeCommentsStateStreamController.add(state); +} diff --git a/lib/features/recipe/services/about_recipe_service.dart b/lib/features/recipe/services/about_recipe_service.dart new file mode 100644 index 00000000..b326f970 --- /dev/null +++ b/lib/features/recipe/services/about_recipe_service.dart @@ -0,0 +1,28 @@ +import 'package:dio/dio.dart'; +import 'package:eatvio_app/features/feed/models/recipes_model.dart'; +import 'package:retrofit/retrofit.dart'; + +part 'about_recipe_service.g.dart'; + +@RestApi() +abstract class AboutRecipeService { + factory AboutRecipeService(Dio dio, {String baseUrl}) = _AboutRecipeService; + + @POST('/api/recipes/{slug}/like') + Future likeRecipe({ + @Path() required String slug, + }); + + @POST('/api/recipes/{slug}/add_to_favorite') + Future addToFavorite({ + @Path() required String slug, + }); + + @POST('/api/recipes/{slug}/remove_from_favorite') + Future removeFromFavorite({ + @Path() required String slug, + }); + + @GET('/api/recipes/show/{id}') + Future getRecipe(@Path() String id); +} diff --git a/lib/features/recipe/services/about_recipe_service.g.dart b/lib/features/recipe/services/about_recipe_service.g.dart new file mode 100644 index 00000000..d8ed1366 --- /dev/null +++ b/lib/features/recipe/services/about_recipe_service.g.dart @@ -0,0 +1,130 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'about_recipe_service.dart'; + +// ************************************************************************** +// RetrofitGenerator +// ************************************************************************** + +// ignore_for_file: unnecessary_brace_in_string_interps,no_leading_underscores_for_local_identifiers,unused_element,unnecessary_string_interpolations + +class _AboutRecipeService implements AboutRecipeService { + _AboutRecipeService(this._dio, {this.baseUrl, this.errorLogger}); + + final Dio _dio; + + String? baseUrl; + + final ParseErrorLogger? errorLogger; + + @override + Future likeRecipe({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/recipes/${slug}/like', + queryParameters: queryParameters, + data: _data, + ) + .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), + ); + await _dio.fetch(_options); + } + + @override + Future addToFavorite({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/recipes/${slug}/add_to_favorite', + queryParameters: queryParameters, + data: _data, + ) + .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), + ); + await _dio.fetch(_options); + } + + @override + Future removeFromFavorite({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/recipes/${slug}/remove_from_favorite', + queryParameters: queryParameters, + data: _data, + ) + .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), + ); + await _dio.fetch(_options); + } + + @override + Future getRecipe(String id) async { + final _extra = {}; + final queryParameters = {}; + final _headers = {}; + const Map? _data = null; + final _options = _setStreamType( + Options(method: 'GET', headers: _headers, extra: _extra) + .compose( + _dio.options, + '/api/recipes/show/${id}', + queryParameters: queryParameters, + data: _data, + ) + .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), + ); + final _result = await _dio.fetch>(_options); + late Recipe _value; + try { + _value = Recipe.fromJson(_result.data!); + } on Object catch (e, s) { + errorLogger?.logError(e, s, _options); + rethrow; + } + return _value; + } + + 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/recipes/presentation/pages/about_recipes_screen.dart b/lib/features/recipes/presentation/pages/about_recipes_screen.dart deleted file mode 100644 index 3bb3d35f..00000000 --- a/lib/features/recipes/presentation/pages/about_recipes_screen.dart +++ /dev/null @@ -1,430 +0,0 @@ -import 'dart:io'; -import 'package:cached_network_image/cached_network_image.dart'; -import 'package:eatvio_app/core/const.dart'; -import 'package:eatvio_app/core/widgets/loader.dart'; - -import 'package:eatvio_app/features/feed/models/recipes_model.dart' as step; -import 'package:eatvio_app/features/feed/models/recipes_model.dart'; -import 'package:eatvio_app/features/feed/presentation/bloc/comments_cubit/comments_cubit.dart'; -import 'package:eatvio_app/features/feed/presentation/bloc/comments_cubit/comments_state.dart'; -import 'package:eatvio_app/features/feed/presentation/widgets/comments_widget.dart'; -import 'package:eatvio_app/core/locator_service.dart'; -import 'package:eatvio_app/core/theme/theme_helper.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_html/flutter_html.dart'; -import 'package:go_router/go_router.dart'; - -class AboutRecipesScreen extends StatefulWidget { - final Recipes recipes; - - const AboutRecipesScreen({super.key, required this.recipes}); - - @override - State createState() => _AboutRecipesScreenState(); -} - -class _AboutRecipesScreenState extends State { - final CommentsCubit commentsCubit = sl(); - final textEditingController = TextEditingController(); - int? replyId; - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (BuildContext context) => commentsCubit - ..getComments( - widget.recipes.slug, - CommentSourceType.recipe, - true, - ), - child: BlocConsumer( - listener: (BuildContext context, CommentsState state) { - if (state is CommentsStateLoaded) { - setState(() { - replyId = state.replyId; - }); - } - }, - builder: (BuildContext context, CommentsState state) { - return Scaffold( - resizeToAvoidBottomInset: true, - bottomSheet: Container( - padding: EdgeInsets.symmetric(vertical: 12) + (Platform.isIOS ? const EdgeInsets.only(bottom: 16) : EdgeInsets.zero), - decoration: BoxDecoration( - color: Colors.white, - border: Border( - top: BorderSide( - color: Colors.black.withValues(alpha: 0.1), - ), - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - if (replyId != null) - GestureDetector( - onTap: () { - context.read().setReplyId(null); - setState(() { - replyId = null; - textEditingController.clear(); - }); - FocusManager.instance.primaryFocus?.unfocus(); - }, - child: Container( - decoration: BoxDecoration( - color: appTheme.blueA400, - shape: BoxShape.circle, - ), - height: 32, - width: 32, - child: Icon( - Icons.close_rounded, - size: 18, - color: Colors.white, - ), - ), - ), - if (replyId != null) SizedBox(width: 8), - SizedBox( - width: MediaQuery.of(context).size.width - (replyId == null ? 64 : 104), - child: TextFormField( - onChanged: (_) { - setState(() {}); - }, - controller: textEditingController, - maxLines: 5, - minLines: 1, - keyboardType: TextInputType.multiline, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w400, - color: Colors.black, - height: 1, - ), - decoration: InputDecoration( - isDense: true, - contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), - hintText: 'Комментарий', - hintStyle: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w400, - height: 1, - color: Color(0xFFAEAEB2), - ), - filled: true, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(16), - borderSide: BorderSide( - color: Color(0xFFD1D1D6), - width: 1, - ), - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(16), - borderSide: BorderSide( - color: Color(0xFFD1D1D6), - width: 1, - ), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(16), - borderSide: BorderSide( - color: Color(0xFFD1D1D6), - width: 1, - ), - ), - ), - ), - ), - SizedBox(width: 8), - GestureDetector( - onTap: () { - if (textEditingController.text.length > 2) { - commentsCubit.sendComment( - id: widget.recipes.slug, - commentText: textEditingController.text, - parentCommentId: replyId, - commentSourceType: CommentSourceType.recipe, - ); - context.read().setReplyId(null); - setState(() { - textEditingController.clear(); - replyId = null; - }); - FocusManager.instance.primaryFocus?.unfocus(); - } - }, - child: Container( - decoration: BoxDecoration( - color: textEditingController.text.length > 2 ? appTheme.blueA400 : Colors.grey, - shape: BoxShape.circle, - ), - height: 32, - width: 32, - child: Icon( - Icons.send_rounded, - size: 18, - color: Colors.white, - ), - ), - ), - ], - ), - ), - appBar: AppBar( - actions: [ - InkWell( - borderRadius: BorderRadius.circular(16), - onTap: () => GoRouter.of(context).pushNamed('profileScreen', extra: widget.recipes.author), - child: Ink( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - CircleAvatar( - maxRadius: 16, - minRadius: 16, - backgroundImage: NetworkImage(widget.recipes.author.authorProfileIcon), - ), - SizedBox(height: 4), - Text( - widget.recipes.author.authorName, - style: TextStyle( - color: Colors.black, - fontSize: 14, - fontWeight: FontWeight.w400, - height: 1, - ), - ), - ], - ), - ), - ), - ], - ), - body: SingleChildScrollView( - padding: EdgeInsets.only(bottom: 80), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Text( - widget.recipes.title ?? '', - style: TextStyle( - color: Colors.black, - fontSize: 24, - fontWeight: FontWeight.w500, - height: 1, - ), - ), - ), - SizedBox(height: 12), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Row( - children: [ - Icon(Icons.timer_outlined), - SizedBox(width: 4), - Text( - widget.recipes.cookingTime.toString() + ' минут', - style: TextStyle( - color: Colors.black, - fontSize: 12, - fontWeight: FontWeight.w400, - height: 1, - ), - ), - SizedBox(width: 16), - Icon(Icons.fast_forward_outlined), - SizedBox(width: 4), - Text( - '${widget.recipes.steps?.length ?? 0}' + ' этапов', - style: TextStyle( - color: Colors.black, - fontSize: 12, - fontWeight: FontWeight.w400, - height: 1, - ), - ), - ], - ), - ), - if (widget.recipes.description.isNotEmpty) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: Html( - data: widget.recipes.description, - style: { - "*": Style( - color: Colors.black87, - ), - }, - ), - ), - SizedBox(height: 12), - CachedNetworkImage( - imageUrl: baseUrl + '/storage/' + widget.recipes.image, - ), - SizedBox(height: 12), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Text( - 'Ингредиенты', - style: TextStyle( - color: Colors.black, - fontSize: 16, - fontWeight: FontWeight.w500, - height: 1, - ), - ), - ), - SizedBox(height: 12), - ListView.separated( - padding: const EdgeInsets.symmetric(horizontal: 16), - shrinkWrap: true, - physics: NeverScrollableScrollPhysics(), - itemBuilder: (context, index) { - if (widget.recipes.ingredients != null) { - return _ingredient(widget.recipes.ingredients![index]); - } - return SizedBox.shrink(); - }, - separatorBuilder: (context, index) => SizedBox(height: 4), - itemCount: widget.recipes.ingredients?.length ?? 0, - ), - SizedBox(height: 12), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Text( - 'Пошаговый рецепт', - style: TextStyle( - color: Colors.black, - fontSize: 16, - fontWeight: FontWeight.w500, - height: 1, - ), - ), - ), - SizedBox(height: 12), - ListView.separated( - shrinkWrap: true, - physics: NeverScrollableScrollPhysics(), - itemBuilder: (context, index) { - if (widget.recipes.steps == null) return SizedBox.shrink(); - return _step(widget.recipes.steps![index], index + 1); - }, - separatorBuilder: (context, index) => Padding( - padding: const EdgeInsets.all(16), - child: Container( - height: 0.4, - decoration: BoxDecoration( - color: Colors.grey, - borderRadius: BorderRadius.circular(16), - ), - ), - ), - itemCount: widget.recipes.steps?.length ?? 0, - ), - SizedBox(height: 8), - Padding( - padding: const EdgeInsets.all(16), - child: Container( - height: 0.4, - decoration: BoxDecoration( - color: Colors.grey, - borderRadius: BorderRadius.circular(16), - ), - ), - ), - SizedBox(height: 8), - Container( - padding: EdgeInsets.symmetric(horizontal: 16), - alignment: Alignment.centerLeft, - child: Text( - widget.recipes.commentsCount.toString() + ' КОММЕНТАРИЕВ', - style: TextStyle( - fontSize: 12, - color: appTheme.gray500, - fontWeight: FontWeight.w400, - ), - ), - ), - SizedBox(height: 16), - if (state is CommentsStateLoading) - Padding( - padding: const EdgeInsets.all(32), - child: const Loader(), - ), - if (state is CommentsStateLoaded) - CommentsWidget( - id: widget.recipes.slug, - commentSourceType: CommentSourceType.recipe, - comment: state.comment, - replyId: replyId, - ), - ], - ), - ), - ); - }, - ), - ); - } - - _ingredient(Ingredient ingredient) { - return Text( - '• ' + ingredient.name + ' ' + ingredient.portion + ' ' + (ingredient.unit ?? ''), - style: TextStyle( - color: Colors.black, - fontSize: 14, - fontWeight: FontWeight.w400, - height: 1.2, - ), - ); - } - - _step(step.Step step, int stepNumber) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Text( - 'Шаг ' + stepNumber.toString(), - style: TextStyle( - color: Colors.black54, - fontSize: 14, - fontWeight: FontWeight.w400, - height: 1, - ), - ), - ), - SizedBox(height: 12), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Text( - step.description, - style: TextStyle( - color: Colors.black, - fontSize: 14, - fontWeight: FontWeight.w400, - height: 1.2, - ), - ), - ), - SizedBox(height: 8), - if (step.image.length > 1) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: ClipRRect( - borderRadius: BorderRadius.circular(16), - child: CachedNetworkImage(imageUrl: baseUrl + '/storage/' + step.image), - ), - ), - ], - ); - } -} diff --git a/lib/features/recipes/services/recipe_service.dart b/lib/features/recipes/services/recipe_service.dart deleted file mode 100644 index 12e9c78b..00000000 --- a/lib/features/recipes/services/recipe_service.dart +++ /dev/null @@ -1,30 +0,0 @@ -import 'package:dio/dio.dart'; -import 'package:eatvio_app/features/recipes/models/recipe_model.dart'; -import 'package:eatvio_app/features/recipes/models/recipe_tag_model.dart'; -import 'package:retrofit/retrofit.dart'; -import 'package:eatvio_app/features/recipes/models/product.dart'; - -part 'recipe_service.g.dart'; - -@RestApi() -abstract class RecipeService { - factory RecipeService(Dio dio, {String baseUrl}) = _RecipeService; - - @GET('/api/recipes/show/{id}') - Future getRecipe(@Path() String id); - - @GET('/api/diary/product_nutritional_values/filter') - Future>> getInputProduct(@Query('name') String name); - - @POST('/api/recipes/create') - @MultiPart() - Future publishRecipe({ - @Body() required FormData formData, - }); - - @POST('/api/recipes/update/{id}') - Future editRecipe(@Part() Map recipe, String id); - - @GET('/api/recipes/tags') - Future getTags(); -} \ No newline at end of file diff --git a/lib/features/settings/presentation/pages/settings_screen.dart b/lib/features/settings/presentation/pages/settings_screen.dart index bd28f36a..c7ac0a30 100644 --- a/lib/features/settings/presentation/pages/settings_screen.dart +++ b/lib/features/settings/presentation/pages/settings_screen.dart @@ -1,4 +1,5 @@ import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/widgets/data_error_widget.dart'; import 'package:eatvio_app/core/widgets/loader.dart'; import 'package:eatvio_app/features/settings/entities/settings_entity.dart'; import 'package:eatvio_app/features/settings/presentation/bloc/settings_cubit/settings_cubit.dart'; @@ -69,14 +70,17 @@ class _SettingsScreenState extends State { } if (state is SettingsStateError) { return Scaffold( - body: Padding( - padding: const EdgeInsets.all(16), - child: Center(child: Text(state.errorMessage)), + appBar: AppBar( + title: Text('Настройки'), ), + body: DataErrorWidget(onReload: () => settingsCubit.init()), ); } return Scaffold( - body: const Loader(), + appBar: AppBar( + title: Text('Настройки'), + ), + body: Loader(), ); }, ), diff --git a/lib/features/support/presentation/pages/support_screen.dart b/lib/features/support/presentation/pages/support_screen.dart index 00e51c42..dcac5568 100644 --- a/lib/features/support/presentation/pages/support_screen.dart +++ b/lib/features/support/presentation/pages/support_screen.dart @@ -24,7 +24,10 @@ class SupportScreen extends StatelessWidget { ), InkWell( onTap: () { - launchUrl(Uri.parse('https://t.me/eatvio')); + launchUrl( + Uri.parse('https://t.me/eatvio'), + mode: LaunchMode.externalApplication, + ); }, borderRadius: BorderRadius.circular(16), child: Ink( @@ -53,7 +56,10 @@ class SupportScreen extends StatelessWidget { const SizedBox(height: 8), InkWell( onTap: () { - launchUrl(Uri.parse('mailto:help@eatv.io')); + launchUrl( + Uri.parse('mailto:help@eatv.io'), + mode: LaunchMode.externalApplication, + ); }, borderRadius: BorderRadius.circular(16), child: Ink( diff --git a/lib/features/user_details/presentation/user_details_screen.dart b/lib/features/user_details/presentation/user_details_screen.dart index 2aa03504..345bc895 100644 --- a/lib/features/user_details/presentation/user_details_screen.dart +++ b/lib/features/user_details/presentation/user_details_screen.dart @@ -1,9 +1,11 @@ +import 'package:eatvio_app/core/enums/complained_content.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/utils/date_time_utils.dart'; import 'package:eatvio_app/core/utils/string_extentions.dart'; import 'package:eatvio_app/core/widgets/custom_text_form_field.dart'; +import 'package:eatvio_app/core/widgets/data_error_widget.dart'; import 'package:eatvio_app/core/widgets/loader.dart'; import 'package:eatvio_app/features/diets/presentation/widgets/more_options.dart'; import 'package:eatvio_app/features/profile/models/circumference_model.dart'; @@ -79,6 +81,8 @@ class _UserDetailsContentState extends State { visible: isMoreOptionsVisible, portalFollower: MoreOptions( path: 'profile/${widget.slug}', + complainedContentType: ComplainedContent.profile, + slug: widget.slug, ), anchor: Aligned( shiftToWithinBound: AxisFlag(x: true, y: true), @@ -395,11 +399,20 @@ class _UserDetailsContentState extends State { ), ), ); - } else { + } else if (state.error != null) { return Scaffold( - body: Loader(), + appBar: AppBar(), + body: DataErrorWidget( + onReload: () async { + context.read().getUserDetails(widget.slug); + }, + ), ); } + return Scaffold( + appBar: AppBar(), + body: Loader(), + ); }, ); } diff --git a/lib/generated/intl/messages_all.dart b/lib/generated/intl/messages_all.dart index 3139e159..abae25a5 100644 --- a/lib/generated/intl/messages_all.dart +++ b/lib/generated/intl/messages_all.dart @@ -39,10 +39,8 @@ MessageLookupByLibrary? _findExact(String localeName) { /// User programs should call this before using [localeName] for messages. Future initializeMessages(String localeName) { var availableLocale = Intl.verifiedLocale( - localeName, - (locale) => _deferredLibraries[locale] != null, - onFailure: (_) => null, - ); + localeName, (locale) => _deferredLibraries[locale] != null, + onFailure: (_) => null); if (availableLocale == null) { return new SynchronousFuture(false); } @@ -62,11 +60,8 @@ bool _messagesExistFor(String locale) { } MessageLookupByLibrary? _findGeneratedMessagesFor(String locale) { - var actualLocale = Intl.verifiedLocale( - locale, - _messagesExistFor, - onFailure: (_) => null, - ); + var actualLocale = + Intl.verifiedLocale(locale, _messagesExistFor, onFailure: (_) => null); if (actualLocale == null) return null; return _findExact(actualLocale); } diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart index 2bcea0e1..9b5fef07 100644 --- a/lib/generated/intl/messages_en.dart +++ b/lib/generated/intl/messages_en.dart @@ -22,381 +22,462 @@ class MessageLookup extends MessageLookupByLibrary { final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { - "Moscow": MessageLookupByLibrary.simpleMessage("Москва"), - "Russia": MessageLookupByLibrary.simpleMessage("Россия"), - "about_diet": MessageLookupByLibrary.simpleMessage("Об этой диете"), - "about_plan": MessageLookupByLibrary.simpleMessage("Об этом плане"), - "active": MessageLookupByLibrary.simpleMessage("Активен"), - "active_diet_state": MessageLookupByLibrary.simpleMessage("Активная"), - "add": MessageLookupByLibrary.simpleMessage("Добавить"), - "add_cover": MessageLookupByLibrary.simpleMessage("Добавьте обложку"), - "add_measure": MessageLookupByLibrary.simpleMessage("Добавить замер"), - "adding_plan": MessageLookupByLibrary.simpleMessage("Добавление плана"), - "additional_information": MessageLookupByLibrary.simpleMessage( - "Дополнительная информация", - ), - "admin": MessageLookupByLibrary.simpleMessage("Админ"), - "all": MessageLookupByLibrary.simpleMessage("Все"), - "allergies_presence": MessageLookupByLibrary.simpleMessage( - "Наличие аллергии", - ), - "another_form_of_comment": MessageLookupByLibrary.simpleMessage( - "комментария", - ), - "another_form_of_day": MessageLookupByLibrary.simpleMessage("дня"), - "answer": MessageLookupByLibrary.simpleMessage("Ответить"), - "archived": MessageLookupByLibrary.simpleMessage("В архиве"), - "authentificate": MessageLookupByLibrary.simpleMessage("Войти"), - "average_pressure": MessageLookupByLibrary.simpleMessage( - "Средние показатели давления:", - ), - "bodymass_chart": MessageLookupByLibrary.simpleMessage("График ИМТ"), - "by_default": MessageLookupByLibrary.simpleMessage("По умолчянию"), - "calories": MessageLookupByLibrary.simpleMessage("Калории"), - "cancel_diet": MessageLookupByLibrary.simpleMessage("Отменить диету?"), - "carb_type": MessageLookupByLibrary.simpleMessage("Мучное"), - "carb_type_desc": MessageLookupByLibrary.simpleMessage( - "Если вы любите мучные продукты, но хотите придерживаться диеты, вам подходит диета с добавленим мучных блюд на основе цельнозерновых вариантов. В них больше сложных углеводов и клетчатки, что способствует длительному чувству сытости и поддерживает здоровье пищеварительной системы.", - ), - "carbohydrates": MessageLookupByLibrary.simpleMessage("Углеводы"), - "chat": MessageLookupByLibrary.simpleMessage("Чат"), - "chest": MessageLookupByLibrary.simpleMessage("Грудь"), - "chest_circumference": MessageLookupByLibrary.simpleMessage("Обхват груди"), - "choose_diet_duration": MessageLookupByLibrary.simpleMessage( - "Выберите срок диеты", - ), - "choose_plan_duration": MessageLookupByLibrary.simpleMessage( - "Выберите срок плана", - ), - "choose_start_diet_date": MessageLookupByLibrary.simpleMessage( - "Выберите дату старты диеты. Она отобразиться у Вас в дневнике", - ), - "circumference_empty": MessageLookupByLibrary.simpleMessage( - "Все замеры обязательны к заполнению", - ), - "circumference_is_not_a_number": MessageLookupByLibrary.simpleMessage( - "Все замеры должны быть числами", - ), - "circumferences_chart": MessageLookupByLibrary.simpleMessage( - "График обхватов", - ), - "city": MessageLookupByLibrary.simpleMessage("Город"), - "city_of_living": MessageLookupByLibrary.simpleMessage("Город проживания"), - "comfort": MessageLookupByLibrary.simpleMessage("Комфорт"), - "comment": MessageLookupByLibrary.simpleMessage("комментарий"), - "comments": MessageLookupByLibrary.simpleMessage("Комментарии"), - "comments_plural": MessageLookupByLibrary.simpleMessage("комментариев"), - "complain": MessageLookupByLibrary.simpleMessage("Пожаловаться"), - "confirm": MessageLookupByLibrary.simpleMessage("Подтвердить"), - "continueButton": MessageLookupByLibrary.simpleMessage("Продолжить"), - "continue_": MessageLookupByLibrary.simpleMessage("Продолжить"), - "copied": MessageLookupByLibrary.simpleMessage("cкопирован"), - "copy_link": MessageLookupByLibrary.simpleMessage("Скопировать ссылку"), - "could_not_cancel_diet": MessageLookupByLibrary.simpleMessage( - "Не удалось отменить диету", - ), - "country": MessageLookupByLibrary.simpleMessage("Страна"), - "create_diet": MessageLookupByLibrary.simpleMessage("Создание диеты"), - "customer_price": MessageLookupByLibrary.simpleMessage( - "Цена для покупателя", - ), - "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_started": MessageLookupByLibrary.simpleMessage("Диета уже начата"), - "diet_state": MessageLookupByLibrary.simpleMessage("Статус диеты"), - "diet_type": MessageLookupByLibrary.simpleMessage("Тип диеты"), - "do_you_have_hypertension_tendency": MessageLookupByLibrary.simpleMessage( - "Есть ли у вас гипертония?", - ), - "do_you_have_hypotension_tendency": MessageLookupByLibrary.simpleMessage( - "Есть ли у вас гипотония?", - ), - "do_you_take_pills": MessageLookupByLibrary.simpleMessage( - "Принимаете ли вы лекарства или бады?", - ), - "down_pressure": MessageLookupByLibrary.simpleMessage( - "Нижние показатели давления:", - ), - "draft": MessageLookupByLibrary.simpleMessage("Черновик"), - "drafted": MessageLookupByLibrary.simpleMessage("Черновики"), - "eating_behavior": MessageLookupByLibrary.simpleMessage( - "Пищевое поведение", - ), - "eating_behavior_type": MessageLookupByLibrary.simpleMessage( - "Формирование пищевого поведения", - ), - "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( - "Необходимо ввести адрес электронной почты", - ), - "email_invalid": MessageLookupByLibrary.simpleMessage( - "Некорректный адрес электронной почты", - ), - "enter_additional_information": MessageLookupByLibrary.simpleMessage( - "Введите любую дополнительную информацию на своё усмотрение:", - ), - "enter_diet_description": MessageLookupByLibrary.simpleMessage( - "Введите описание диеты", - ), - "enter_diet_name": MessageLookupByLibrary.simpleMessage( - "Выведите название диеты", - ), - "enter_information": MessageLookupByLibrary.simpleMessage( - "Введите информацию...", - ), - "exit": MessageLookupByLibrary.simpleMessage("Выйти"), - "expand": MessageLookupByLibrary.simpleMessage("Показать полностью"), - "fats": MessageLookupByLibrary.simpleMessage("Жиры"), - "favorite_foods": MessageLookupByLibrary.simpleMessage("Любимые продукты"), - "favorite_unfavorite_foods": MessageLookupByLibrary.simpleMessage( - "Ваши любимые/нелюбимые продукты питания?", - ), - "female": MessageLookupByLibrary.simpleMessage("Женский"), - "fill_in_information": MessageLookupByLibrary.simpleMessage( - "Заполните информацию:", - ), - "finish": MessageLookupByLibrary.simpleMessage("Завершить"), - "first_name": MessageLookupByLibrary.simpleMessage("Имя"), - "first_name_empty": MessageLookupByLibrary.simpleMessage( - "Имя обязательно для заполнения", - ), - "first_name_short": MessageLookupByLibrary.simpleMessage( - "В имени должен быть минимум 1 символ", - ), - "fit_type": MessageLookupByLibrary.simpleMessage("Поддержание формы"), - "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("Галерея"), - "general_info": MessageLookupByLibrary.simpleMessage("Общая информация"), - "gr": MessageLookupByLibrary.simpleMessage("гр"), - "grams": MessageLookupByLibrary.simpleMessage("гр"), - "has_alergy": MessageLookupByLibrary.simpleMessage("Наличие аллергии"), - "has_hyper": MessageLookupByLibrary.simpleMessage( - "Склонность к гипертонии", - ), - "has_hypo": MessageLookupByLibrary.simpleMessage("Склонность к гипотонии"), - "height": MessageLookupByLibrary.simpleMessage("Рост"), - "height_measurements": MessageLookupByLibrary.simpleMessage("Замер роста"), - "hide": MessageLookupByLibrary.simpleMessage("Свернуть"), - "hip_circumference": MessageLookupByLibrary.simpleMessage("Обхват бедер"), - "hips": MessageLookupByLibrary.simpleMessage("Бедра"), - "in_progress": MessageLookupByLibrary.simpleMessage("В процессе"), - "inactive_diet_state": MessageLookupByLibrary.simpleMessage("Черновик"), - "initial_measurements": MessageLookupByLibrary.simpleMessage( - "Первичные замеры", - ), - "label": MessageLookupByLibrary.simpleMessage("Название"), - "language": MessageLookupByLibrary.simpleMessage("Язык"), - "last_name": MessageLookupByLibrary.simpleMessage("Фамилия"), - "last_name_empty": MessageLookupByLibrary.simpleMessage( - "Фамилия обязательна для заполнения", - ), - "last_name_short": MessageLookupByLibrary.simpleMessage( - "В фамилии должен быть минимум 6 символов", - ), - "lbl_english": MessageLookupByLibrary.simpleMessage("English"), - "light_type": MessageLookupByLibrary.simpleMessage("Легкая"), - "light_type_desc": MessageLookupByLibrary.simpleMessage( - "С некоторыми видами диет можно употреблять алкоголь в умеренных количествах. Рекомендуется совмещать вместе с низкокалорийными закусками.", - ), - "liked_groceries": MessageLookupByLibrary.simpleMessage("Любимые продукты"), - "link_was_copied": MessageLookupByLibrary.simpleMessage( - "Ссылка успешно скопирована", - ), - "loading": MessageLookupByLibrary.simpleMessage("Загрузка..."), - "lower": MessageLookupByLibrary.simpleMessage("Нижнее"), - "male": MessageLookupByLibrary.simpleMessage("Мужской"), - "meal": MessageLookupByLibrary.simpleMessage("Приём пищи"), - "meat": MessageLookupByLibrary.simpleMessage("Мясо"), - "meat_type": MessageLookupByLibrary.simpleMessage("С мясом"), - "meat_type_desc": MessageLookupByLibrary.simpleMessage( - "Диета с мясом полезная и сбалансированная. Включает в себя нежирное мясо: курицу, индейку, кролика или говядину. В сочетании с овощами получаете все необходимые витамины и минералы. Не забывайте о порциях и старайтесь есть медленно, прислушиваясь к своему организму.", - ), - "milliliter": MessageLookupByLibrary.simpleMessage("мл"), - "minimal_customer_price": MessageLookupByLibrary.simpleMessage( - "Минимальная цена для покупателя: 1200₽", - ), - "most_new": MessageLookupByLibrary.simpleMessage("Самые новые"), - "most_old": MessageLookupByLibrary.simpleMessage("Самые старые"), - "msg_network_err": MessageLookupByLibrary.simpleMessage("Сетевая ошибка!"), - "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( - "Имя пользователя обязательно для заполнения", - ), - "name_short": MessageLookupByLibrary.simpleMessage( - "В имени пользователя должно быть минимум 6 символов", - ), - "no": MessageLookupByLibrary.simpleMessage("Нет"), - "no_account": MessageLookupByLibrary.simpleMessage("У меня нет аккаунта"), - "not_liked_groceries": MessageLookupByLibrary.simpleMessage( - "Не любимые продукты", - ), - "one_option": MessageLookupByLibrary.simpleMessage("1 опция"), - "option": MessageLookupByLibrary.simpleMessage("Опция"), - "option_content": MessageLookupByLibrary.simpleMessage("Содержание опции"), - "option_content_hint_text": MessageLookupByLibrary.simpleMessage( - "На связи 7 дней в неделю", - ), - "option_desc": MessageLookupByLibrary.simpleMessage("Описание опции"), - "option_desc_hint_text": MessageLookupByLibrary.simpleMessage( - "Этот план питания для похудения на 7 дней сбалансированный и разнообразный", - ), - "option_duration": MessageLookupByLibrary.simpleMessage( - "Продолжительность опции", - ), - "option_personal_diet_desc": MessageLookupByLibrary.simpleMessage( - "Опция подразумевает создание индивидуальной диеты", - ), - "option_title": MessageLookupByLibrary.simpleMessage("Заголовок опции"), - "order": MessageLookupByLibrary.simpleMessage("Порядок"), - "other_info": MessageLookupByLibrary.simpleMessage("Прочая информация"), - "password": MessageLookupByLibrary.simpleMessage("Пароль"), - "password_confirmation": MessageLookupByLibrary.simpleMessage( - "Подтвердите пароль", - ), - "password_confirmation_mismatch": MessageLookupByLibrary.simpleMessage( - "Пароли не совпадают", - ), - "password_empty": MessageLookupByLibrary.simpleMessage( - "Пароль обязателен для заполнения", - ), - "password_short": MessageLookupByLibrary.simpleMessage( - "В пароле должно быть минимум 6 символов", - ), - "pcs": MessageLookupByLibrary.simpleMessage("шт"), - "personal": MessageLookupByLibrary.simpleMessage("Персональные"), - "personal_diet_state": MessageLookupByLibrary.simpleMessage("Персональная"), - "photo": MessageLookupByLibrary.simpleMessage("Фото"), - "physical_fitness": MessageLookupByLibrary.simpleMessage( - "Физическая форма", - ), - "place_of_living": MessageLookupByLibrary.simpleMessage("Место проживания"), - "plan_author": MessageLookupByLibrary.simpleMessage("Автор плана"), - "plan_desc": MessageLookupByLibrary.simpleMessage("Описание плана"), - "plan_desc_example": MessageLookupByLibrary.simpleMessage( - "Большое подробное, привлекательное описание плана", - ), - "plan_duration": MessageLookupByLibrary.simpleMessage("Срок плана"), - "plan_label_desc": MessageLookupByLibrary.simpleMessage( - "План для поддержания формы", - ), - "plan_type": MessageLookupByLibrary.simpleMessage("Тип плана"), - "plans_complete": MessageLookupByLibrary.simpleMessage("Пройденные планы"), - "popular": MessageLookupByLibrary.simpleMessage("Популярные"), - "preferred_language": MessageLookupByLibrary.simpleMessage( - "Предпочитаемый язык общения", - ), - "pressure": MessageLookupByLibrary.simpleMessage("Давление"), - "pressure_empty": MessageLookupByLibrary.simpleMessage( - "Все показатели давления обязательны к заполнению", - ), - "pressure_is_not_a_number": MessageLookupByLibrary.simpleMessage( - "Все показатели давления должны быть числами", - ), - "preview": MessageLookupByLibrary.simpleMessage("Превью"), - "price": MessageLookupByLibrary.simpleMessage("Цена"), - "price_explanation": MessageLookupByLibrary.simpleMessage( - "Разница в ценах, это комиссия Итвио", - ), - "profile": MessageLookupByLibrary.simpleMessage("Профиль"), - "proteins": MessageLookupByLibrary.simpleMessage("Белки"), - "publish": MessageLookupByLibrary.simpleMessage("Опубликовать"), - "published": MessageLookupByLibrary.simpleMessage("Опубликованные"), - "removeFromActive": MessageLookupByLibrary.simpleMessage( - "Снять с публикации", - ), - "save": MessageLookupByLibrary.simpleMessage("Сохранить"), - "services": MessageLookupByLibrary.simpleMessage("Оказание услуг"), - "settings": MessageLookupByLibrary.simpleMessage("Настройки"), - "sex": MessageLookupByLibrary.simpleMessage("Пол"), - "share": MessageLookupByLibrary.simpleMessage("Поделиться"), - "skip": MessageLookupByLibrary.simpleMessage("Пропустить"), - "slim": MessageLookupByLibrary.simpleMessage("Похудение"), - "slim_type": MessageLookupByLibrary.simpleMessage("Похудение"), - "slim_type_desc": MessageLookupByLibrary.simpleMessage( - "План питания для похудения должен быть разработан с учётом индивидуальных особенностей организма, возраста, пола, уровня физической активности и состояния здоровья. Общие принципы: \n● Снижение калорийности; \n● Сбалансированный рацион; \n● Разнообразие продуктов; \n● Питьевой режим; \n● Ограничение вредных продуктов;", - ), - "sport_hobbies": MessageLookupByLibrary.simpleMessage( - "Спортивные увлечения", - ), - "standard": MessageLookupByLibrary.simpleMessage("Стандарт"), - "startDiet": MessageLookupByLibrary.simpleMessage("Начать диету"), - "takes_pills": MessageLookupByLibrary.simpleMessage("Принимает лекарства"), - "tell_your_schedule": MessageLookupByLibrary.simpleMessage( - "Расскажите в свободной форме о том как просыпаетесь, когда едите, когда ложитесь спать:", - ), - "three_options": MessageLookupByLibrary.simpleMessage("3 опции"), - "time": MessageLookupByLibrary.simpleMessage("Время прохождения"), - "toOffer": MessageLookupByLibrary.simpleMessage("Предложить"), - "to_draft": MessageLookupByLibrary.simpleMessage("В черновик"), - "unfavorite_foods": MessageLookupByLibrary.simpleMessage( - "Нелюбимые продукты", - ), - "up_pressure": MessageLookupByLibrary.simpleMessage( - "Верхние показатели давления:", - ), - "upper": MessageLookupByLibrary.simpleMessage("Верхнее"), - "user_authorization": MessageLookupByLibrary.simpleMessage( - "Аутентификация", - ), - "user_data": MessageLookupByLibrary.simpleMessage("Данные пользователя"), - "user_registration": MessageLookupByLibrary.simpleMessage( - "Регистрация пользователя", - ), - "username": MessageLookupByLibrary.simpleMessage("Имя пользователя"), - "vegan": MessageLookupByLibrary.simpleMessage("Веган."), - "vegan_type": MessageLookupByLibrary.simpleMessage("Вегетарианская"), - "vegan_type_desc": MessageLookupByLibrary.simpleMessage( - "Диета основана на потреблении растительной пищи и отказе от продуктов животного происхождения. Такой тип питания может быть разнообразным и вкусным, а также полезным для здоровья.", - ), - "waist": MessageLookupByLibrary.simpleMessage("Талия"), - "waist_circumference": MessageLookupByLibrary.simpleMessage("Обхват талии"), - "weight": MessageLookupByLibrary.simpleMessage("Вес"), - "weight_chart": MessageLookupByLibrary.simpleMessage("График веса"), - "weight_measurements": MessageLookupByLibrary.simpleMessage("Замер веса"), - "wellness": MessageLookupByLibrary.simpleMessage("Улучшение самочувствия"), - "wellness_type": MessageLookupByLibrary.simpleMessage( - "Улучшение самочувствия", - ), - "wellness_type_desc": MessageLookupByLibrary.simpleMessage( - "Если вы любите мучные продукты, но хотите придерживаться диеты, вам подходит диета с добавленим мучных блюд на основе цельнозерновых вариантов. В них больше сложных углеводов и клетчатки, что способствует длительному чувству сытости и поддерживает здоровье пищеварительной системы.", - ), - "what_is_your_name": MessageLookupByLibrary.simpleMessage("Как вас зовут?"), - "wrong_credentials": MessageLookupByLibrary.simpleMessage( - "Неверные данные пользователя", - ), - "you_will_get": MessageLookupByLibrary.simpleMessage("Вы получите"), - "your_blood_pressure": MessageLookupByLibrary.simpleMessage( - "Ваше артериальное давление?", - ), - "your_favorite_sports": MessageLookupByLibrary.simpleMessage( - "Ваши спортивные увлечения?", - ), - "your_gender": MessageLookupByLibrary.simpleMessage("Ваш пол?"), - "your_height": MessageLookupByLibrary.simpleMessage("Ваш рост:"), - "your_schedule": MessageLookupByLibrary.simpleMessage("Ваш режим дня?"), - "your_weight": MessageLookupByLibrary.simpleMessage("Ваш вес:"), - }; + "Moscow": MessageLookupByLibrary.simpleMessage("Москва"), + "Russia": MessageLookupByLibrary.simpleMessage("Россия"), + "about_diet": MessageLookupByLibrary.simpleMessage("Об этой диете"), + "about_plan": MessageLookupByLibrary.simpleMessage("Об этом плане"), + "about_recipe": MessageLookupByLibrary.simpleMessage("Об этом рецепте"), + "active": MessageLookupByLibrary.simpleMessage("Активен"), + "active_consultations_count": MessageLookupByLibrary.simpleMessage( + "Кол-во активных консультаций"), + "active_diet_state": MessageLookupByLibrary.simpleMessage("Активная"), + "add": MessageLookupByLibrary.simpleMessage("Добавить"), + "add_cover": MessageLookupByLibrary.simpleMessage("Добавьте обложку"), + "add_measure": MessageLookupByLibrary.simpleMessage("Добавить замер"), + "adding_plan": MessageLookupByLibrary.simpleMessage("Добавление плана"), + "additional_information": + MessageLookupByLibrary.simpleMessage("Дополнительная информация"), + "admin": MessageLookupByLibrary.simpleMessage("Админ"), + "alcohol": MessageLookupByLibrary.simpleMessage("Алкоголь"), + "all": MessageLookupByLibrary.simpleMessage("Все"), + "allergies_presence": + MessageLookupByLibrary.simpleMessage("Наличие аллергии"), + "another_form_of_comment": + MessageLookupByLibrary.simpleMessage("комментария"), + "another_form_of_day": MessageLookupByLibrary.simpleMessage("дня"), + "another_form_of_hour": MessageLookupByLibrary.simpleMessage("часа"), + "another_form_of_minute": MessageLookupByLibrary.simpleMessage("минут"), + "another_form_of_stage": MessageLookupByLibrary.simpleMessage("этапа"), + "answer": MessageLookupByLibrary.simpleMessage("Ответить"), + "archived": MessageLookupByLibrary.simpleMessage("В архиве"), + "authentificate": MessageLookupByLibrary.simpleMessage("Войти"), + "average_pressure": MessageLookupByLibrary.simpleMessage( + "Средние показатели давления:"), + "bodymass_chart": MessageLookupByLibrary.simpleMessage("График ИМТ"), + "by_default": MessageLookupByLibrary.simpleMessage("По умолчянию"), + "calls_for_bullying": + MessageLookupByLibrary.simpleMessage("Призывы к травле"), + "calories": MessageLookupByLibrary.simpleMessage("Калории"), + "can_not_load_data_please_try_again": + MessageLookupByLibrary.simpleMessage( + "Не удалось загрузить данные. Попробуйте обновить страницу"), + "cancel_diet": MessageLookupByLibrary.simpleMessage("Отменить диету?"), + "carb_type": MessageLookupByLibrary.simpleMessage("Мучное"), + "carb_type_desc": MessageLookupByLibrary.simpleMessage( + "Если вы любите мучные продукты, но хотите придерживаться диеты, вам подходит диета с добавленим мучных блюд на основе цельнозерновых вариантов. В них больше сложных углеводов и клетчатки, что способствует длительному чувству сытости и поддерживает здоровье пищеварительной системы."), + "carbohydrates": MessageLookupByLibrary.simpleMessage("Углеводы"), + "chat": MessageLookupByLibrary.simpleMessage("Чат"), + "check_out_video_recipe": + MessageLookupByLibrary.simpleMessage("Посмотреть видео рецепт"), + "chest": MessageLookupByLibrary.simpleMessage("Грудь"), + "chest_circumference": + MessageLookupByLibrary.simpleMessage("Обхват груди"), + "choose_diet_duration": + MessageLookupByLibrary.simpleMessage("Выберите срок диеты"), + "choose_plan_duration": + MessageLookupByLibrary.simpleMessage("Выберите срок плана"), + "choose_start_diet_date": MessageLookupByLibrary.simpleMessage( + "Выберите дату старты диеты. Она отобразиться у Вас в дневнике"), + "circumference_empty": MessageLookupByLibrary.simpleMessage( + "Все замеры обязательны к заполнению"), + "circumference_is_not_a_number": MessageLookupByLibrary.simpleMessage( + "Все замеры должны быть числами"), + "circumferences_chart": + MessageLookupByLibrary.simpleMessage("График обхватов"), + "city": MessageLookupByLibrary.simpleMessage("Город"), + "city_of_living": + MessageLookupByLibrary.simpleMessage("Город проживания"), + "comfort": MessageLookupByLibrary.simpleMessage("Комфорт"), + "comment": MessageLookupByLibrary.simpleMessage("комментарий"), + "comments": MessageLookupByLibrary.simpleMessage("Комментарии"), + "comments_plural": MessageLookupByLibrary.simpleMessage("комментариев"), + "complain": MessageLookupByLibrary.simpleMessage("Пожаловаться"), + "complaint": MessageLookupByLibrary.simpleMessage("Жалоба"), + "complaint_question": MessageLookupByLibrary.simpleMessage( + "Что именно в этом материале Вы считаете неприемлемым и нарушающим правила?"), + "complaint_sent": + MessageLookupByLibrary.simpleMessage("Жалоба успешно отправлена"), + "complaint_sent_description": MessageLookupByLibrary.simpleMessage( + "Ваше обращение принято! Мы рассмотрим его в ближайшее время и примем необходимые меры. Спасибо за вашу бдительность и помощь в улучшении нашего сервиса."), + "confirm": MessageLookupByLibrary.simpleMessage("Подтвердить"), + "consultations_count": MessageLookupByLibrary.simpleMessage( + "Кол-во проведенных консулдьтаций"), + "continueButton": MessageLookupByLibrary.simpleMessage("Продолжить"), + "continue_": MessageLookupByLibrary.simpleMessage("Продолжить"), + "copied": MessageLookupByLibrary.simpleMessage("cкопирован"), + "copy_link": MessageLookupByLibrary.simpleMessage("Скопировать ссылку"), + "copy_referral_link": MessageLookupByLibrary.simpleMessage( + "Скопировать пригласительную ссылку"), + "could_not_cancel_diet": + MessageLookupByLibrary.simpleMessage("Не удалось отменить диету"), + "could_not_delete_blog_post": + MessageLookupByLibrary.simpleMessage("Не удалось удалить пост"), + "could_not_launch_video": + MessageLookupByLibrary.simpleMessage("Не удалось запустить видео"), + "could_not_publish_blog_post": MessageLookupByLibrary.simpleMessage( + "Не удалось опубликовать пост"), + "could_not_publish_diet": MessageLookupByLibrary.simpleMessage( + "Не удалось опубликовать диету"), + "could_not_publish_recipe": MessageLookupByLibrary.simpleMessage( + "Не удалось опубликовать рецепт"), + "could_not_send_complain": + MessageLookupByLibrary.simpleMessage("Не удалось отправить жалобу"), + "could_not_update_blog_post": + MessageLookupByLibrary.simpleMessage("Не удалось обновить пост"), + "could_not_update_diet": + MessageLookupByLibrary.simpleMessage("Не удалось обновить диету"), + "country": MessageLookupByLibrary.simpleMessage("Страна"), + "create_diet": MessageLookupByLibrary.simpleMessage("Создание диеты"), + "customer_price": + MessageLookupByLibrary.simpleMessage("Цена для покупателя"), + "date_of_birth": MessageLookupByLibrary.simpleMessage("Дата рождения"), + "day": MessageLookupByLibrary.simpleMessage("День"), + "days": MessageLookupByLibrary.simpleMessage("дней"), + "deception": MessageLookupByLibrary.simpleMessage("Обман"), + "deleted": MessageLookupByLibrary.simpleMessage("Удалена"), + "diary": MessageLookupByLibrary.simpleMessage("Дневник"), + "diet": MessageLookupByLibrary.simpleMessage("Диета"), + "diet_author": MessageLookupByLibrary.simpleMessage("Автор диеты"), + "diet_has_been_sent_to_draft": MessageLookupByLibrary.simpleMessage( + "снята с публикации и перенесена в черновик"), + "diet_plan": MessageLookupByLibrary.simpleMessage("План диеты"), + "diet_started": + MessageLookupByLibrary.simpleMessage("Диета уже начата"), + "diet_state": MessageLookupByLibrary.simpleMessage("Статус диеты"), + "diet_type": MessageLookupByLibrary.simpleMessage("Тип диеты"), + "do_you_have_hypertension_tendency": + MessageLookupByLibrary.simpleMessage("Есть ли у вас гипертония?"), + "do_you_have_hypotension_tendency": + MessageLookupByLibrary.simpleMessage("Есть ли у вас гипотония?"), + "do_you_take_pills": MessageLookupByLibrary.simpleMessage( + "Принимаете ли вы лекарства или бады?"), + "down_pressure": + MessageLookupByLibrary.simpleMessage("Нижние показатели давления:"), + "draft": MessageLookupByLibrary.simpleMessage("Черновик"), + "drafted": MessageLookupByLibrary.simpleMessage("Черновики"), + "drugs": MessageLookupByLibrary.simpleMessage("Наркотики"), + "eating_behavior": + MessageLookupByLibrary.simpleMessage("Пищевое поведение"), + "eating_behavior_type": MessageLookupByLibrary.simpleMessage( + "Формирование пищевого поведения"), + "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( + "Необходимо ввести адрес электронной почты"), + "email_invalid": MessageLookupByLibrary.simpleMessage( + "Некорректный адрес электронной почты"), + "enter_additional_information": MessageLookupByLibrary.simpleMessage( + "Введите любую дополнительную информацию на своё усмотрение:"), + "enter_diet_description": + MessageLookupByLibrary.simpleMessage("Введите описание диеты"), + "enter_diet_name": + MessageLookupByLibrary.simpleMessage("Выведите название диеты"), + "enter_information": + MessageLookupByLibrary.simpleMessage("Введите информацию..."), + "exit": MessageLookupByLibrary.simpleMessage("Выйти"), + "expand": MessageLookupByLibrary.simpleMessage("Показать полностью"), + "explicitImage": + MessageLookupByLibrary.simpleMessage("Откровенное изображение"), + "explicitImages": + MessageLookupByLibrary.simpleMessage("Откровенные изображения"), + "explicitImagesDescription": MessageLookupByLibrary.simpleMessage( + "Отправьте жалобу, если кто-то разместил откровенные изображения"), + "extremism": MessageLookupByLibrary.simpleMessage("Экстремизм"), + "fake_documents": + MessageLookupByLibrary.simpleMessage("Поддельные документы"), + "fats": MessageLookupByLibrary.simpleMessage("Жиры"), + "favorite_foods": + MessageLookupByLibrary.simpleMessage("Любимые продукты"), + "favorite_unfavorite_foods": MessageLookupByLibrary.simpleMessage( + "Ваши любимые/нелюбимые продукты питания?"), + "female": MessageLookupByLibrary.simpleMessage("Женский"), + "fill_in_information": + MessageLookupByLibrary.simpleMessage("Заполните информацию:"), + "finish": MessageLookupByLibrary.simpleMessage("Завершить"), + "first_name": MessageLookupByLibrary.simpleMessage("Имя"), + "first_name_empty": MessageLookupByLibrary.simpleMessage( + "Имя обязательно для заполнения"), + "first_name_short": MessageLookupByLibrary.simpleMessage( + "В имени должен быть минимум 1 символ"), + "fit_type": MessageLookupByLibrary.simpleMessage("Поддержание формы"), + "fit_type_desc": MessageLookupByLibrary.simpleMessage( + "План питания для поддержания формы сбалансированный и разнообразный, чтобы обеспечить организм всеми необходимыми питательными веществами. Общие принципы: \n● Дробное питание; \n● Разнообразие продуктов; \n● Контроль порций; \n● Питьевой режим; \n● Ограничение вредных продуктов;"), + "fitness": MessageLookupByLibrary.simpleMessage("Поддержка формы"), + "for_a_month": MessageLookupByLibrary.simpleMessage("За месяц"), + "for_a_week": MessageLookupByLibrary.simpleMessage("За неделю"), + "for_a_year": MessageLookupByLibrary.simpleMessage("За год"), + "for_whole_period_of_time": + MessageLookupByLibrary.simpleMessage("За всё время"), + "fraud": MessageLookupByLibrary.simpleMessage("Мошенничество"), + "fraud_description": MessageLookupByLibrary.simpleMessage( + "Мошенничество с целью наживы. Если вас обманули, пожалуйста, сообщите об этом в правоохранительные органы."), + "from": MessageLookupByLibrary.simpleMessage("от"), + "from_a_to_z": MessageLookupByLibrary.simpleMessage("Название: А-Я"), + "from_z_to_a": MessageLookupByLibrary.simpleMessage("Название: Я-А"), + "gallery": MessageLookupByLibrary.simpleMessage("Галерея"), + "general_info": + MessageLookupByLibrary.simpleMessage("Общая информация"), + "gr": MessageLookupByLibrary.simpleMessage("гр"), + "grams": MessageLookupByLibrary.simpleMessage("гр"), + "has_alergy": MessageLookupByLibrary.simpleMessage("Наличие аллергии"), + "has_hyper": + MessageLookupByLibrary.simpleMessage("Склонность к гипертонии"), + "has_hypo": + MessageLookupByLibrary.simpleMessage("Склонность к гипотонии"), + "hatred": MessageLookupByLibrary.simpleMessage("Оскорбление"), + "hatred_description": + MessageLookupByLibrary.simpleMessage("Враждебные высказывания"), + "height": MessageLookupByLibrary.simpleMessage("Рост"), + "height_measurements": + MessageLookupByLibrary.simpleMessage("Замер роста"), + "hide": MessageLookupByLibrary.simpleMessage("Свернуть"), + "hip_circumference": + MessageLookupByLibrary.simpleMessage("Обхват бедер"), + "hips": MessageLookupByLibrary.simpleMessage("Бедра"), + "hour": MessageLookupByLibrary.simpleMessage("час"), + "hours": MessageLookupByLibrary.simpleMessage("часов"), + "in_progress": MessageLookupByLibrary.simpleMessage("В процессе"), + "inactive_diet_state": MessageLookupByLibrary.simpleMessage("Черновик"), + "ingredients": MessageLookupByLibrary.simpleMessage("Ингредиенты"), + "initial_measurements": + MessageLookupByLibrary.simpleMessage("Первичные замеры"), + "invited_people_count": + MessageLookupByLibrary.simpleMessage("Количество приглашенных"), + "label": MessageLookupByLibrary.simpleMessage("Название"), + "language": MessageLookupByLibrary.simpleMessage("Язык"), + "last_name": MessageLookupByLibrary.simpleMessage("Фамилия"), + "last_name_empty": MessageLookupByLibrary.simpleMessage( + "Фамилия обязательна для заполнения"), + "last_name_short": MessageLookupByLibrary.simpleMessage( + "В фамилии должен быть минимум 6 символов"), + "lawViolation": + MessageLookupByLibrary.simpleMessage("Нарушение законодательства"), + "lbl_english": MessageLookupByLibrary.simpleMessage("English"), + "light_type": MessageLookupByLibrary.simpleMessage("Легкая"), + "light_type_desc": MessageLookupByLibrary.simpleMessage( + "С некоторыми видами диет можно употреблять алкоголь в умеренных количествах. Рекомендуется совмещать вместе с низкокалорийными закусками."), + "liked_groceries": + MessageLookupByLibrary.simpleMessage("Любимые продукты"), + "link_was_copied": + MessageLookupByLibrary.simpleMessage("Ссылка успешно скопирована"), + "loading": MessageLookupByLibrary.simpleMessage("Загрузка..."), + "lower": MessageLookupByLibrary.simpleMessage("Нижнее"), + "male": MessageLookupByLibrary.simpleMessage("Мужской"), + "meal": MessageLookupByLibrary.simpleMessage("Приём пищи"), + "meat": MessageLookupByLibrary.simpleMessage("Мясо"), + "meat_type": MessageLookupByLibrary.simpleMessage("С мясом"), + "meat_type_desc": MessageLookupByLibrary.simpleMessage( + "Диета с мясом полезная и сбалансированная. Включает в себя нежирное мясо: курицу, индейку, кролика или говядину. В сочетании с овощами получаете все необходимые витамины и минералы. Не забывайте о порциях и старайтесь есть медленно, прислушиваясь к своему организму."), + "milliliter": MessageLookupByLibrary.simpleMessage("мл"), + "minimal_customer_price": MessageLookupByLibrary.simpleMessage( + "Минимальная цена для покупателя: 1200₽"), + "minute": MessageLookupByLibrary.simpleMessage("минута"), + "minutes": MessageLookupByLibrary.simpleMessage("минуты"), + "misrepresentation": + MessageLookupByLibrary.simpleMessage("Введение в заблуждение"), + "misrepresentation_description": MessageLookupByLibrary.simpleMessage( + "Новости, которые могут вызвать широкий резонанс или несут угрозу, распространяемые с целью введения людей в заблуждение."), + "most_new": MessageLookupByLibrary.simpleMessage("Самые новые"), + "most_old": MessageLookupByLibrary.simpleMessage("Самые старые"), + "msg_network_err": + MessageLookupByLibrary.simpleMessage("Сетевая ошибка!"), + "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( + "Имя пользователя обязательно для заполнения"), + "name_short": MessageLookupByLibrary.simpleMessage( + "В имени пользователя должно быть минимум 6 символов"), + "nicotine": MessageLookupByLibrary.simpleMessage("Никотин"), + "no": MessageLookupByLibrary.simpleMessage("Нет"), + "no_account": + MessageLookupByLibrary.simpleMessage("У меня нет аккаунта"), + "not_liked_groceries": + MessageLookupByLibrary.simpleMessage("Не любимые продукты"), + "ok": MessageLookupByLibrary.simpleMessage("Хорошо"), + "one_option": MessageLookupByLibrary.simpleMessage("1 опция"), + "option": MessageLookupByLibrary.simpleMessage("Опция"), + "option_content": + MessageLookupByLibrary.simpleMessage("Содержание опции"), + "option_content_hint_text": + MessageLookupByLibrary.simpleMessage("На связи 7 дней в неделю"), + "option_desc": MessageLookupByLibrary.simpleMessage("Описание опции"), + "option_desc_hint_text": MessageLookupByLibrary.simpleMessage( + "Этот план питания для похудения на 7 дней сбалансированный и разнообразный"), + "option_duration": + MessageLookupByLibrary.simpleMessage("Продолжительность опции"), + "option_personal_diet_desc": MessageLookupByLibrary.simpleMessage( + "Опция подразумевает создание индивидуальной диеты"), + "option_title": MessageLookupByLibrary.simpleMessage("Заголовок опции"), + "order": MessageLookupByLibrary.simpleMessage("Порядок"), + "other": MessageLookupByLibrary.simpleMessage("Другое"), + "other_info": MessageLookupByLibrary.simpleMessage("Прочая информация"), + "password": MessageLookupByLibrary.simpleMessage("Пароль"), + "password_confirmation": + MessageLookupByLibrary.simpleMessage("Подтвердите пароль"), + "password_confirmation_mismatch": + MessageLookupByLibrary.simpleMessage("Пароли не совпадают"), + "password_empty": MessageLookupByLibrary.simpleMessage( + "Пароль обязателен для заполнения"), + "password_short": MessageLookupByLibrary.simpleMessage( + "В пароле должно быть минимум 6 символов"), + "pcs": MessageLookupByLibrary.simpleMessage("шт"), + "personal": MessageLookupByLibrary.simpleMessage("Персональные"), + "personal_diet_state": + MessageLookupByLibrary.simpleMessage("Персональная"), + "photo": MessageLookupByLibrary.simpleMessage("Фото"), + "physical_fitness": + MessageLookupByLibrary.simpleMessage("Физическая форма"), + "place_of_living": + MessageLookupByLibrary.simpleMessage("Место проживания"), + "plan_author": MessageLookupByLibrary.simpleMessage("Автор плана"), + "plan_desc": MessageLookupByLibrary.simpleMessage("Описание плана"), + "plan_desc_example": MessageLookupByLibrary.simpleMessage( + "Большое подробное, привлекательное описание плана"), + "plan_duration": MessageLookupByLibrary.simpleMessage("Срок плана"), + "plan_label_desc": + MessageLookupByLibrary.simpleMessage("План для поддержания формы"), + "plan_type": MessageLookupByLibrary.simpleMessage("Тип плана"), + "plans_complete": + MessageLookupByLibrary.simpleMessage("Пройденные планы"), + "please_try_again_later": MessageLookupByLibrary.simpleMessage( + "Пожалуйста, попробуйте еще раз позже"), + "popular": MessageLookupByLibrary.simpleMessage("Популярные"), + "preferred_language": + MessageLookupByLibrary.simpleMessage("Предпочитаемый язык общения"), + "pressure": MessageLookupByLibrary.simpleMessage("Давление"), + "pressure_empty": MessageLookupByLibrary.simpleMessage( + "Все показатели давления обязательны к заполнению"), + "pressure_is_not_a_number": MessageLookupByLibrary.simpleMessage( + "Все показатели давления должны быть числами"), + "preview": MessageLookupByLibrary.simpleMessage("Превью"), + "price": MessageLookupByLibrary.simpleMessage("Цена"), + "price_explanation": MessageLookupByLibrary.simpleMessage( + "Разница в ценах, это комиссия Итвио"), + "profile": MessageLookupByLibrary.simpleMessage("Профиль"), + "prostitution": MessageLookupByLibrary.simpleMessage("Проституция"), + "proteins": MessageLookupByLibrary.simpleMessage("Белки"), + "publish": MessageLookupByLibrary.simpleMessage("Опубликовать"), + "published": MessageLookupByLibrary.simpleMessage("Опубликованные"), + "published_diets_count": + MessageLookupByLibrary.simpleMessage("Кол-во опубликованных диет"), + "published_posts_count": MessageLookupByLibrary.simpleMessage( + "Количество опубликованных постов"), + "published_recipes_count": MessageLookupByLibrary.simpleMessage( + "Кол-во опубликованных рецептов"), + "rating_and_statistics": + MessageLookupByLibrary.simpleMessage("Рейтинг и статистика"), + "rating_history": + MessageLookupByLibrary.simpleMessage("Начисление рейтинга"), + "recipe_author": MessageLookupByLibrary.simpleMessage("Автор рецепта"), + "recipe_publishing": + MessageLookupByLibrary.simpleMessage("Публикация рецепта"), + "removeFromActive": + MessageLookupByLibrary.simpleMessage("Снять с публикации"), + "save": MessageLookupByLibrary.simpleMessage("Сохранить"), + "send_complain": + MessageLookupByLibrary.simpleMessage("Отправить жалобу"), + "services": MessageLookupByLibrary.simpleMessage("Оказание услуг"), + "settings": MessageLookupByLibrary.simpleMessage("Настройки"), + "sex": MessageLookupByLibrary.simpleMessage("Пол"), + "share": MessageLookupByLibrary.simpleMessage("Поделиться"), + "skip": MessageLookupByLibrary.simpleMessage("Пропустить"), + "slim": MessageLookupByLibrary.simpleMessage("Похудение"), + "slim_type": MessageLookupByLibrary.simpleMessage("Похудение"), + "slim_type_desc": MessageLookupByLibrary.simpleMessage( + "План питания для похудения должен быть разработан с учётом индивидуальных особенностей организма, возраста, пола, уровня физической активности и состояния здоровья. Общие принципы: \n● Снижение калорийности; \n● Сбалансированный рацион; \n● Разнообразие продуктов; \n● Питьевой режим; \n● Ограничение вредных продуктов;"), + "some_error_occurred": + MessageLookupByLibrary.simpleMessage("Упс! Произошла ошибка"), + "some_error_occurred_description": MessageLookupByLibrary.simpleMessage( + "Произошла ошибка. Мы уже работаем над ее устранением. Попробуйте позже."), + "something_went_wrong": + MessageLookupByLibrary.simpleMessage("Упс! Что-то пошло не так"), + "spam": MessageLookupByLibrary.simpleMessage("Спам"), + "spam_description": MessageLookupByLibrary.simpleMessage( + "Отправьте жалобу, если Вы заметили рекламу, размещенную в не предназначенном для этого месте."), + "sport_hobbies": + MessageLookupByLibrary.simpleMessage("Спортивные увлечения"), + "stage": MessageLookupByLibrary.simpleMessage("этап"), + "stages": MessageLookupByLibrary.simpleMessage("этапов"), + "standard": MessageLookupByLibrary.simpleMessage("Стандарт"), + "startDiet": MessageLookupByLibrary.simpleMessage("Начать диету"), + "statistic": MessageLookupByLibrary.simpleMessage("Статистика"), + "step": MessageLookupByLibrary.simpleMessage("Шаг"), + "step_by_step_photo_recipe": + MessageLookupByLibrary.simpleMessage("Пошаговый фоторецепт"), + "successfully_published": + MessageLookupByLibrary.simpleMessage("успешно опубликован"), + "suicide": + MessageLookupByLibrary.simpleMessage("Склонение к самоубийству"), + "takes_pills": + MessageLookupByLibrary.simpleMessage("Принимает лекарства"), + "tell_your_schedule": MessageLookupByLibrary.simpleMessage( + "Расскажите в свободной форме о том как просыпаетесь, когда едите, когда ложитесь спать:"), + "there_is_a_video": MessageLookupByLibrary.simpleMessage("Есть видео"), + "three_options": MessageLookupByLibrary.simpleMessage("3 опции"), + "time": MessageLookupByLibrary.simpleMessage("Время прохождения"), + "toOffer": MessageLookupByLibrary.simpleMessage("Предложить"), + "to_draft": MessageLookupByLibrary.simpleMessage("В черновик"), + "try_again": MessageLookupByLibrary.simpleMessage("Попробовать снова"), + "type_here_your_complaint": + MessageLookupByLibrary.simpleMessage("Напишите здесь свою жалобу"), + "unfavorite_foods": + MessageLookupByLibrary.simpleMessage("Нелюбимые продукты"), + "up_pressure": MessageLookupByLibrary.simpleMessage( + "Верхние показатели давления:"), + "upper": MessageLookupByLibrary.simpleMessage("Верхнее"), + "user_authorization": + MessageLookupByLibrary.simpleMessage("Аутентификация"), + "user_data": + MessageLookupByLibrary.simpleMessage("Данные пользователя"), + "user_registration": + MessageLookupByLibrary.simpleMessage("Регистрация пользователя"), + "username": MessageLookupByLibrary.simpleMessage("Имя пользователя"), + "vegan": MessageLookupByLibrary.simpleMessage("Веган."), + "vegan_type": MessageLookupByLibrary.simpleMessage("Вегетарианская"), + "vegan_type_desc": MessageLookupByLibrary.simpleMessage( + "Диета основана на потреблении растительной пищи и отказе от продуктов животного происхождения. Такой тип питания может быть разнообразным и вкусным, а также полезным для здоровья."), + "violenceAndHostility": + MessageLookupByLibrary.simpleMessage("Насилие и вражда"), + "violence_against_people_or_animal": + MessageLookupByLibrary.simpleMessage( + "Насилие над людьми или животными"), + "waist": MessageLookupByLibrary.simpleMessage("Талия"), + "waist_circumference": + MessageLookupByLibrary.simpleMessage("Обхват талии"), + "weapon": MessageLookupByLibrary.simpleMessage("Оружие"), + "weight": MessageLookupByLibrary.simpleMessage("Вес"), + "weight_chart": MessageLookupByLibrary.simpleMessage("График веса"), + "weight_measurements": + MessageLookupByLibrary.simpleMessage("Замер веса"), + "wellness": + MessageLookupByLibrary.simpleMessage("Улучшение самочувствия"), + "wellness_type": + MessageLookupByLibrary.simpleMessage("Улучшение самочувствия"), + "wellness_type_desc": MessageLookupByLibrary.simpleMessage( + "Если вы любите мучные продукты, но хотите придерживаться диеты, вам подходит диета с добавленим мучных блюд на основе цельнозерновых вариантов. В них больше сложных углеводов и клетчатки, что способствует длительному чувству сытости и поддерживает здоровье пищеварительной системы."), + "what_do_you_want_to_complaint_about": + MessageLookupByLibrary.simpleMessage( + "На что Вы хотите пожаловаться?"), + "what_is_your_name": + MessageLookupByLibrary.simpleMessage("Как вас зовут?"), + "wrong_credentials": MessageLookupByLibrary.simpleMessage( + "Неверные данные пользователя"), + "you_will_get": MessageLookupByLibrary.simpleMessage("Вы получите"), + "your_blood_pressure": + MessageLookupByLibrary.simpleMessage("Ваше артериальное давление?"), + "your_favorite_sports": + MessageLookupByLibrary.simpleMessage("Ваши спортивные увлечения?"), + "your_gender": MessageLookupByLibrary.simpleMessage("Ваш пол?"), + "your_height": MessageLookupByLibrary.simpleMessage("Ваш рост:"), + "your_rating": MessageLookupByLibrary.simpleMessage("Ваш рейтинг"), + "your_schedule": MessageLookupByLibrary.simpleMessage("Ваш режим дня?"), + "your_weight": MessageLookupByLibrary.simpleMessage("Ваш вес:") + }; } diff --git a/lib/generated/intl/messages_ru.dart b/lib/generated/intl/messages_ru.dart index 1d41b34b..c3bbcbf6 100644 --- a/lib/generated/intl/messages_ru.dart +++ b/lib/generated/intl/messages_ru.dart @@ -22,350 +22,299 @@ class MessageLookup extends MessageLookupByLibrary { final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { - "Moscow": MessageLookupByLibrary.simpleMessage("Москва"), - "Russia": MessageLookupByLibrary.simpleMessage("Россия"), - "about_diet": MessageLookupByLibrary.simpleMessage("Об этой диете"), - "about_plan": MessageLookupByLibrary.simpleMessage("Об этом плане"), - "active": MessageLookupByLibrary.simpleMessage("Активен"), - "active_diet_state": MessageLookupByLibrary.simpleMessage("Активная"), - "add": MessageLookupByLibrary.simpleMessage("Добавить"), - "add_cover": MessageLookupByLibrary.simpleMessage("Добавьте обложку"), - "add_measure": MessageLookupByLibrary.simpleMessage("Добавить замер"), - "adding_plan": MessageLookupByLibrary.simpleMessage("Добавление плана"), - "additional_information": MessageLookupByLibrary.simpleMessage( - "Дополнительная информация", - ), - "admin": MessageLookupByLibrary.simpleMessage("Админ"), - "all": MessageLookupByLibrary.simpleMessage("Все"), - "allergies_presence": MessageLookupByLibrary.simpleMessage( - "Наличие аллергии", - ), - "another_form_of_comment": MessageLookupByLibrary.simpleMessage( - "комментария", - ), - "another_form_of_day": MessageLookupByLibrary.simpleMessage("дня"), - "answer": MessageLookupByLibrary.simpleMessage("Ответить"), - "archived": MessageLookupByLibrary.simpleMessage("В архиве"), - "authentificate": MessageLookupByLibrary.simpleMessage("Войти"), - "average_pressure": MessageLookupByLibrary.simpleMessage( - "Средние показатели давления:", - ), - "bodymass_chart": MessageLookupByLibrary.simpleMessage("График ИМТ"), - "by_default": MessageLookupByLibrary.simpleMessage("По умолчянию"), - "calories": MessageLookupByLibrary.simpleMessage("Калории"), - "carb_type": MessageLookupByLibrary.simpleMessage("Мучное"), - "carb_type_desc": MessageLookupByLibrary.simpleMessage( - "Если вы любите мучные продукты, но хотите придерживаться диеты, вам подходит диета с добавленим мучных блюд на основе цельнозерновых вариантов. В них больше сложных углеводов и клетчатки, что способствует длительному чувству сытости и поддерживает здоровье пищеварительной системы.", - ), - "carbohydrates": MessageLookupByLibrary.simpleMessage("Углеводы"), - "chat": MessageLookupByLibrary.simpleMessage("Чат"), - "chest": MessageLookupByLibrary.simpleMessage("Грудь"), - "chest_circumference": MessageLookupByLibrary.simpleMessage("Обхват груди"), - "choose_diet_duration": MessageLookupByLibrary.simpleMessage( - "Выберите срок диеты", - ), - "choose_plan_duration": MessageLookupByLibrary.simpleMessage( - "Выберите срок плана", - ), - "choose_start_diet_date": MessageLookupByLibrary.simpleMessage( - "Выберите дату старты диеты. Она отобразиться у Вас в дневнике", - ), - "circumference_empty": MessageLookupByLibrary.simpleMessage( - "Все замеры обязательны к заполнению", - ), - "circumference_is_not_a_number": MessageLookupByLibrary.simpleMessage( - "Все замеры должны быть числами", - ), - "circumferences_chart": MessageLookupByLibrary.simpleMessage( - "График обхватов", - ), - "city": MessageLookupByLibrary.simpleMessage("Город"), - "city_of_living": MessageLookupByLibrary.simpleMessage("Город проживания"), - "comfort": MessageLookupByLibrary.simpleMessage("Комфорт"), - "comment": MessageLookupByLibrary.simpleMessage("комментарий"), - "comments": MessageLookupByLibrary.simpleMessage("Комментарии"), - "comments_plural": MessageLookupByLibrary.simpleMessage("комментариев"), - "complain": MessageLookupByLibrary.simpleMessage("Пожаловаться"), - "continueButton": MessageLookupByLibrary.simpleMessage("Продолжить"), - "continue_": MessageLookupByLibrary.simpleMessage("Продолжить"), - "copied": MessageLookupByLibrary.simpleMessage("cкопирован"), - "copy_link": MessageLookupByLibrary.simpleMessage("Скопировать ссылку"), - "country": MessageLookupByLibrary.simpleMessage("Страна"), - "create_diet": MessageLookupByLibrary.simpleMessage("Создание диеты"), - "customer_price": MessageLookupByLibrary.simpleMessage( - "Цена для покупателя", - ), - "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("Статус диеты"), - "diet_type": MessageLookupByLibrary.simpleMessage("Тип диеты"), - "do_you_have_hypertension_tendency": MessageLookupByLibrary.simpleMessage( - "Есть ли у вас гипертония?", - ), - "do_you_have_hypotension_tendency": MessageLookupByLibrary.simpleMessage( - "Есть ли у вас гипотония?", - ), - "do_you_take_pills": MessageLookupByLibrary.simpleMessage( - "Принимаете ли вы лекарства или бады?", - ), - "down_pressure": MessageLookupByLibrary.simpleMessage( - "Нижние показатели давления:", - ), - "draft": MessageLookupByLibrary.simpleMessage("Черновик"), - "drafted": MessageLookupByLibrary.simpleMessage("Черновики"), - "eating_behavior": MessageLookupByLibrary.simpleMessage( - "Пищевое поведение", - ), - "eating_behavior_type": MessageLookupByLibrary.simpleMessage( - "Формирование пищевого поведения", - ), - "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( - "Необходимо ввести адрес электронной почты", - ), - "email_invalid": MessageLookupByLibrary.simpleMessage( - "Некорректный адрес электронной почты", - ), - "enter_additional_information": MessageLookupByLibrary.simpleMessage( - "Введите любую дополнительную информацию на своё усмотрение:", - ), - "enter_diet_description": MessageLookupByLibrary.simpleMessage( - "Введите описание диеты", - ), - "enter_diet_name": MessageLookupByLibrary.simpleMessage( - "Выведите название диеты", - ), - "enter_information": MessageLookupByLibrary.simpleMessage( - "Введите информацию...", - ), - "exit": MessageLookupByLibrary.simpleMessage("Выйти"), - "expand": MessageLookupByLibrary.simpleMessage("Показать полностью"), - "fats": MessageLookupByLibrary.simpleMessage("Жиры"), - "favorite_foods": MessageLookupByLibrary.simpleMessage("Любимые продукты"), - "favorite_unfavorite_foods": MessageLookupByLibrary.simpleMessage( - "Ваши любимые/нелюбимые продукты питания?", - ), - "female": MessageLookupByLibrary.simpleMessage("Женский"), - "fill_in_information": MessageLookupByLibrary.simpleMessage( - "Заполните информацию:", - ), - "finish": MessageLookupByLibrary.simpleMessage("Завершить"), - "first_name": MessageLookupByLibrary.simpleMessage("Имя"), - "first_name_empty": MessageLookupByLibrary.simpleMessage( - "Имя обязательно для заполнения", - ), - "first_name_short": MessageLookupByLibrary.simpleMessage( - "В имени должен быть минимум 1 символ", - ), - "fit_type": MessageLookupByLibrary.simpleMessage("Поддержание формы"), - "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("Галерея"), - "gr": MessageLookupByLibrary.simpleMessage("гр"), - "grams": MessageLookupByLibrary.simpleMessage("гр"), - "height_measurements": MessageLookupByLibrary.simpleMessage("Замер роста"), - "hide": MessageLookupByLibrary.simpleMessage("Свернуть"), - "hip_circumference": MessageLookupByLibrary.simpleMessage("Обхват бедер"), - "hips": MessageLookupByLibrary.simpleMessage("Бедра"), - "in_progress": MessageLookupByLibrary.simpleMessage("В процессе"), - "inactive_diet_state": MessageLookupByLibrary.simpleMessage("Черновик"), - "initial_measurements": MessageLookupByLibrary.simpleMessage( - "Первичные замеры", - ), - "label": MessageLookupByLibrary.simpleMessage("Название"), - "language": MessageLookupByLibrary.simpleMessage("Язык"), - "last_name": MessageLookupByLibrary.simpleMessage("Фамилия"), - "last_name_empty": MessageLookupByLibrary.simpleMessage( - "Фамилия обязательна для заполнения", - ), - "last_name_short": MessageLookupByLibrary.simpleMessage( - "В фамилии должен быть минимум 6 символов", - ), - "lbl_english": MessageLookupByLibrary.simpleMessage("English"), - "light_type": MessageLookupByLibrary.simpleMessage("Легкая"), - "light_type_desc": MessageLookupByLibrary.simpleMessage( - "С некоторыми видами диет можно употреблять алкоголь в умеренных количествах. Рекомендуется совмещать вместе с низкокалорийными закусками.", - ), - "link_was_copied": MessageLookupByLibrary.simpleMessage( - "Ссылка успешно скопирована", - ), - "loading": MessageLookupByLibrary.simpleMessage("Загрузка..."), - "male": MessageLookupByLibrary.simpleMessage("Мужской"), - "meal": MessageLookupByLibrary.simpleMessage("Приём пищи"), - "meat": MessageLookupByLibrary.simpleMessage("Мясо"), - "meat_type": MessageLookupByLibrary.simpleMessage("С мясом"), - "meat_type_desc": MessageLookupByLibrary.simpleMessage( - "Диета с мясом полезная и сбалансированная. Включает в себя нежирное мясо: курицу, индейку, кролика или говядину. В сочетании с овощами получаете все необходимые витамины и минералы. Не забывайте о порциях и старайтесь есть медленно, прислушиваясь к своему организму.", - ), - "milliliter": MessageLookupByLibrary.simpleMessage("мл"), - "minimal_customer_price": MessageLookupByLibrary.simpleMessage( - "Минимальная цена для покупателя: 1200₽", - ), - "most_new": MessageLookupByLibrary.simpleMessage("Самые новые"), - "most_old": MessageLookupByLibrary.simpleMessage("Самые старые"), - "msg_network_err": MessageLookupByLibrary.simpleMessage("Сетевая ошибка!"), - "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( - "Имя пользователя обязательно для заполнения", - ), - "name_short": MessageLookupByLibrary.simpleMessage( - "В имени пользователя должно быть минимум 6 символов", - ), - "no": MessageLookupByLibrary.simpleMessage("Нет"), - "no_account": MessageLookupByLibrary.simpleMessage("У меня нет аккаунта"), - "one_option": MessageLookupByLibrary.simpleMessage("1 опция"), - "option": MessageLookupByLibrary.simpleMessage("Опция"), - "option_content": MessageLookupByLibrary.simpleMessage("Содержание опции"), - "option_content_hint_text": MessageLookupByLibrary.simpleMessage( - "На связи 7 дней в неделю", - ), - "option_desc": MessageLookupByLibrary.simpleMessage("Описание опции"), - "option_desc_hint_text": MessageLookupByLibrary.simpleMessage( - "Этот план питания для похудения на 7 дней сбалансированный и разнообразный", - ), - "option_duration": MessageLookupByLibrary.simpleMessage( - "Продолжительность опции", - ), - "option_personal_diet_desc": MessageLookupByLibrary.simpleMessage( - "Опция подразумевает создание индивидуальной диеты", - ), - "option_title": MessageLookupByLibrary.simpleMessage("Заголовок опции"), - "order": MessageLookupByLibrary.simpleMessage("Порядок"), - "password": MessageLookupByLibrary.simpleMessage("Пароль"), - "password_confirmation": MessageLookupByLibrary.simpleMessage( - "Подтвердите пароль", - ), - "password_confirmation_mismatch": MessageLookupByLibrary.simpleMessage( - "Пароли не совпадают", - ), - "password_empty": MessageLookupByLibrary.simpleMessage( - "Пароль обязателен для заполнения", - ), - "password_short": MessageLookupByLibrary.simpleMessage( - "В пароле должно быть минимум 6 символов", - ), - "pcs": MessageLookupByLibrary.simpleMessage("шт"), - "personal": MessageLookupByLibrary.simpleMessage("Персональные"), - "personal_diet_state": MessageLookupByLibrary.simpleMessage("Персональная"), - "photo": MessageLookupByLibrary.simpleMessage("Фото"), - "place_of_living": MessageLookupByLibrary.simpleMessage("Место проживания"), - "plan_author": MessageLookupByLibrary.simpleMessage("Автор плана"), - "plan_desc": MessageLookupByLibrary.simpleMessage("Описание плана"), - "plan_desc_example": MessageLookupByLibrary.simpleMessage( - "Большое подробное, привлекательное описание плана", - ), - "plan_duration": MessageLookupByLibrary.simpleMessage("Срок плана"), - "plan_label_desc": MessageLookupByLibrary.simpleMessage( - "План для поддержания формы", - ), - "plans_complete": MessageLookupByLibrary.simpleMessage("Пройденные планы"), - "popular": MessageLookupByLibrary.simpleMessage("Популярные"), - "preferred_language": MessageLookupByLibrary.simpleMessage( - "Предпочитаемый язык общения", - ), - "pressure_empty": MessageLookupByLibrary.simpleMessage( - "Все показатели давления обязательны к заполнению", - ), - "pressure_is_not_a_number": MessageLookupByLibrary.simpleMessage( - "Все показатели давления должны быть числами", - ), - "preview": MessageLookupByLibrary.simpleMessage("Превью"), - "price": MessageLookupByLibrary.simpleMessage("Цена"), - "price_explanation": MessageLookupByLibrary.simpleMessage( - "Разница в ценах, это комиссия Итвио", - ), - "profile": MessageLookupByLibrary.simpleMessage("Профиль"), - "proteins": MessageLookupByLibrary.simpleMessage("Белки"), - "publish": MessageLookupByLibrary.simpleMessage("Опубликовать"), - "published": MessageLookupByLibrary.simpleMessage("Опубликованные"), - "removeFromActive": MessageLookupByLibrary.simpleMessage( - "Снять с публикации", - ), - "save": MessageLookupByLibrary.simpleMessage("Сохранить"), - "services": MessageLookupByLibrary.simpleMessage("Оказание услуг"), - "settings": MessageLookupByLibrary.simpleMessage("Настройки"), - "share": MessageLookupByLibrary.simpleMessage("Поделиться"), - "skip": MessageLookupByLibrary.simpleMessage("Пропустить"), - "slim": MessageLookupByLibrary.simpleMessage("Похудение"), - "slim_type": MessageLookupByLibrary.simpleMessage("Похудение"), - "slim_type_desc": MessageLookupByLibrary.simpleMessage( - "План питания для похудения должен быть разработан с учётом индивидуальных особенностей организма, возраста, пола, уровня физической активности и состояния здоровья. Общие принципы: \n● Снижение калорийности; \n● Сбалансированный рацион; \n● Разнообразие продуктов; \n● Питьевой режим; \n● Ограничение вредных продуктов;", - ), - "standard": MessageLookupByLibrary.simpleMessage("Стандарт"), - "startDiet": MessageLookupByLibrary.simpleMessage("Начать диету"), - "tell_your_schedule": MessageLookupByLibrary.simpleMessage( - "Расскажите в свободной форме о том как просыпаетесь, когда едите, когда ложитесь спать:", - ), - "three_options": MessageLookupByLibrary.simpleMessage("3 опции"), - "toOffer": MessageLookupByLibrary.simpleMessage("Предложить"), - "to_draft": MessageLookupByLibrary.simpleMessage("В черновик"), - "unfavorite_foods": MessageLookupByLibrary.simpleMessage( - "Нелюбимые продукты", - ), - "up_pressure": MessageLookupByLibrary.simpleMessage( - "Верхние показатели давления:", - ), - "user_authorization": MessageLookupByLibrary.simpleMessage( - "Аутентификация", - ), - "user_data": MessageLookupByLibrary.simpleMessage("Данные пользователя"), - "user_registration": MessageLookupByLibrary.simpleMessage( - "Регистрация пользователя", - ), - "username": MessageLookupByLibrary.simpleMessage("Имя пользователя"), - "vegan": MessageLookupByLibrary.simpleMessage("Веган."), - "vegan_type": MessageLookupByLibrary.simpleMessage("Вегетарианская"), - "vegan_type_desc": MessageLookupByLibrary.simpleMessage( - "Диета основана на потреблении растительной пищи и отказе от продуктов животного происхождения. Такой тип питания может быть разнообразным и вкусным, а также полезным для здоровья.", - ), - "waist": MessageLookupByLibrary.simpleMessage("Талия"), - "waist_circumference": MessageLookupByLibrary.simpleMessage("Обхват талии"), - "weight": MessageLookupByLibrary.simpleMessage("Вес"), - "weight_chart": MessageLookupByLibrary.simpleMessage("График веса"), - "weight_measurements": MessageLookupByLibrary.simpleMessage("Замер веса"), - "wellness": MessageLookupByLibrary.simpleMessage("Улучшение самочувствия"), - "wellness_type": MessageLookupByLibrary.simpleMessage( - "Улучшение самочувствия", - ), - "wellness_type_desc": MessageLookupByLibrary.simpleMessage( - "Если вы любите мучные продукты, но хотите придерживаться диеты, вам подходит диета с добавленим мучных блюд на основе цельнозерновых вариантов. В них больше сложных углеводов и клетчатки, что способствует длительному чувству сытости и поддерживает здоровье пищеварительной системы.", - ), - "what_is_your_name": MessageLookupByLibrary.simpleMessage("Как вас зовут?"), - "wrong_credentials": MessageLookupByLibrary.simpleMessage( - "Неверные данные пользователя", - ), - "you_will_get": MessageLookupByLibrary.simpleMessage("Вы получите"), - "your_blood_pressure": MessageLookupByLibrary.simpleMessage( - "Ваше артериальное давление?", - ), - "your_favorite_sports": MessageLookupByLibrary.simpleMessage( - "Ваши спортивные увлечения?", - ), - "your_gender": MessageLookupByLibrary.simpleMessage("Ваш пол?"), - "your_height": MessageLookupByLibrary.simpleMessage("Ваш рост:"), - "your_schedule": MessageLookupByLibrary.simpleMessage("Ваш режим дня?"), - "your_weight": MessageLookupByLibrary.simpleMessage("Ваш вес:"), - }; + "Moscow": MessageLookupByLibrary.simpleMessage("Москва"), + "Russia": MessageLookupByLibrary.simpleMessage("Россия"), + "about_diet": MessageLookupByLibrary.simpleMessage("Об этой диете"), + "about_plan": MessageLookupByLibrary.simpleMessage("Об этом плане"), + "active": MessageLookupByLibrary.simpleMessage("Активен"), + "active_diet_state": MessageLookupByLibrary.simpleMessage("Активная"), + "add": MessageLookupByLibrary.simpleMessage("Добавить"), + "add_cover": MessageLookupByLibrary.simpleMessage("Добавьте обложку"), + "add_measure": MessageLookupByLibrary.simpleMessage("Добавить замер"), + "adding_plan": MessageLookupByLibrary.simpleMessage("Добавление плана"), + "additional_information": + MessageLookupByLibrary.simpleMessage("Дополнительная информация"), + "admin": MessageLookupByLibrary.simpleMessage("Админ"), + "all": MessageLookupByLibrary.simpleMessage("Все"), + "allergies_presence": + MessageLookupByLibrary.simpleMessage("Наличие аллергии"), + "another_form_of_comment": + MessageLookupByLibrary.simpleMessage("комментария"), + "another_form_of_day": MessageLookupByLibrary.simpleMessage("дня"), + "answer": MessageLookupByLibrary.simpleMessage("Ответить"), + "archived": MessageLookupByLibrary.simpleMessage("В архиве"), + "authentificate": MessageLookupByLibrary.simpleMessage("Войти"), + "average_pressure": MessageLookupByLibrary.simpleMessage( + "Средние показатели давления:"), + "bodymass_chart": MessageLookupByLibrary.simpleMessage("График ИМТ"), + "by_default": MessageLookupByLibrary.simpleMessage("По умолчянию"), + "calories": MessageLookupByLibrary.simpleMessage("Калории"), + "carb_type": MessageLookupByLibrary.simpleMessage("Мучное"), + "carb_type_desc": MessageLookupByLibrary.simpleMessage( + "Если вы любите мучные продукты, но хотите придерживаться диеты, вам подходит диета с добавленим мучных блюд на основе цельнозерновых вариантов. В них больше сложных углеводов и клетчатки, что способствует длительному чувству сытости и поддерживает здоровье пищеварительной системы."), + "carbohydrates": MessageLookupByLibrary.simpleMessage("Углеводы"), + "chat": MessageLookupByLibrary.simpleMessage("Чат"), + "chest": MessageLookupByLibrary.simpleMessage("Грудь"), + "chest_circumference": + MessageLookupByLibrary.simpleMessage("Обхват груди"), + "choose_diet_duration": + MessageLookupByLibrary.simpleMessage("Выберите срок диеты"), + "choose_plan_duration": + MessageLookupByLibrary.simpleMessage("Выберите срок плана"), + "choose_start_diet_date": MessageLookupByLibrary.simpleMessage( + "Выберите дату старты диеты. Она отобразиться у Вас в дневнике"), + "circumference_empty": MessageLookupByLibrary.simpleMessage( + "Все замеры обязательны к заполнению"), + "circumference_is_not_a_number": MessageLookupByLibrary.simpleMessage( + "Все замеры должны быть числами"), + "circumferences_chart": + MessageLookupByLibrary.simpleMessage("График обхватов"), + "city": MessageLookupByLibrary.simpleMessage("Город"), + "city_of_living": + MessageLookupByLibrary.simpleMessage("Город проживания"), + "comfort": MessageLookupByLibrary.simpleMessage("Комфорт"), + "comment": MessageLookupByLibrary.simpleMessage("комментарий"), + "comments": MessageLookupByLibrary.simpleMessage("Комментарии"), + "comments_plural": MessageLookupByLibrary.simpleMessage("комментариев"), + "complain": MessageLookupByLibrary.simpleMessage("Пожаловаться"), + "continueButton": MessageLookupByLibrary.simpleMessage("Продолжить"), + "continue_": MessageLookupByLibrary.simpleMessage("Продолжить"), + "copied": MessageLookupByLibrary.simpleMessage("cкопирован"), + "copy_link": MessageLookupByLibrary.simpleMessage("Скопировать ссылку"), + "country": MessageLookupByLibrary.simpleMessage("Страна"), + "create_diet": MessageLookupByLibrary.simpleMessage("Создание диеты"), + "customer_price": + MessageLookupByLibrary.simpleMessage("Цена для покупателя"), + "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("Статус диеты"), + "diet_type": MessageLookupByLibrary.simpleMessage("Тип диеты"), + "do_you_have_hypertension_tendency": + MessageLookupByLibrary.simpleMessage("Есть ли у вас гипертония?"), + "do_you_have_hypotension_tendency": + MessageLookupByLibrary.simpleMessage("Есть ли у вас гипотония?"), + "do_you_take_pills": MessageLookupByLibrary.simpleMessage( + "Принимаете ли вы лекарства или бады?"), + "down_pressure": + MessageLookupByLibrary.simpleMessage("Нижние показатели давления:"), + "draft": MessageLookupByLibrary.simpleMessage("Черновик"), + "drafted": MessageLookupByLibrary.simpleMessage("Черновики"), + "eating_behavior": + MessageLookupByLibrary.simpleMessage("Пищевое поведение"), + "eating_behavior_type": MessageLookupByLibrary.simpleMessage( + "Формирование пищевого поведения"), + "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( + "Необходимо ввести адрес электронной почты"), + "email_invalid": MessageLookupByLibrary.simpleMessage( + "Некорректный адрес электронной почты"), + "enter_additional_information": MessageLookupByLibrary.simpleMessage( + "Введите любую дополнительную информацию на своё усмотрение:"), + "enter_diet_description": + MessageLookupByLibrary.simpleMessage("Введите описание диеты"), + "enter_diet_name": + MessageLookupByLibrary.simpleMessage("Выведите название диеты"), + "enter_information": + MessageLookupByLibrary.simpleMessage("Введите информацию..."), + "exit": MessageLookupByLibrary.simpleMessage("Выйти"), + "expand": MessageLookupByLibrary.simpleMessage("Показать полностью"), + "fats": MessageLookupByLibrary.simpleMessage("Жиры"), + "favorite_foods": + MessageLookupByLibrary.simpleMessage("Любимые продукты"), + "favorite_unfavorite_foods": MessageLookupByLibrary.simpleMessage( + "Ваши любимые/нелюбимые продукты питания?"), + "female": MessageLookupByLibrary.simpleMessage("Женский"), + "fill_in_information": + MessageLookupByLibrary.simpleMessage("Заполните информацию:"), + "finish": MessageLookupByLibrary.simpleMessage("Завершить"), + "first_name": MessageLookupByLibrary.simpleMessage("Имя"), + "first_name_empty": MessageLookupByLibrary.simpleMessage( + "Имя обязательно для заполнения"), + "first_name_short": MessageLookupByLibrary.simpleMessage( + "В имени должен быть минимум 1 символ"), + "fit_type": MessageLookupByLibrary.simpleMessage("Поддержание формы"), + "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("Галерея"), + "gr": MessageLookupByLibrary.simpleMessage("гр"), + "grams": MessageLookupByLibrary.simpleMessage("гр"), + "height_measurements": + MessageLookupByLibrary.simpleMessage("Замер роста"), + "hide": MessageLookupByLibrary.simpleMessage("Свернуть"), + "hip_circumference": + MessageLookupByLibrary.simpleMessage("Обхват бедер"), + "hips": MessageLookupByLibrary.simpleMessage("Бедра"), + "in_progress": MessageLookupByLibrary.simpleMessage("В процессе"), + "inactive_diet_state": MessageLookupByLibrary.simpleMessage("Черновик"), + "initial_measurements": + MessageLookupByLibrary.simpleMessage("Первичные замеры"), + "label": MessageLookupByLibrary.simpleMessage("Название"), + "language": MessageLookupByLibrary.simpleMessage("Язык"), + "last_name": MessageLookupByLibrary.simpleMessage("Фамилия"), + "last_name_empty": MessageLookupByLibrary.simpleMessage( + "Фамилия обязательна для заполнения"), + "last_name_short": MessageLookupByLibrary.simpleMessage( + "В фамилии должен быть минимум 6 символов"), + "lbl_english": MessageLookupByLibrary.simpleMessage("English"), + "light_type": MessageLookupByLibrary.simpleMessage("Легкая"), + "light_type_desc": MessageLookupByLibrary.simpleMessage( + "С некоторыми видами диет можно употреблять алкоголь в умеренных количествах. Рекомендуется совмещать вместе с низкокалорийными закусками."), + "link_was_copied": + MessageLookupByLibrary.simpleMessage("Ссылка успешно скопирована"), + "loading": MessageLookupByLibrary.simpleMessage("Загрузка..."), + "male": MessageLookupByLibrary.simpleMessage("Мужской"), + "meal": MessageLookupByLibrary.simpleMessage("Приём пищи"), + "meat": MessageLookupByLibrary.simpleMessage("Мясо"), + "meat_type": MessageLookupByLibrary.simpleMessage("С мясом"), + "meat_type_desc": MessageLookupByLibrary.simpleMessage( + "Диета с мясом полезная и сбалансированная. Включает в себя нежирное мясо: курицу, индейку, кролика или говядину. В сочетании с овощами получаете все необходимые витамины и минералы. Не забывайте о порциях и старайтесь есть медленно, прислушиваясь к своему организму."), + "milliliter": MessageLookupByLibrary.simpleMessage("мл"), + "minimal_customer_price": MessageLookupByLibrary.simpleMessage( + "Минимальная цена для покупателя: 1200₽"), + "most_new": MessageLookupByLibrary.simpleMessage("Самые новые"), + "most_old": MessageLookupByLibrary.simpleMessage("Самые старые"), + "msg_network_err": + MessageLookupByLibrary.simpleMessage("Сетевая ошибка!"), + "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( + "Имя пользователя обязательно для заполнения"), + "name_short": MessageLookupByLibrary.simpleMessage( + "В имени пользователя должно быть минимум 6 символов"), + "no": MessageLookupByLibrary.simpleMessage("Нет"), + "no_account": + MessageLookupByLibrary.simpleMessage("У меня нет аккаунта"), + "one_option": MessageLookupByLibrary.simpleMessage("1 опция"), + "option": MessageLookupByLibrary.simpleMessage("Опция"), + "option_content": + MessageLookupByLibrary.simpleMessage("Содержание опции"), + "option_content_hint_text": + MessageLookupByLibrary.simpleMessage("На связи 7 дней в неделю"), + "option_desc": MessageLookupByLibrary.simpleMessage("Описание опции"), + "option_desc_hint_text": MessageLookupByLibrary.simpleMessage( + "Этот план питания для похудения на 7 дней сбалансированный и разнообразный"), + "option_duration": + MessageLookupByLibrary.simpleMessage("Продолжительность опции"), + "option_personal_diet_desc": MessageLookupByLibrary.simpleMessage( + "Опция подразумевает создание индивидуальной диеты"), + "option_title": MessageLookupByLibrary.simpleMessage("Заголовок опции"), + "order": MessageLookupByLibrary.simpleMessage("Порядок"), + "password": MessageLookupByLibrary.simpleMessage("Пароль"), + "password_confirmation": + MessageLookupByLibrary.simpleMessage("Подтвердите пароль"), + "password_confirmation_mismatch": + MessageLookupByLibrary.simpleMessage("Пароли не совпадают"), + "password_empty": MessageLookupByLibrary.simpleMessage( + "Пароль обязателен для заполнения"), + "password_short": MessageLookupByLibrary.simpleMessage( + "В пароле должно быть минимум 6 символов"), + "pcs": MessageLookupByLibrary.simpleMessage("шт"), + "personal": MessageLookupByLibrary.simpleMessage("Персональные"), + "personal_diet_state": + MessageLookupByLibrary.simpleMessage("Персональная"), + "photo": MessageLookupByLibrary.simpleMessage("Фото"), + "place_of_living": + MessageLookupByLibrary.simpleMessage("Место проживания"), + "plan_author": MessageLookupByLibrary.simpleMessage("Автор плана"), + "plan_desc": MessageLookupByLibrary.simpleMessage("Описание плана"), + "plan_desc_example": MessageLookupByLibrary.simpleMessage( + "Большое подробное, привлекательное описание плана"), + "plan_duration": MessageLookupByLibrary.simpleMessage("Срок плана"), + "plan_label_desc": + MessageLookupByLibrary.simpleMessage("План для поддержания формы"), + "plans_complete": + MessageLookupByLibrary.simpleMessage("Пройденные планы"), + "popular": MessageLookupByLibrary.simpleMessage("Популярные"), + "preferred_language": + MessageLookupByLibrary.simpleMessage("Предпочитаемый язык общения"), + "pressure_empty": MessageLookupByLibrary.simpleMessage( + "Все показатели давления обязательны к заполнению"), + "pressure_is_not_a_number": MessageLookupByLibrary.simpleMessage( + "Все показатели давления должны быть числами"), + "preview": MessageLookupByLibrary.simpleMessage("Превью"), + "price": MessageLookupByLibrary.simpleMessage("Цена"), + "price_explanation": MessageLookupByLibrary.simpleMessage( + "Разница в ценах, это комиссия Итвио"), + "profile": MessageLookupByLibrary.simpleMessage("Профиль"), + "proteins": MessageLookupByLibrary.simpleMessage("Белки"), + "publish": MessageLookupByLibrary.simpleMessage("Опубликовать"), + "published": MessageLookupByLibrary.simpleMessage("Опубликованные"), + "removeFromActive": + MessageLookupByLibrary.simpleMessage("Снять с публикации"), + "save": MessageLookupByLibrary.simpleMessage("Сохранить"), + "services": MessageLookupByLibrary.simpleMessage("Оказание услуг"), + "settings": MessageLookupByLibrary.simpleMessage("Настройки"), + "share": MessageLookupByLibrary.simpleMessage("Поделиться"), + "skip": MessageLookupByLibrary.simpleMessage("Пропустить"), + "slim": MessageLookupByLibrary.simpleMessage("Похудение"), + "slim_type": MessageLookupByLibrary.simpleMessage("Похудение"), + "slim_type_desc": MessageLookupByLibrary.simpleMessage( + "План питания для похудения должен быть разработан с учётом индивидуальных особенностей организма, возраста, пола, уровня физической активности и состояния здоровья. Общие принципы: \n● Снижение калорийности; \n● Сбалансированный рацион; \n● Разнообразие продуктов; \n● Питьевой режим; \n● Ограничение вредных продуктов;"), + "standard": MessageLookupByLibrary.simpleMessage("Стандарт"), + "startDiet": MessageLookupByLibrary.simpleMessage("Начать диету"), + "tell_your_schedule": MessageLookupByLibrary.simpleMessage( + "Расскажите в свободной форме о том как просыпаетесь, когда едите, когда ложитесь спать:"), + "three_options": MessageLookupByLibrary.simpleMessage("3 опции"), + "toOffer": MessageLookupByLibrary.simpleMessage("Предложить"), + "to_draft": MessageLookupByLibrary.simpleMessage("В черновик"), + "unfavorite_foods": + MessageLookupByLibrary.simpleMessage("Нелюбимые продукты"), + "up_pressure": MessageLookupByLibrary.simpleMessage( + "Верхние показатели давления:"), + "user_authorization": + MessageLookupByLibrary.simpleMessage("Аутентификация"), + "user_data": + MessageLookupByLibrary.simpleMessage("Данные пользователя"), + "user_registration": + MessageLookupByLibrary.simpleMessage("Регистрация пользователя"), + "username": MessageLookupByLibrary.simpleMessage("Имя пользователя"), + "vegan": MessageLookupByLibrary.simpleMessage("Веган."), + "vegan_type": MessageLookupByLibrary.simpleMessage("Вегетарианская"), + "vegan_type_desc": MessageLookupByLibrary.simpleMessage( + "Диета основана на потреблении растительной пищи и отказе от продуктов животного происхождения. Такой тип питания может быть разнообразным и вкусным, а также полезным для здоровья."), + "waist": MessageLookupByLibrary.simpleMessage("Талия"), + "waist_circumference": + MessageLookupByLibrary.simpleMessage("Обхват талии"), + "weight": MessageLookupByLibrary.simpleMessage("Вес"), + "weight_chart": MessageLookupByLibrary.simpleMessage("График веса"), + "weight_measurements": + MessageLookupByLibrary.simpleMessage("Замер веса"), + "wellness": + MessageLookupByLibrary.simpleMessage("Улучшение самочувствия"), + "wellness_type": + MessageLookupByLibrary.simpleMessage("Улучшение самочувствия"), + "wellness_type_desc": MessageLookupByLibrary.simpleMessage( + "Если вы любите мучные продукты, но хотите придерживаться диеты, вам подходит диета с добавленим мучных блюд на основе цельнозерновых вариантов. В них больше сложных углеводов и клетчатки, что способствует длительному чувству сытости и поддерживает здоровье пищеварительной системы."), + "what_is_your_name": + MessageLookupByLibrary.simpleMessage("Как вас зовут?"), + "wrong_credentials": MessageLookupByLibrary.simpleMessage( + "Неверные данные пользователя"), + "you_will_get": MessageLookupByLibrary.simpleMessage("Вы получите"), + "your_blood_pressure": + MessageLookupByLibrary.simpleMessage("Ваше артериальное давление?"), + "your_favorite_sports": + MessageLookupByLibrary.simpleMessage("Ваши спортивные увлечения?"), + "your_gender": MessageLookupByLibrary.simpleMessage("Ваш пол?"), + "your_height": MessageLookupByLibrary.simpleMessage("Ваш рост:"), + "your_schedule": MessageLookupByLibrary.simpleMessage("Ваш режим дня?"), + "your_weight": MessageLookupByLibrary.simpleMessage("Ваш вес:") + }; } diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index 62d2834b..1bca8570 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -18,20 +18,17 @@ class S { static S? _current; static S get current { - assert( - _current != null, - 'No instance of S was loaded. Try to initialize the S delegate before accessing S.current.', - ); + assert(_current != null, + 'No instance of S was loaded. Try to initialize the S delegate before accessing S.current.'); return _current!; } static const AppLocalizationDelegate delegate = AppLocalizationDelegate(); static Future load(Locale locale) { - final name = - (locale.countryCode?.isEmpty ?? false) - ? locale.languageCode - : locale.toString(); + final name = (locale.countryCode?.isEmpty ?? false) + ? locale.languageCode + : locale.toString(); final localeName = Intl.canonicalizedLocale(name); return initializeMessages(localeName).then((_) { Intl.defaultLocale = localeName; @@ -44,10 +41,8 @@ class S { static S of(BuildContext context) { final instance = S.maybeOf(context); - assert( - instance != null, - 'No instance of S present in the widget tree. Did you add S.delegate in localizationsDelegates?', - ); + assert(instance != null, + 'No instance of S present in the widget tree. Did you add S.delegate in localizationsDelegates?'); return instance!; } @@ -67,7 +62,12 @@ class S { /// `Электронная почта` String get email { - return Intl.message('Электронная почта', name: 'email', desc: '', args: []); + return Intl.message( + 'Электронная почта', + name: 'email', + desc: '', + args: [], + ); } /// `Имя пользователя` @@ -82,7 +82,12 @@ class S { /// `Пароль` String get password { - return Intl.message('Пароль', name: 'password', desc: '', args: []); + return Intl.message( + 'Пароль', + name: 'password', + desc: '', + args: [], + ); } /// `Подтвердите пароль` @@ -107,12 +112,22 @@ class S { /// `Имя` String get first_name { - return Intl.message('Имя', name: 'first_name', desc: '', args: []); + return Intl.message( + 'Имя', + name: 'first_name', + desc: '', + args: [], + ); } /// `Фамилия` String get last_name { - return Intl.message('Фамилия', name: 'last_name', desc: '', args: []); + return Intl.message( + 'Фамилия', + name: 'last_name', + desc: '', + args: [], + ); } /// `Как вас зовут?` @@ -137,22 +152,42 @@ class S { /// `Страна` String get country { - return Intl.message('Страна', name: 'country', desc: '', args: []); + return Intl.message( + 'Страна', + name: 'country', + desc: '', + args: [], + ); } /// `Россия` String get Russia { - return Intl.message('Россия', name: 'Russia', desc: '', args: []); + return Intl.message( + 'Россия', + name: 'Russia', + desc: '', + args: [], + ); } /// `Город` String get city { - return Intl.message('Город', name: 'city', desc: '', args: []); + return Intl.message( + 'Город', + name: 'city', + desc: '', + args: [], + ); } /// `Москва` String get Moscow { - return Intl.message('Москва', name: 'Moscow', desc: '', args: []); + return Intl.message( + 'Москва', + name: 'Moscow', + desc: '', + args: [], + ); } /// `Место проживания` @@ -207,12 +242,22 @@ class S { /// `Мужской` String get male { - return Intl.message('Мужской', name: 'male', desc: '', args: []); + return Intl.message( + 'Мужской', + name: 'male', + desc: '', + args: [], + ); } /// `Женский` String get female { - return Intl.message('Женский', name: 'female', desc: '', args: []); + return Intl.message( + 'Женский', + name: 'female', + desc: '', + args: [], + ); } /// `Замер веса` @@ -227,7 +272,12 @@ class S { /// `Ваш вес:` String get your_weight { - return Intl.message('Ваш вес:', name: 'your_weight', desc: '', args: []); + return Intl.message( + 'Ваш вес:', + name: 'your_weight', + desc: '', + args: [], + ); } /// `Замер роста` @@ -242,7 +292,12 @@ class S { /// `Ваш рост:` String get your_height { - return Intl.message('Ваш рост:', name: 'your_height', desc: '', args: []); + return Intl.message( + 'Ваш рост:', + name: 'your_height', + desc: '', + args: [], + ); } /// `Наличие аллергии` @@ -267,12 +322,22 @@ class S { /// `Ваш пол?` String get your_gender { - return Intl.message('Ваш пол?', name: 'your_gender', desc: '', args: []); + return Intl.message( + 'Ваш пол?', + name: 'your_gender', + desc: '', + args: [], + ); } /// `Нет` String get no { - return Intl.message('Нет', name: 'no', desc: '', args: []); + return Intl.message( + 'Нет', + name: 'no', + desc: '', + args: [], + ); } /// `Есть ли у вас гипертония?` @@ -417,12 +482,22 @@ class S { /// `English` String get lbl_english { - return Intl.message('English', name: 'lbl_english', desc: '', args: []); + return Intl.message( + 'English', + name: 'lbl_english', + desc: '', + args: [], + ); } /// `Завершить` String get finish { - return Intl.message('Завершить', name: 'finish', desc: '', args: []); + return Intl.message( + 'Завершить', + name: 'finish', + desc: '', + args: [], + ); } /// `Предпочитаемый язык общения` @@ -447,7 +522,12 @@ class S { /// `Пропустить` String get skip { - return Intl.message('Пропустить', name: 'skip', desc: '', args: []); + return Intl.message( + 'Пропустить', + name: 'skip', + desc: '', + args: [], + ); } /// `Заполните информацию:` @@ -652,7 +732,12 @@ class S { /// `Войти` String get authentificate { - return Intl.message('Войти', name: 'authentificate', desc: '', args: []); + return Intl.message( + 'Войти', + name: 'authentificate', + desc: '', + args: [], + ); } /// `Аутентификация` @@ -667,7 +752,12 @@ class S { /// `Загрузка...` String get loading { - return Intl.message('Загрузка...', name: 'loading', desc: '', args: []); + return Intl.message( + 'Загрузка...', + name: 'loading', + desc: '', + args: [], + ); } /// `График веса` @@ -712,52 +802,102 @@ class S { /// `Грудь` String get chest { - return Intl.message('Грудь', name: 'chest', desc: '', args: []); + return Intl.message( + 'Грудь', + name: 'chest', + desc: '', + args: [], + ); } /// `Талия` String get waist { - return Intl.message('Талия', name: 'waist', desc: '', args: []); + return Intl.message( + 'Талия', + name: 'waist', + desc: '', + args: [], + ); } /// `Бедра` String get hips { - return Intl.message('Бедра', name: 'hips', desc: '', args: []); + return Intl.message( + 'Бедра', + name: 'hips', + desc: '', + args: [], + ); } /// `Выйти` String get exit { - return Intl.message('Выйти', name: 'exit', desc: '', args: []); + return Intl.message( + 'Выйти', + name: 'exit', + desc: '', + args: [], + ); } /// `Активен` String get active { - return Intl.message('Активен', name: 'active', desc: '', args: []); + return Intl.message( + 'Активен', + name: 'active', + desc: '', + args: [], + ); } /// `В архиве` String get archived { - return Intl.message('В архиве', name: 'archived', desc: '', args: []); + return Intl.message( + 'В архиве', + name: 'archived', + desc: '', + args: [], + ); } /// `Черновик` String get draft { - return Intl.message('Черновик', name: 'draft', desc: '', args: []); + return Intl.message( + 'Черновик', + name: 'draft', + desc: '', + args: [], + ); } /// `Мой блог` String get my_blog { - return Intl.message('Мой блог', name: 'my_blog', desc: '', args: []); + return Intl.message( + 'Мой блог', + name: 'my_blog', + desc: '', + args: [], + ); } /// `Язык` String get language { - return Intl.message('Язык', name: 'language', desc: '', args: []); + return Intl.message( + 'Язык', + name: 'language', + desc: '', + args: [], + ); } /// `В процессе` String get in_progress { - return Intl.message('В процессе', name: 'in_progress', desc: '', args: []); + return Intl.message( + 'В процессе', + name: 'in_progress', + desc: '', + args: [], + ); } /// `Добавить замер` @@ -772,7 +912,12 @@ class S { /// `Вес` String get weight { - return Intl.message('Вес', name: 'weight', desc: '', args: []); + return Intl.message( + 'Вес', + name: 'weight', + desc: '', + args: [], + ); } /// `Пройденные планы` @@ -797,67 +942,132 @@ class S { /// `Настройки` String get settings { - return Intl.message('Настройки', name: 'settings', desc: '', args: []); + return Intl.message( + 'Настройки', + name: 'settings', + desc: '', + args: [], + ); } /// `Профиль` String get profile { - return Intl.message('Профиль', name: 'profile', desc: '', args: []); + return Intl.message( + 'Профиль', + name: 'profile', + desc: '', + args: [], + ); } /// `Чат` String get chat { - return Intl.message('Чат', name: 'chat', desc: '', args: []); + return Intl.message( + 'Чат', + name: 'chat', + desc: '', + args: [], + ); } /// `Админ` String get admin { - return Intl.message('Админ', name: 'admin', desc: '', args: []); + return Intl.message( + 'Админ', + name: 'admin', + desc: '', + args: [], + ); } /// `Дневник` String get diary { - return Intl.message('Дневник', name: 'diary', desc: '', args: []); + return Intl.message( + 'Дневник', + name: 'diary', + desc: '', + args: [], + ); } /// `Сохранить` String get save { - return Intl.message('Сохранить', name: 'save', desc: '', args: []); + return Intl.message( + 'Сохранить', + name: 'save', + desc: '', + args: [], + ); } /// `шт` String get pcs { - return Intl.message('шт', name: 'pcs', desc: '', args: []); + return Intl.message( + 'шт', + name: 'pcs', + desc: '', + args: [], + ); } /// `гр` String get gr { - return Intl.message('гр', name: 'gr', desc: '', args: []); + return Intl.message( + 'гр', + name: 'gr', + desc: '', + args: [], + ); } /// `мл` String get milliliter { - return Intl.message('мл', name: 'milliliter', desc: '', args: []); + return Intl.message( + 'мл', + name: 'milliliter', + desc: '', + args: [], + ); } /// `гр` String get grams { - return Intl.message('гр', name: 'grams', desc: '', args: []); + return Intl.message( + 'гр', + name: 'grams', + desc: '', + args: [], + ); } /// `Начать диету` String get startDiet { - return Intl.message('Начать диету', name: 'startDiet', desc: '', args: []); + return Intl.message( + 'Начать диету', + name: 'startDiet', + desc: '', + args: [], + ); } /// `Комментарии` String get comments { - return Intl.message('Комментарии', name: 'comments', desc: '', args: []); + return Intl.message( + 'Комментарии', + name: 'comments', + desc: '', + args: [], + ); } /// `комментарий` String get comment { - return Intl.message('комментарий', name: 'comment', desc: '', args: []); + return Intl.message( + 'комментарий', + name: 'comment', + desc: '', + args: [], + ); } /// `комментария` @@ -882,17 +1092,32 @@ class S { /// `дней` String get days { - return Intl.message('дней', name: 'days', desc: '', args: []); + return Intl.message( + 'дней', + name: 'days', + desc: '', + args: [], + ); } /// `День` String get day { - return Intl.message('День', name: 'day', desc: '', args: []); + return Intl.message( + 'День', + name: 'day', + desc: '', + args: [], + ); } /// `дня` String get another_form_of_day { - return Intl.message('дня', name: 'another_form_of_day', desc: '', args: []); + return Intl.message( + 'дня', + name: 'another_form_of_day', + desc: '', + args: [], + ); } /// `комментариев` @@ -907,17 +1132,32 @@ class S { /// `Приём пищи` String get meal { - return Intl.message('Приём пищи', name: 'meal', desc: '', args: []); + return Intl.message( + 'Приём пищи', + name: 'meal', + desc: '', + args: [], + ); } /// `Автор диеты` String get diet_author { - return Intl.message('Автор диеты', name: 'diet_author', desc: '', args: []); + return Intl.message( + 'Автор диеты', + name: 'diet_author', + desc: '', + args: [], + ); } /// `Свернуть` String get hide { - return Intl.message('Свернуть', name: 'hide', desc: '', args: []); + return Intl.message( + 'Свернуть', + name: 'hide', + desc: '', + args: [], + ); } /// `Показать полностью` @@ -942,12 +1182,22 @@ class S { /// `Поделиться` String get share { - return Intl.message('Поделиться', name: 'share', desc: '', args: []); + return Intl.message( + 'Поделиться', + name: 'share', + desc: '', + args: [], + ); } /// `Пожаловаться` String get complain { - return Intl.message('Пожаловаться', name: 'complain', desc: '', args: []); + return Intl.message( + 'Пожаловаться', + name: 'complain', + desc: '', + args: [], + ); } /// `Вегетарианская` @@ -962,17 +1212,32 @@ class S { /// `Легкая` String get light_type { - return Intl.message('Легкая', name: 'light_type', desc: '', args: []); + return Intl.message( + 'Легкая', + name: 'light_type', + desc: '', + args: [], + ); } /// `С мясом` String get meat_type { - return Intl.message('С мясом', name: 'meat_type', desc: '', args: []); + return Intl.message( + 'С мясом', + name: 'meat_type', + desc: '', + args: [], + ); } /// `Мучное` String get carb_type { - return Intl.message('Мучное', name: 'carb_type', desc: '', args: []); + return Intl.message( + 'Мучное', + name: 'carb_type', + desc: '', + args: [], + ); } /// `Диета основана на потреблении растительной пищи и отказе от продуктов животного происхождения. Такой тип питания может быть разнообразным и вкусным, а также полезным для здоровья.` @@ -1017,32 +1282,62 @@ class S { /// `План диеты` String get diet_plan { - return Intl.message('План диеты', name: 'diet_plan', desc: '', args: []); + return Intl.message( + 'План диеты', + name: 'diet_plan', + desc: '', + args: [], + ); } /// `Калории` String get calories { - return Intl.message('Калории', name: 'calories', desc: '', args: []); + return Intl.message( + 'Калории', + name: 'calories', + desc: '', + args: [], + ); } /// `Белки` String get proteins { - return Intl.message('Белки', name: 'proteins', desc: '', args: []); + return Intl.message( + 'Белки', + name: 'proteins', + desc: '', + args: [], + ); } /// `Жиры` String get fats { - return Intl.message('Жиры', name: 'fats', desc: '', args: []); + return Intl.message( + 'Жиры', + name: 'fats', + desc: '', + args: [], + ); } /// `Углеводы` String get carbohydrates { - return Intl.message('Углеводы', name: 'carbohydrates', desc: '', args: []); + return Intl.message( + 'Углеводы', + name: 'carbohydrates', + desc: '', + args: [], + ); } /// `Ответить` String get answer { - return Intl.message('Ответить', name: 'answer', desc: '', args: []); + return Intl.message( + 'Ответить', + name: 'answer', + desc: '', + args: [], + ); } /// `Ссылка успешно скопирована` @@ -1067,47 +1362,92 @@ class S { /// `Продолжить` String get continue_ { - return Intl.message('Продолжить', name: 'continue_', desc: '', args: []); + return Intl.message( + 'Продолжить', + name: 'continue_', + desc: '', + args: [], + ); } /// `Порядок` String get order { - return Intl.message('Порядок', name: 'order', desc: '', args: []); + return Intl.message( + 'Порядок', + name: 'order', + desc: '', + args: [], + ); } /// `Время прохождения` String get time { - return Intl.message('Время прохождения', name: 'time', desc: '', args: []); + return Intl.message( + 'Время прохождения', + name: 'time', + desc: '', + args: [], + ); } /// `Все` String get all { - return Intl.message('Все', name: 'all', desc: '', args: []); + return Intl.message( + 'Все', + name: 'all', + desc: '', + args: [], + ); } /// `Тип диеты` String get diet_type { - return Intl.message('Тип диеты', name: 'diet_type', desc: '', args: []); + return Intl.message( + 'Тип диеты', + name: 'diet_type', + desc: '', + args: [], + ); } /// `Тип плана` String get plan_type { - return Intl.message('Тип плана', name: 'plan_type', desc: '', args: []); + return Intl.message( + 'Тип плана', + name: 'plan_type', + desc: '', + args: [], + ); } /// `Популярные` String get popular { - return Intl.message('Популярные', name: 'popular', desc: '', args: []); + return Intl.message( + 'Популярные', + name: 'popular', + desc: '', + args: [], + ); } /// `Самые новые` String get most_new { - return Intl.message('Самые новые', name: 'most_new', desc: '', args: []); + return Intl.message( + 'Самые новые', + name: 'most_new', + desc: '', + args: [], + ); } /// `Самые старые` String get most_old { - return Intl.message('Самые старые', name: 'most_old', desc: '', args: []); + return Intl.message( + 'Самые старые', + name: 'most_old', + desc: '', + args: [], + ); } /// `Название: А-Я` @@ -1132,7 +1472,12 @@ class S { /// `По умолчянию` String get by_default { - return Intl.message('По умолчянию', name: 'by_default', desc: '', args: []); + return Intl.message( + 'По умолчянию', + name: 'by_default', + desc: '', + args: [], + ); } /// `Создание диеты` @@ -1177,7 +1522,12 @@ class S { /// `Статус диеты` String get diet_state { - return Intl.message('Статус диеты', name: 'diet_state', desc: '', args: []); + return Intl.message( + 'Статус диеты', + name: 'diet_state', + desc: '', + args: [], + ); } /// `Активная` @@ -1212,22 +1562,42 @@ class S { /// `Мясо` String get meat { - return Intl.message('Мясо', name: 'meat', desc: '', args: []); + return Intl.message( + 'Мясо', + name: 'meat', + desc: '', + args: [], + ); } /// `Веган.` String get vegan { - return Intl.message('Веган.', name: 'vegan', desc: '', args: []); + return Intl.message( + 'Веган.', + name: 'vegan', + desc: '', + args: [], + ); } /// `Фото` String get photo { - return Intl.message('Фото', name: 'photo', desc: '', args: []); + return Intl.message( + 'Фото', + name: 'photo', + desc: '', + args: [], + ); } /// `Галерея` String get gallery { - return Intl.message('Галерея', name: 'gallery', desc: '', args: []); + return Intl.message( + 'Галерея', + name: 'gallery', + desc: '', + args: [], + ); } /// `Добавьте обложку` @@ -1242,27 +1612,52 @@ class S { /// `Добавить` String get add { - return Intl.message('Добавить', name: 'add', desc: '', args: []); + return Intl.message( + 'Добавить', + name: 'add', + desc: '', + args: [], + ); } /// `cкопирован` String get copied { - return Intl.message('cкопирован', name: 'copied', desc: '', args: []); + return Intl.message( + 'cкопирован', + name: 'copied', + desc: '', + args: [], + ); } /// `В черновик` String get to_draft { - return Intl.message('В черновик', name: 'to_draft', desc: '', args: []); + return Intl.message( + 'В черновик', + name: 'to_draft', + desc: '', + args: [], + ); } /// `Превью` String get preview { - return Intl.message('Превью', name: 'preview', desc: '', args: []); + return Intl.message( + 'Превью', + name: 'preview', + desc: '', + args: [], + ); } /// `Опубликовать` String get publish { - return Intl.message('Опубликовать', name: 'publish', desc: '', args: []); + return Intl.message( + 'Опубликовать', + name: 'publish', + desc: '', + args: [], + ); } /// `Добавление плана` @@ -1277,7 +1672,12 @@ class S { /// `Название` String get label { - return Intl.message('Название', name: 'label', desc: '', args: []); + return Intl.message( + 'Название', + name: 'label', + desc: '', + args: [], + ); } /// `План для поддержания формы` @@ -1302,12 +1702,22 @@ class S { /// `Похудение` String get slim { - return Intl.message('Похудение', name: 'slim', desc: '', args: []); + return Intl.message( + 'Похудение', + name: 'slim', + desc: '', + args: [], + ); } /// `Поддержка формы` String get fitness { - return Intl.message('Поддержка формы', name: 'fitness', desc: '', args: []); + return Intl.message( + 'Поддержка формы', + name: 'fitness', + desc: '', + args: [], + ); } /// `Улучшение самочувствия` @@ -1362,37 +1772,72 @@ class S { /// `1 опция` String get one_option { - return Intl.message('1 опция', name: 'one_option', desc: '', args: []); + return Intl.message( + '1 опция', + name: 'one_option', + desc: '', + args: [], + ); } /// `3 опции` String get three_options { - return Intl.message('3 опции', name: 'three_options', desc: '', args: []); + return Intl.message( + '3 опции', + name: 'three_options', + desc: '', + args: [], + ); } /// `Опция` String get option { - return Intl.message('Опция', name: 'option', desc: '', args: []); + return Intl.message( + 'Опция', + name: 'option', + desc: '', + args: [], + ); } /// `Оказание услуг` String get services { - return Intl.message('Оказание услуг', name: 'services', desc: '', args: []); + return Intl.message( + 'Оказание услуг', + name: 'services', + desc: '', + args: [], + ); } /// `Эконом` String get economy { - return Intl.message('Эконом', name: 'economy', desc: '', args: []); + return Intl.message( + 'Эконом', + name: 'economy', + desc: '', + args: [], + ); } /// `Стандарт` String get standard { - return Intl.message('Стандарт', name: 'standard', desc: '', args: []); + return Intl.message( + 'Стандарт', + name: 'standard', + desc: '', + args: [], + ); } /// `Комфорт` String get comfort { - return Intl.message('Комфорт', name: 'comfort', desc: '', args: []); + return Intl.message( + 'Комфорт', + name: 'comfort', + desc: '', + args: [], + ); } /// `Заголовок опции` @@ -1507,7 +1952,12 @@ class S { /// `Автор плана` String get plan_author { - return Intl.message('Автор плана', name: 'plan_author', desc: '', args: []); + return Intl.message( + 'Автор плана', + name: 'plan_author', + desc: '', + args: [], + ); } /// `Продолжительность опции` @@ -1542,7 +1992,12 @@ class S { /// `Похудение` String get slim_type { - return Intl.message('Похудение', name: 'slim_type', desc: '', args: []); + return Intl.message( + 'Похудение', + name: 'slim_type', + desc: '', + args: [], + ); } /// `План питания для похудения должен быть разработан с учётом индивидуальных особенностей организма, возраста, пола, уровня физической активности и состояния здоровья. Общие принципы: \n● Снижение калорийности; \n● Сбалансированный рацион; \n● Разнообразие продуктов; \n● Питьевой режим; \n● Ограничение вредных продуктов;` @@ -1597,12 +2052,22 @@ class S { /// `Цена` String get price { - return Intl.message('Цена', name: 'price', desc: '', args: []); + return Intl.message( + 'Цена', + name: 'price', + desc: '', + args: [], + ); } /// `Мои диеты` String get my_diets { - return Intl.message('Мои диеты', name: 'my_diets', desc: '', args: []); + return Intl.message( + 'Мои диеты', + name: 'my_diets', + desc: '', + args: [], + ); } /// `Опубликованные` @@ -1617,22 +2082,42 @@ class S { /// `Черновики` String get drafted { - return Intl.message('Черновики', name: 'drafted', desc: '', args: []); + return Intl.message( + 'Черновики', + name: 'drafted', + desc: '', + args: [], + ); } /// `Персональные` String get personal { - return Intl.message('Персональные', name: 'personal', desc: '', args: []); + return Intl.message( + 'Персональные', + name: 'personal', + desc: '', + args: [], + ); } /// `Редактировать` String get edit { - return Intl.message('Редактировать', name: 'edit', desc: '', args: []); + return Intl.message( + 'Редактировать', + name: 'edit', + desc: '', + args: [], + ); } /// `Предложить` String get toOffer { - return Intl.message('Предложить', name: 'toOffer', desc: '', args: []); + return Intl.message( + 'Предложить', + name: 'toOffer', + desc: '', + args: [], + ); } /// `Снять с публикации` @@ -1647,22 +2132,42 @@ class S { /// `Диета` String get diet { - return Intl.message('Диета', name: 'diet', desc: '', args: []); + return Intl.message( + 'Диета', + name: 'diet', + desc: '', + args: [], + ); } /// `Удалена` String get deleted { - return Intl.message('Удалена', name: 'deleted', desc: '', args: []); + return Intl.message( + 'Удалена', + name: 'deleted', + desc: '', + args: [], + ); } /// `Мои планы` String get my_plans { - return Intl.message('Мои планы', name: 'my_plans', desc: '', args: []); + return Intl.message( + 'Мои планы', + name: 'my_plans', + desc: '', + args: [], + ); } /// `от` String get from { - return Intl.message('от', name: 'from', desc: '', args: []); + return Intl.message( + 'от', + name: 'from', + desc: '', + args: [], + ); } /// `Данные пользователя` @@ -1687,7 +2192,12 @@ class S { /// `Пол` String get sex { - return Intl.message('Пол', name: 'sex', desc: '', args: []); + return Intl.message( + 'Пол', + name: 'sex', + desc: '', + args: [], + ); } /// `Физическая форма` @@ -1702,22 +2212,42 @@ class S { /// `Рост` String get height { - return Intl.message('Рост', name: 'height', desc: '', args: []); + return Intl.message( + 'Рост', + name: 'height', + desc: '', + args: [], + ); } /// `Давление` String get pressure { - return Intl.message('Давление', name: 'pressure', desc: '', args: []); + return Intl.message( + 'Давление', + name: 'pressure', + desc: '', + args: [], + ); } /// `Верхнее` String get upper { - return Intl.message('Верхнее', name: 'upper', desc: '', args: []); + return Intl.message( + 'Верхнее', + name: 'upper', + desc: '', + args: [], + ); } /// `Нижнее` String get lower { - return Intl.message('Нижнее', name: 'lower', desc: '', args: []); + return Intl.message( + 'Нижнее', + name: 'lower', + desc: '', + args: [], + ); } /// `Склонность к гипотонии` @@ -1822,7 +2352,12 @@ class S { /// `Подтвердить` String get confirm { - return Intl.message('Подтвердить', name: 'confirm', desc: '', args: []); + return Intl.message( + 'Подтвердить', + name: 'confirm', + desc: '', + args: [], + ); } /// `Не удалось отменить диету` @@ -1834,6 +2369,816 @@ class S { args: [], ); } + + /// `часов` + String get hours { + return Intl.message( + 'часов', + name: 'hours', + desc: '', + args: [], + ); + } + + /// `час` + String get hour { + return Intl.message( + 'час', + name: 'hour', + desc: '', + args: [], + ); + } + + /// `часа` + String get another_form_of_hour { + return Intl.message( + 'часа', + name: 'another_form_of_hour', + desc: '', + args: [], + ); + } + + /// `минуты` + String get minutes { + return Intl.message( + 'минуты', + name: 'minutes', + desc: '', + args: [], + ); + } + + /// `минута` + String get minute { + return Intl.message( + 'минута', + name: 'minute', + desc: '', + args: [], + ); + } + + /// `минут` + String get another_form_of_minute { + return Intl.message( + 'минут', + name: 'another_form_of_minute', + desc: '', + args: [], + ); + } + + /// `Есть видео` + String get there_is_a_video { + return Intl.message( + 'Есть видео', + name: 'there_is_a_video', + desc: '', + args: [], + ); + } + + /// `Автор рецепта` + String get recipe_author { + return Intl.message( + 'Автор рецепта', + name: 'recipe_author', + desc: '', + args: [], + ); + } + + /// `Об этом рецепте` + String get about_recipe { + return Intl.message( + 'Об этом рецепте', + name: 'about_recipe', + desc: '', + args: [], + ); + } + + /// `этапов` + String get stages { + return Intl.message( + 'этапов', + name: 'stages', + desc: '', + args: [], + ); + } + + /// `этап` + String get stage { + return Intl.message( + 'этап', + name: 'stage', + desc: '', + args: [], + ); + } + + /// `этапа` + String get another_form_of_stage { + return Intl.message( + 'этапа', + name: 'another_form_of_stage', + desc: '', + args: [], + ); + } + + /// `Посмотреть видео рецепт` + String get check_out_video_recipe { + return Intl.message( + 'Посмотреть видео рецепт', + name: 'check_out_video_recipe', + desc: '', + args: [], + ); + } + + /// `Не удалось запустить видео` + String get could_not_launch_video { + return Intl.message( + 'Не удалось запустить видео', + name: 'could_not_launch_video', + desc: '', + args: [], + ); + } + + /// `Ингредиенты` + String get ingredients { + return Intl.message( + 'Ингредиенты', + name: 'ingredients', + desc: '', + args: [], + ); + } + + /// `Шаг` + String get step { + return Intl.message( + 'Шаг', + name: 'step', + desc: '', + args: [], + ); + } + + /// `Пошаговый фоторецепт` + String get step_by_step_photo_recipe { + return Intl.message( + 'Пошаговый фоторецепт', + name: 'step_by_step_photo_recipe', + desc: '', + args: [], + ); + } + + /// `снята с публикации и перенесена в черновик` + String get diet_has_been_sent_to_draft { + return Intl.message( + 'снята с публикации и перенесена в черновик', + name: 'diet_has_been_sent_to_draft', + desc: '', + args: [], + ); + } + + /// `Спам` + String get spam { + return Intl.message( + 'Спам', + name: 'spam', + desc: '', + args: [], + ); + } + + /// `Нарушение законодательства` + String get lawViolation { + return Intl.message( + 'Нарушение законодательства', + name: 'lawViolation', + desc: '', + args: [], + ); + } + + /// `Обман` + String get deception { + return Intl.message( + 'Обман', + name: 'deception', + desc: '', + args: [], + ); + } + + /// `Насилие и вражда` + String get violenceAndHostility { + return Intl.message( + 'Насилие и вражда', + name: 'violenceAndHostility', + desc: '', + args: [], + ); + } + + /// `Откровенное изображение` + String get explicitImage { + return Intl.message( + 'Откровенное изображение', + name: 'explicitImage', + desc: '', + args: [], + ); + } + + /// `Откровенные изображения` + String get explicitImages { + return Intl.message( + 'Откровенные изображения', + name: 'explicitImages', + desc: '', + args: [], + ); + } + + /// `Отправьте жалобу, если кто-то разместил откровенные изображения` + String get explicitImagesDescription { + return Intl.message( + 'Отправьте жалобу, если кто-то разместил откровенные изображения', + name: 'explicitImagesDescription', + desc: '', + args: [], + ); + } + + /// `Жалоба` + String get complaint { + return Intl.message( + 'Жалоба', + name: 'complaint', + desc: '', + args: [], + ); + } + + /// `Что именно в этом материале Вы считаете неприемлемым и нарушающим правила?` + String get complaint_question { + return Intl.message( + 'Что именно в этом материале Вы считаете неприемлемым и нарушающим правила?', + name: 'complaint_question', + desc: '', + args: [], + ); + } + + /// `Отправьте жалобу, если Вы заметили рекламу, размещенную в не предназначенном для этого месте.` + String get spam_description { + return Intl.message( + 'Отправьте жалобу, если Вы заметили рекламу, размещенную в не предназначенном для этого месте.', + name: 'spam_description', + desc: '', + args: [], + ); + } + + /// `На что Вы хотите пожаловаться?` + String get what_do_you_want_to_complaint_about { + return Intl.message( + 'На что Вы хотите пожаловаться?', + name: 'what_do_you_want_to_complaint_about', + desc: '', + args: [], + ); + } + + /// `Отправить жалобу` + String get send_complain { + return Intl.message( + 'Отправить жалобу', + name: 'send_complain', + desc: '', + args: [], + ); + } + + /// `Жалоба успешно отправлена` + String get complaint_sent { + return Intl.message( + 'Жалоба успешно отправлена', + name: 'complaint_sent', + desc: '', + args: [], + ); + } + + /// `Ваше обращение принято! Мы рассмотрим его в ближайшее время и примем необходимые меры. Спасибо за вашу бдительность и помощь в улучшении нашего сервиса.` + String get complaint_sent_description { + return Intl.message( + 'Ваше обращение принято! Мы рассмотрим его в ближайшее время и примем необходимые меры. Спасибо за вашу бдительность и помощь в улучшении нашего сервиса.', + name: 'complaint_sent_description', + desc: '', + args: [], + ); + } + + /// `Хорошо` + String get ok { + return Intl.message( + 'Хорошо', + name: 'ok', + desc: '', + args: [], + ); + } + + /// `Алкоголь` + String get alcohol { + return Intl.message( + 'Алкоголь', + name: 'alcohol', + desc: '', + args: [], + ); + } + + /// `Наркотики` + String get drugs { + return Intl.message( + 'Наркотики', + name: 'drugs', + desc: '', + args: [], + ); + } + + /// `Никотин` + String get nicotine { + return Intl.message( + 'Никотин', + name: 'nicotine', + desc: '', + args: [], + ); + } + + /// `Оружие` + String get weapon { + return Intl.message( + 'Оружие', + name: 'weapon', + desc: '', + args: [], + ); + } + + /// `Поддельные документы` + String get fake_documents { + return Intl.message( + 'Поддельные документы', + name: 'fake_documents', + desc: '', + args: [], + ); + } + + /// `Проституция` + String get prostitution { + return Intl.message( + 'Проституция', + name: 'prostitution', + desc: '', + args: [], + ); + } + + /// `Другое` + String get other { + return Intl.message( + 'Другое', + name: 'other', + desc: '', + args: [], + ); + } + + /// `Введение в заблуждение` + String get misrepresentation { + return Intl.message( + 'Введение в заблуждение', + name: 'misrepresentation', + desc: '', + args: [], + ); + } + + /// `Новости, которые могут вызвать широкий резонанс или несут угрозу, распространяемые с целью введения людей в заблуждение.` + String get misrepresentation_description { + return Intl.message( + 'Новости, которые могут вызвать широкий резонанс или несут угрозу, распространяемые с целью введения людей в заблуждение.', + name: 'misrepresentation_description', + desc: '', + args: [], + ); + } + + /// `Мошенничество` + String get fraud { + return Intl.message( + 'Мошенничество', + name: 'fraud', + desc: '', + args: [], + ); + } + + /// `Мошенничество с целью наживы. Если вас обманули, пожалуйста, сообщите об этом в правоохранительные органы.` + String get fraud_description { + return Intl.message( + 'Мошенничество с целью наживы. Если вас обманули, пожалуйста, сообщите об этом в правоохранительные органы.', + name: 'fraud_description', + desc: '', + args: [], + ); + } + + /// `Насилие над людьми или животными` + String get violence_against_people_or_animal { + return Intl.message( + 'Насилие над людьми или животными', + name: 'violence_against_people_or_animal', + desc: '', + args: [], + ); + } + + /// `Оскорбление` + String get hatred { + return Intl.message( + 'Оскорбление', + name: 'hatred', + desc: '', + args: [], + ); + } + + /// `Склонение к самоубийству` + String get suicide { + return Intl.message( + 'Склонение к самоубийству', + name: 'suicide', + desc: '', + args: [], + ); + } + + /// `Враждебные высказывания` + String get hatred_description { + return Intl.message( + 'Враждебные высказывания', + name: 'hatred_description', + desc: '', + args: [], + ); + } + + /// `Экстремизм` + String get extremism { + return Intl.message( + 'Экстремизм', + name: 'extremism', + desc: '', + args: [], + ); + } + + /// `Призывы к травле` + String get calls_for_bullying { + return Intl.message( + 'Призывы к травле', + name: 'calls_for_bullying', + desc: '', + args: [], + ); + } + + /// `Напишите здесь свою жалобу` + String get type_here_your_complaint { + return Intl.message( + 'Напишите здесь свою жалобу', + name: 'type_here_your_complaint', + desc: '', + args: [], + ); + } + + /// `Не удалось отправить жалобу` + String get could_not_send_complain { + return Intl.message( + 'Не удалось отправить жалобу', + name: 'could_not_send_complain', + desc: '', + args: [], + ); + } + + /// `Пожалуйста, попробуйте еще раз позже` + String get please_try_again_later { + return Intl.message( + 'Пожалуйста, попробуйте еще раз позже', + name: 'please_try_again_later', + desc: '', + args: [], + ); + } + + /// `Рейтинг и статистика` + String get rating_and_statistics { + return Intl.message( + 'Рейтинг и статистика', + name: 'rating_and_statistics', + desc: '', + args: [], + ); + } + + /// `Ваш рейтинг` + String get your_rating { + return Intl.message( + 'Ваш рейтинг', + name: 'your_rating', + desc: '', + args: [], + ); + } + + /// `Скопировать пригласительную ссылку` + String get copy_referral_link { + return Intl.message( + 'Скопировать пригласительную ссылку', + name: 'copy_referral_link', + desc: '', + args: [], + ); + } + + /// `Статистика` + String get statistic { + return Intl.message( + 'Статистика', + name: 'statistic', + desc: '', + args: [], + ); + } + + /// `За всё время` + String get for_whole_period_of_time { + return Intl.message( + 'За всё время', + name: 'for_whole_period_of_time', + desc: '', + args: [], + ); + } + + /// `За год` + String get for_a_year { + return Intl.message( + 'За год', + name: 'for_a_year', + desc: '', + args: [], + ); + } + + /// `За месяц` + String get for_a_month { + return Intl.message( + 'За месяц', + name: 'for_a_month', + desc: '', + args: [], + ); + } + + /// `За неделю` + String get for_a_week { + return Intl.message( + 'За неделю', + name: 'for_a_week', + desc: '', + args: [], + ); + } + + /// `Количество приглашенных` + String get invited_people_count { + return Intl.message( + 'Количество приглашенных', + name: 'invited_people_count', + desc: '', + args: [], + ); + } + + /// `Количество опубликованных постов` + String get published_posts_count { + return Intl.message( + 'Количество опубликованных постов', + name: 'published_posts_count', + desc: '', + args: [], + ); + } + + /// `Кол-во опубликованных рецептов` + String get published_recipes_count { + return Intl.message( + 'Кол-во опубликованных рецептов', + name: 'published_recipes_count', + desc: '', + args: [], + ); + } + + /// `Кол-во опубликованных диет` + String get published_diets_count { + return Intl.message( + 'Кол-во опубликованных диет', + name: 'published_diets_count', + desc: '', + args: [], + ); + } + + /// `Кол-во проведенных консулдьтаций` + String get consultations_count { + return Intl.message( + 'Кол-во проведенных консулдьтаций', + name: 'consultations_count', + desc: '', + args: [], + ); + } + + /// `Кол-во активных консультаций` + String get active_consultations_count { + return Intl.message( + 'Кол-во активных консультаций', + name: 'active_consultations_count', + desc: '', + args: [], + ); + } + + /// `Начисление рейтинга` + String get rating_history { + return Intl.message( + 'Начисление рейтинга', + name: 'rating_history', + desc: '', + args: [], + ); + } + + /// `Упс! Что-то пошло не так` + String get something_went_wrong { + return Intl.message( + 'Упс! Что-то пошло не так', + name: 'something_went_wrong', + desc: '', + args: [], + ); + } + + /// `Не удалось загрузить данные. Попробуйте обновить страницу` + String get can_not_load_data_please_try_again { + return Intl.message( + 'Не удалось загрузить данные. Попробуйте обновить страницу', + name: 'can_not_load_data_please_try_again', + desc: '', + args: [], + ); + } + + /// `Попробовать снова` + String get try_again { + return Intl.message( + 'Попробовать снова', + name: 'try_again', + desc: '', + args: [], + ); + } + + /// `Публикация рецепта` + String get recipe_publishing { + return Intl.message( + 'Публикация рецепта', + name: 'recipe_publishing', + desc: '', + args: [], + ); + } + + /// `успешно опубликован` + String get successfully_published { + return Intl.message( + 'успешно опубликован', + name: 'successfully_published', + desc: '', + args: [], + ); + } + + /// `Упс! Произошла ошибка` + String get some_error_occurred { + return Intl.message( + 'Упс! Произошла ошибка', + name: 'some_error_occurred', + desc: '', + args: [], + ); + } + + /// `Произошла ошибка. Мы уже работаем над ее устранением. Попробуйте позже.` + String get some_error_occurred_description { + return Intl.message( + 'Произошла ошибка. Мы уже работаем над ее устранением. Попробуйте позже.', + name: 'some_error_occurred_description', + desc: '', + args: [], + ); + } + + /// `Не удалось опубликовать рецепт` + String get could_not_publish_recipe { + return Intl.message( + 'Не удалось опубликовать рецепт', + name: 'could_not_publish_recipe', + desc: '', + args: [], + ); + } + + /// `Не удалось опубликовать диету` + String get could_not_publish_diet { + return Intl.message( + 'Не удалось опубликовать диету', + name: 'could_not_publish_diet', + desc: '', + args: [], + ); + } + + /// `Не удалось обновить диету` + String get could_not_update_diet { + return Intl.message( + 'Не удалось обновить диету', + name: 'could_not_update_diet', + desc: '', + args: [], + ); + } + + /// `Не удалось опубликовать пост` + String get could_not_publish_blog_post { + return Intl.message( + 'Не удалось опубликовать пост', + name: 'could_not_publish_blog_post', + desc: '', + args: [], + ); + } + + /// `Не удалось обновить пост` + String get could_not_update_blog_post { + return Intl.message( + 'Не удалось обновить пост', + name: 'could_not_update_blog_post', + desc: '', + args: [], + ); + } + + /// `Не удалось удалить пост` + String get could_not_delete_blog_post { + return Intl.message( + 'Не удалось удалить пост', + name: 'could_not_delete_blog_post', + desc: '', + args: [], + ); + } } class AppLocalizationDelegate extends LocalizationsDelegate { diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 3fb9e4f4..c976ed0e 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -230,5 +230,87 @@ "diet_started": "Диета уже начата", "cancel_diet": "Отменить диету?", "confirm": "Подтвердить", - "could_not_cancel_diet": "Не удалось отменить диету" + "could_not_cancel_diet": "Не удалось отменить диету", + "hours": "часов", + "hour": "час", + "another_form_of_hour": "часа", + "minutes": "минуты", + "minute": "минута", + "another_form_of_minute": "минут", + "there_is_a_video": "Есть видео", + "recipe_author": "Автор рецепта", + "about_recipe": "Об этом рецепте", + "stages": "этапов", + "stage": "этап", + "another_form_of_stage": "этапа", + "check_out_video_recipe": "Посмотреть видео рецепт", + "could_not_launch_video": "Не удалось запустить видео", + "ingredients": "Ингредиенты", + "step": "Шаг", + "step_by_step_photo_recipe": "Пошаговый фоторецепт", + "diet_has_been_sent_to_draft": "снята с публикации и перенесена в черновик", + "spam": "Спам", + "lawViolation": "Нарушение законодательства", + "deception": "Обман", + "violenceAndHostility": "Насилие и вражда", + "explicitImage": "Откровенное изображение", + "explicitImages": "Откровенные изображения", + "explicitImagesDescription": "Отправьте жалобу, если кто-то разместил откровенные изображения", + "complaint": "Жалоба", + "complaint_question": "Что именно в этом материале Вы считаете неприемлемым и нарушающим правила?", + "spam_description": "Отправьте жалобу, если Вы заметили рекламу, размещенную в не предназначенном для этого месте.", + "what_do_you_want_to_complaint_about": "На что Вы хотите пожаловаться?", + "send_complain": "Отправить жалобу", + "complaint_sent": "Жалоба успешно отправлена", + "complaint_sent_description": "Ваше обращение принято! Мы рассмотрим его в ближайшее время и примем необходимые меры. Спасибо за вашу бдительность и помощь в улучшении нашего сервиса.", + "ok": "Хорошо", + "alcohol": "Алкоголь", + "drugs": "Наркотики", + "nicotine": "Никотин", + "weapon": "Оружие", + "fake_documents": "Поддельные документы", + "prostitution": "Проституция", + "other": "Другое", + "misrepresentation": "Введение в заблуждение", + "misrepresentation_description": "Новости, которые могут вызвать широкий резонанс или несут угрозу, распространяемые с целью введения людей в заблуждение.", + "fraud": "Мошенничество", + "fraud_description": "Мошенничество с целью наживы. Если вас обманули, пожалуйста, сообщите об этом в правоохранительные органы.", + "violence_against_people_or_animal": "Насилие над людьми или животными", + "hatred": "Оскорбление", + "suicide": "Склонение к самоубийству", + "hatred_description": "Враждебные высказывания", + "extremism": "Экстремизм", + "calls_for_bullying": "Призывы к травле", + "type_here_your_complaint": "Напишите здесь свою жалобу", + "could_not_send_complain": "Не удалось отправить жалобу", + "please_try_again_later": "Пожалуйста, попробуйте еще раз позже", + "rating_and_statistics": "Рейтинг и статистика", + "your_rating": "Ваш рейтинг", + "copy_referral_link": "Скопировать пригласительную ссылку", + "statistic": "Статистика", + "for_whole_period_of_time": "За всё время", + "for_a_year": "За год", + "for_a_month": "За месяц", + "for_a_week": "За неделю", + "invited_people_count": "Количество приглашенных", + "published_posts_count": "Количество опубликованных постов", + "published_recipes_count": "Кол-во опубликованных рецептов", + "published_diets_count": "Кол-во опубликованных диет", + "consultations_count": "Кол-во проведенных консулдьтаций", + "active_consultations_count": "Кол-во активных консультаций", + "rating_history": "Начисление рейтинга", + "something_went_wrong": "Упс! Что-то пошло не так", + "can_not_load_data_please_try_again": "Не удалось загрузить данные. Попробуйте обновить страницу", + "try_again": "Попробовать снова", + "recipe_publishing": "Публикация рецепта", + "successfully_published": "успешно опубликован", + "some_error_occurred": "Упс! Произошла ошибка", + "some_error_occurred_description": "Произошла ошибка. Мы уже работаем над ее устранением. Попробуйте позже.", + "could_not_publish_recipe": "Не удалось опубликовать рецепт", + "could_not_publish_diet": "Не удалось опубликовать диету", + "could_not_update_diet": "Не удалось обновить диету", + "could_not_publish_blog_post": "Не удалось опубликовать пост", + "could_not_update_blog_post": "Не удалось обновить пост", + "could_not_delete_blog_post": "Не удалось удалить пост" } + diff --git a/lib/main.dart b/lib/main.dart index 5b223f8a..7cdeb5fa 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -5,6 +5,7 @@ import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/new_messages_cubit/new_messages_cubit.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/notifications_cubit/notifications_cubit.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; +import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart'; import 'package:eatvio_app/features/internet_connection_checker/internet_connection_checker_screen.dart'; import 'package:eatvio_app/generated/l10n.dart'; @@ -39,6 +40,7 @@ class MyApp extends StatelessWidget { BlocProvider(create: (context) => sl()), BlocProvider(create: (context) => sl()..loadFeedBlogPostList()), BlocProvider(create: (context) => sl()..getMyProfile()), + BlocProvider(create: (context) => sl()..getChatRoomsWithLoading()), ], child: Portal( child: KeyboardDismisser( diff --git a/pubspec.yaml b/pubspec.yaml index f4226fd9..a7d1a446 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,10 +1,10 @@ name: eatvio_app description: A new Flutter project. version: 1.0.40+40 -publish_to: "none" +publish_to: 'none' environment: - sdk: ">=3.3.4 <4.0.0" + sdk: '>=3.3.4 <4.0.0' dependencies: flutter: @@ -61,9 +61,12 @@ dependencies: webview_flutter: ^4.10.0 sentry_flutter: ^8.13.0 connectivity_plus: ^6.1.3 + visibility_detector: ^0.4.0+2 dependency_overrides: intl: ^0.19.0 + connectivity_plus: ^6.1.2 + dev_dependencies: build_runner: ^2.4.15 @@ -102,4 +105,4 @@ flutter_native_splash: android_12: image: 'assets/splash/splash.png' image_dark: 'assets/splash/splash.png' - branding: 'assets/splash/branding.png' \ No newline at end of file + branding: 'assets/splash/branding.png'