diff --git a/analysis_options.yaml b/analysis_options.yaml index fc61170b..7db716b4 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -8,6 +8,7 @@ analyzer: - '**/l10n.dart' - '**/messages_all.dart' - '**/messages_en.dart' + - '**/messages_ru.dart' errors: invalid_annotation_target: ignore plugins: diff --git a/lib/core/app_router.dart b/lib/core/app_router.dart index 7a90c01c..dd1b63cb 100644 --- a/lib/core/app_router.dart +++ b/lib/core/app_router.dart @@ -22,6 +22,8 @@ import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart'; import 'package:eatvio_app/features/feed/models/recipes_model.dart'; import 'package:eatvio_app/features/feed/presentation/pages/feed_screen.dart'; import 'package:eatvio_app/features/menu/presentation/pages/menu_screen.dart'; +import 'package:eatvio_app/features/my_diets/presentation/my_diets_screen.dart'; +import 'package:eatvio_app/features/my_plans/presentation/my_plans_screen.dart'; import 'package:eatvio_app/features/notifications/presentation/pages/notifications_screen.dart'; import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart'; import 'package:eatvio_app/features/plans/presentation/pages/about_plan_screen.dart'; @@ -39,6 +41,7 @@ import 'package:eatvio_app/features/settings/presentation/pages/physical_activit import 'package:eatvio_app/features/settings/presentation/pages/physical_form_screen.dart'; import 'package:eatvio_app/features/settings/presentation/pages/pressure_screen.dart'; import 'package:eatvio_app/features/settings/presentation/pages/settings_screen.dart'; +import 'package:eatvio_app/features/user_details/presentation/user_details_screen.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; @@ -115,7 +118,7 @@ class AppRouter { final data = state.extra as Map; return AboutPlanScreen( plan: data['plan'] as PlanModel, - // index: data['index'] as int, + index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool, ); }, @@ -154,7 +157,7 @@ class AppRouter { final data = state.extra as Map; return AboutDietScreen( diet: data['diet'] as Diet, - index: data['index'] as int, + index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool, ); }, @@ -282,6 +285,23 @@ class AppRouter { name: CreatePlanScreen.name, builder: (context, state) => CreatePlanScreen(), ), + GoRoute( + path: MyDietsScreen.path, + name: MyDietsScreen.name, + builder: (context, state) => MyDietsScreen(), + ), + GoRoute( + path: MyPlansScreen.path, + name: MyPlansScreen.name, + builder: (context, state) => MyPlansScreen(), + ), + GoRoute( + path: UserDetailsScreen.path, + name: UserDetailsScreen.name, + builder: (context, state) => UserDetailsScreen( + slug: state.extra as String, + ), + ), ], ); } diff --git a/lib/core/const.dart b/lib/core/const.dart index 7ec91b46..24e72cdd 100644 --- a/lib/core/const.dart +++ b/lib/core/const.dart @@ -2,4 +2,5 @@ String baseUrl = 'https://dev2.eatvio.com'; const List durationValues = [7, 14, 21]; -const List typeValues = ['veg', 'light', 'carb', 'meat']; +const List dietTypeValues = ['veg', 'light', 'carb', 'meat']; +const List planTypeValues = ['fit', 'slim', 'wellness', 'behavior']; diff --git a/lib/core/locator_service.dart b/lib/core/locator_service.dart index 95ca1af9..3dda7501 100644 --- a/lib/core/locator_service.dart +++ b/lib/core/locator_service.dart @@ -37,6 +37,12 @@ import 'package:eatvio_app/features/feed/services/feed_service.dart'; import 'package:eatvio_app/features/glossary/repositories/glossary_repository.dart'; import 'package:eatvio_app/features/glossary/services/glossary_service.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_cubit.dart'; +import 'package:eatvio_app/features/my_diets/presentation/cubit/my_diets_cubit.dart'; +import 'package:eatvio_app/features/my_diets/repositories/my_diets_repository.dart'; +import 'package:eatvio_app/features/my_diets/services/my_diets_service.dart'; +import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_cubit.dart'; +import 'package:eatvio_app/features/my_plans/repositories/my_diets_repository.dart'; +import 'package:eatvio_app/features/my_plans/services/my_plans_service.dart'; import 'package:eatvio_app/features/plans/repositories/plans_repository.dart'; import 'package:eatvio_app/features/plans/services/plans_service.dart'; import 'package:eatvio_app/features/plans/presentation/cubit/about_plan_cubit.dart'; @@ -55,6 +61,7 @@ import 'package:eatvio_app/features/registration/services/registration_service.d import 'package:eatvio_app/features/settings/presentation/bloc/settings_cubit/settings_cubit.dart'; import 'package:eatvio_app/features/settings/repositories/service_repository.dart'; import 'package:eatvio_app/features/settings/services/settings_service.dart'; +import 'package:eatvio_app/features/user_details/presentation/cubit/user_details_cubit.dart'; import 'package:get_it/get_it.dart'; import 'package:eatvio_app/features/profile/presentation/bloc/plans_cubit/plans_cubit.dart' as profilePlans; @@ -78,7 +85,7 @@ Future init() async { sl.registerFactory(() => DietsCubit(feedRepository: sl(), dietsRepository: sl())); sl.registerFactory(() => CommentsCubit(feedRepository: sl())); sl.registerFactory(() => ChatSelectCubit(chatRepository: sl())); - sl.registerFactory(() => PlansCubit(feedRepository: sl())); + sl.registerFactory(() => PlansCubit(feedRepository: sl(), planRepository: sl())); sl.registerFactory(() => SettingsCubit(profileRepository: sl(), settingsRepository: sl())); sl.registerFactory(() => RegisterConsultantCubit(consultantRepository: sl())); sl.registerFactory(() => DiaryCubit(diaryRepository: sl(), glossaryRepository: sl())); @@ -90,6 +97,9 @@ Future init() async { sl.registerFactory(() => FeedProfileCubit(profileRepository: sl())); sl.registerFactory(() => AboutPlanCubit(planRepository: sl())); sl.registerFactory(() => CreateRecipeCubit()); + sl.registerFactory(() => MyDietsCubit(myDietsRepository: sl())); + sl.registerFactory(() => MyPlansCubit(myPlansRepository: sl())); + sl.registerFactory(() => UserDetailsCubit(profileRepository: sl())); // Repository sl.registerLazySingleton(() => ChatRepository(chatService: sl())); @@ -107,6 +117,8 @@ Future init() async { sl.registerLazySingleton(() => CreateDietRepository(createDietService: sl())); sl.registerLazySingleton(() => PlansRepository(plansService: sl())); sl.registerLazySingleton(() => PlanRepository(planService: sl())); + sl.registerLazySingleton(() => MyDietsRepository(myDietsService: sl())); + sl.registerLazySingleton(() => MyPlansRepository(myPlansService: sl())); // Services sl.registerLazySingleton(() => ChatService(sl())); @@ -124,6 +136,8 @@ Future init() async { sl.registerLazySingleton(() => CreateDietService(sl())); sl.registerLazySingleton(() => PlansService(sl())); sl.registerLazySingleton(() => PlanService(sl())); + sl.registerLazySingleton(() => MyDietsService(sl())); + sl.registerLazySingleton(() => MyPlansService(sl())); // External sl.registerLazySingleton( diff --git a/lib/core/models/pagination_model.dart b/lib/core/models/pagination_model.dart index d54863b6..36f21ed7 100644 --- a/lib/core/models/pagination_model.dart +++ b/lib/core/models/pagination_model.dart @@ -26,6 +26,8 @@ class Meta with _$Meta { @JsonKey(name: "per_page") required int perPage, @JsonKey(name: "to") required dynamic to, @JsonKey(name: "total") required int total, + @JsonKey(name: "total_min_price") int? totalMinPrice, + @JsonKey(name: "total_max_price") int? totalMaxPrice, }) = _Meta; factory Meta.fromJson(Map json) => _$MetaFromJson(json); diff --git a/lib/core/models/pagination_model.freezed.dart b/lib/core/models/pagination_model.freezed.dart index 35f2c080..4d014abf 100644 --- a/lib/core/models/pagination_model.freezed.dart +++ b/lib/core/models/pagination_model.freezed.dart @@ -262,6 +262,10 @@ mixin _$Meta { dynamic get to => throw _privateConstructorUsedError; @JsonKey(name: "total") int get total => throw _privateConstructorUsedError; + @JsonKey(name: "total_min_price") + int? get totalMinPrice => throw _privateConstructorUsedError; + @JsonKey(name: "total_max_price") + int? get totalMaxPrice => throw _privateConstructorUsedError; /// Serializes this Meta to a JSON map. Map toJson() => throw _privateConstructorUsedError; @@ -285,7 +289,9 @@ abstract class $MetaCopyWith<$Res> { @JsonKey(name: "path") String? path, @JsonKey(name: "per_page") int perPage, @JsonKey(name: "to") dynamic to, - @JsonKey(name: "total") int total}); + @JsonKey(name: "total") int total, + @JsonKey(name: "total_min_price") int? totalMinPrice, + @JsonKey(name: "total_max_price") int? totalMaxPrice}); } /// @nodoc @@ -311,6 +317,8 @@ class _$MetaCopyWithImpl<$Res, $Val extends Meta> Object? perPage = null, Object? to = freezed, Object? total = null, + Object? totalMinPrice = freezed, + Object? totalMaxPrice = freezed, }) { return _then(_value.copyWith( currentPage: null == currentPage @@ -345,6 +353,14 @@ class _$MetaCopyWithImpl<$Res, $Val extends Meta> ? _value.total : total // ignore: cast_nullable_to_non_nullable as int, + totalMinPrice: freezed == totalMinPrice + ? _value.totalMinPrice + : totalMinPrice // ignore: cast_nullable_to_non_nullable + as int?, + totalMaxPrice: freezed == totalMaxPrice + ? _value.totalMaxPrice + : totalMaxPrice // ignore: cast_nullable_to_non_nullable + as int?, ) as $Val); } } @@ -364,7 +380,9 @@ abstract class _$$MetaImplCopyWith<$Res> implements $MetaCopyWith<$Res> { @JsonKey(name: "path") String? path, @JsonKey(name: "per_page") int perPage, @JsonKey(name: "to") dynamic to, - @JsonKey(name: "total") int total}); + @JsonKey(name: "total") int total, + @JsonKey(name: "total_min_price") int? totalMinPrice, + @JsonKey(name: "total_max_price") int? totalMaxPrice}); } /// @nodoc @@ -387,6 +405,8 @@ class __$$MetaImplCopyWithImpl<$Res> Object? perPage = null, Object? to = freezed, Object? total = null, + Object? totalMinPrice = freezed, + Object? totalMaxPrice = freezed, }) { return _then(_$MetaImpl( currentPage: null == currentPage @@ -421,6 +441,14 @@ class __$$MetaImplCopyWithImpl<$Res> ? _value.total : total // ignore: cast_nullable_to_non_nullable as int, + totalMinPrice: freezed == totalMinPrice + ? _value.totalMinPrice + : totalMinPrice // ignore: cast_nullable_to_non_nullable + as int?, + totalMaxPrice: freezed == totalMaxPrice + ? _value.totalMaxPrice + : totalMaxPrice // ignore: cast_nullable_to_non_nullable + as int?, )); } } @@ -436,7 +464,9 @@ class _$MetaImpl implements _Meta { @JsonKey(name: "path") this.path, @JsonKey(name: "per_page") required this.perPage, @JsonKey(name: "to") required this.to, - @JsonKey(name: "total") required this.total}) + @JsonKey(name: "total") required this.total, + @JsonKey(name: "total_min_price") this.totalMinPrice, + @JsonKey(name: "total_max_price") this.totalMaxPrice}) : _links = links; factory _$MetaImpl.fromJson(Map json) => @@ -472,10 +502,16 @@ class _$MetaImpl implements _Meta { @override @JsonKey(name: "total") final int total; + @override + @JsonKey(name: "total_min_price") + final int? totalMinPrice; + @override + @JsonKey(name: "total_max_price") + final int? totalMaxPrice; @override String toString() { - return 'Meta(currentPage: $currentPage, from: $from, lastPage: $lastPage, links: $links, path: $path, perPage: $perPage, to: $to, total: $total)'; + return 'Meta(currentPage: $currentPage, from: $from, lastPage: $lastPage, links: $links, path: $path, perPage: $perPage, to: $to, total: $total, totalMinPrice: $totalMinPrice, totalMaxPrice: $totalMaxPrice)'; } @override @@ -492,7 +528,11 @@ class _$MetaImpl implements _Meta { (identical(other.path, path) || other.path == path) && (identical(other.perPage, perPage) || other.perPage == perPage) && const DeepCollectionEquality().equals(other.to, to) && - (identical(other.total, total) || other.total == total)); + (identical(other.total, total) || other.total == total) && + (identical(other.totalMinPrice, totalMinPrice) || + other.totalMinPrice == totalMinPrice) && + (identical(other.totalMaxPrice, totalMaxPrice) || + other.totalMaxPrice == totalMaxPrice)); } @JsonKey(includeFromJson: false, includeToJson: false) @@ -506,7 +546,9 @@ class _$MetaImpl implements _Meta { path, perPage, const DeepCollectionEquality().hash(to), - total); + total, + totalMinPrice, + totalMaxPrice); /// Create a copy of Meta /// with the given fields replaced by the non-null parameter values. @@ -533,7 +575,9 @@ abstract class _Meta implements Meta { @JsonKey(name: "path") final String? path, @JsonKey(name: "per_page") required final int perPage, @JsonKey(name: "to") required final dynamic to, - @JsonKey(name: "total") required final int total}) = _$MetaImpl; + @JsonKey(name: "total") required final int total, + @JsonKey(name: "total_min_price") final int? totalMinPrice, + @JsonKey(name: "total_max_price") final int? totalMaxPrice}) = _$MetaImpl; factory _Meta.fromJson(Map json) = _$MetaImpl.fromJson; @@ -561,6 +605,12 @@ abstract class _Meta implements Meta { @override @JsonKey(name: "total") int get total; + @override + @JsonKey(name: "total_min_price") + int? get totalMinPrice; + @override + @JsonKey(name: "total_max_price") + int? get totalMaxPrice; /// Create a copy of Meta /// with the given fields replaced by the non-null parameter values. diff --git a/lib/core/models/pagination_model.g.dart b/lib/core/models/pagination_model.g.dart index 610d19b2..b89eb0b8 100644 --- a/lib/core/models/pagination_model.g.dart +++ b/lib/core/models/pagination_model.g.dart @@ -32,6 +32,8 @@ _$MetaImpl _$$MetaImplFromJson(Map json) => _$MetaImpl( perPage: (json['per_page'] as num).toInt(), to: json['to'], total: (json['total'] as num).toInt(), + totalMinPrice: (json['total_min_price'] as num?)?.toInt(), + totalMaxPrice: (json['total_max_price'] as num?)?.toInt(), ); Map _$$MetaImplToJson(_$MetaImpl instance) => @@ -44,6 +46,8 @@ Map _$$MetaImplToJson(_$MetaImpl instance) => 'per_page': instance.perPage, 'to': instance.to, 'total': instance.total, + 'total_min_price': instance.totalMinPrice, + 'total_max_price': instance.totalMaxPrice, }; _$LinkImpl _$$LinkImplFromJson(Map json) => _$LinkImpl( diff --git a/lib/core/theme/custom_text_style.dart b/lib/core/theme/custom_text_style.dart index 7f00ad9b..a4e2539d 100644 --- a/lib/core/theme/custom_text_style.dart +++ b/lib/core/theme/custom_text_style.dart @@ -224,7 +224,7 @@ abstract class TextStyles { fontSize: 12, fontWeight: FontWeight.w500, height: 1.255, - ); + ); static const textFS10FW500 = TextStyle( fontSize: 12, diff --git a/lib/core/utils/date_time_utils.dart b/lib/core/utils/date_time_utils.dart index 574799cd..b6c1c86e 100644 --- a/lib/core/utils/date_time_utils.dart +++ b/lib/core/utils/date_time_utils.dart @@ -13,12 +13,24 @@ extension DateTimeExtension on DateTime { } return DateFormat(pattern, locale).format(this); } + + bool isYesterday(DateTime date) { + final DateTime localDate = date.isUtc ? date.toLocal() : date; + final yesterday = DateTime.now().subtract(const Duration(days: 1)); + return yesterday.day == localDate.day && yesterday.month == localDate.month && yesterday.year == localDate.year; + } + + bool isToday(DateTime date) { + final DateTime localDate = date.isUtc ? date.toLocal() : date; + final now = DateTime.now(); + return now.day == localDate.day && now.month == localDate.month && now.year == localDate.year; + } } String calculateDifference(DateTime dateTime) { - if (dateTime.difference(DateTime.now()).inDays == 0) { + if (dateTime.isToday(dateTime)) { return 'Сегодня'; - } else if (dateTime.difference(DateTime.now()).inDays == -1) { + } else if (dateTime.isYesterday(dateTime)) { return 'Вчера'; } else { return dateTime.format('d MMMM', 'ru'); diff --git a/lib/core/utils/string_extentions.dart b/lib/core/utils/string_extentions.dart index f474175e..1d366a54 100644 --- a/lib/core/utils/string_extentions.dart +++ b/lib/core/utils/string_extentions.dart @@ -2,4 +2,13 @@ extension Capitalization on String { String capitalize() { return "${this[0].toUpperCase()}${substring(1).toLowerCase()}"; } -} \ No newline at end of file + + String parseGender() { + if (this == 'male') { + return 'Мужской'; + } else if (this == 'female') { + return 'Женский'; + } + return this; + } +} diff --git a/lib/core/widgets/add_item_button.dart b/lib/core/widgets/add_item_button.dart new file mode 100644 index 00000000..4a0f9816 --- /dev/null +++ b/lib/core/widgets/add_item_button.dart @@ -0,0 +1,30 @@ +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +class AddItemButton extends StatelessWidget { + const AddItemButton({super.key, this.onTap}); + final Function()? onTap; + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap ?? + () { + GoRouter.of(context).pushNamed('blogPostCreateScreen'); + }, + borderRadius: BorderRadius.circular(64), + child: Ink( + decoration: BoxDecoration( + color: appTheme.blueA400, + shape: BoxShape.circle, + ), + child: Icon( + Icons.add_rounded, + color: appTheme.gray100, + ), + width: 40, + height: 40, + ), + ); + } +} \ No newline at end of file diff --git a/lib/core/widgets/animated_option_button.dart b/lib/core/widgets/animated_option_button.dart index f196c042..4491293b 100644 --- a/lib/core/widgets/animated_option_button.dart +++ b/lib/core/widgets/animated_option_button.dart @@ -3,11 +3,11 @@ import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:flutter/material.dart'; class AnimatedOptionButton extends StatelessWidget { - const AnimatedOptionButton({required this.text, required this.isSelected, required this.onTap}); + const AnimatedOptionButton({required this.text, required this.isSelected, required this.onTap, this.textStyle}); final String text; final bool isSelected; final void Function() onTap; - + final TextStyle? textStyle; @override Widget build(BuildContext context) { return GestureDetector( @@ -22,9 +22,12 @@ class AnimatedOptionButton extends StatelessWidget { child: Center( child: AnimatedDefaultTextStyle( duration: const Duration(milliseconds: 300), - style: TextStyles.textMSemibold.copyWith( - color: isSelected ? appTheme.whiteA700 : appTheme.black, - ), + style: textStyle?.copyWith( + color: isSelected ? appTheme.whiteA700 : appTheme.black, + ) ?? + TextStyles.textMSemibold.copyWith( + color: isSelected ? appTheme.whiteA700 : appTheme.black, + ), child: Text(text), ), ), diff --git a/lib/core/widgets/bottom_sheet_with_blur.dart b/lib/core/widgets/bottom_sheet_with_blur.dart new file mode 100644 index 00000000..303170c4 --- /dev/null +++ b/lib/core/widgets/bottom_sheet_with_blur.dart @@ -0,0 +1,45 @@ +import 'dart:ui'; + +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:flutter/material.dart'; + +Future showBottomSheetWithBlur({ + required BuildContext context, + required Widget child, + double height = 200, + bool hasBlur = true, + bool enableDrag = true, + }) { + return showModalBottomSheet( + context: context, + isScrollControlled: true, + barrierColor: appTheme.transparentBlack, + backgroundColor: Colors.transparent, + elevation: 0, + useSafeArea: true, + enableDrag: enableDrag, + builder: (context) => GestureDetector( + behavior: HitTestBehavior.opaque, + child: BackdropFilter( + filter: ImageFilter.blur( + sigmaX: hasBlur ? 24 : 0, + sigmaY: hasBlur ? 24 : 0, + tileMode: TileMode.mirror, + ), + child: SizedBox( + child: Container( + height: height, + decoration: BoxDecoration( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ), + color: appTheme.whiteA700, + ), + child: child, + ), + ), + ), + ), + ); + } \ No newline at end of file diff --git a/lib/core/widgets/custom_outlined_button.dart b/lib/core/widgets/custom_outlined_button.dart index 38a9be27..e8692a87 100644 --- a/lib/core/widgets/custom_outlined_button.dart +++ b/lib/core/widgets/custom_outlined_button.dart @@ -69,7 +69,7 @@ class CustomOutlinedButton extends BaseButton { width: this.width ?? double.maxFinite, margin: margin, decoration: decoration, - child: OutlinedButton( + child: OutlinedButton( style: buttonStyle!, onPressed: isDisabled ?? false ? null : onPressed ?? () {}, child: Row( diff --git a/lib/core/widgets/custom_search_bar.dart b/lib/core/widgets/custom_search_bar.dart new file mode 100644 index 00000000..94b3c5c4 --- /dev/null +++ b/lib/core/widgets/custom_search_bar.dart @@ -0,0 +1,59 @@ +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:flutter/material.dart'; + +class CustomSearchBar extends StatelessWidget { + const CustomSearchBar({super.key, required this.searchController, required this.onChanged, this.backgroundColor, this.borderRadius, this.enableIcon = true, this.width}); + final Color? backgroundColor; + final double? width; + final double? borderRadius; + final TextEditingController searchController; + final void Function(String) onChanged; + final bool enableIcon; + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 40, + width: width ?? MediaQuery.of(context).size.width - 84, + child: TextFormField( + controller: searchController, + autofocus: false, + style: TextStyle( + fontSize: 12, + ), + onChanged: onChanged, + decoration: InputDecoration( + hintText: 'Поиск', + hintStyle: TextStyle( + fontSize: 12, + color: appTheme.gray600, + fontWeight: FontWeight.w400, + ), + prefixIcon: enableIcon + ? Icon( + Icons.search, + color: appTheme.gray600, + size: 16, + ) + : null, + isDense: false, + contentPadding: enableIcon ? EdgeInsets.all(8) : EdgeInsets.only(top: 8, left: 16, right: 8, bottom: 8), + fillColor: backgroundColor ?? appTheme.gray100, + filled: true, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(borderRadius ?? 16), + borderSide: BorderSide.none, + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(borderRadius ?? 16), + borderSide: BorderSide.none, + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(borderRadius ?? 16), + borderSide: BorderSide.none, + ), + ), + ), + ); + } +} diff --git a/lib/core/widgets/custom_text_form_field.dart b/lib/core/widgets/custom_text_form_field.dart index 85b9c7ce..490255db 100644 --- a/lib/core/widgets/custom_text_form_field.dart +++ b/lib/core/widgets/custom_text_form_field.dart @@ -41,6 +41,7 @@ class CustomTextFormField extends StatelessWidget { this.enabled = true, super.key, this.hasError = false, + this.readOnly = false, }); final Widget? counter; @@ -79,13 +80,15 @@ class CustomTextFormField extends StatelessWidget { final TextAlignVertical? textAlignVertical; final bool hasError; final bool enabled; - + // final TextAlign + final bool readOnly; @override Widget build(BuildContext context) { return Align( alignment: alignment ?? Alignment.center, child: TextFormField( enabled: enabled, + readOnly: readOnly, textAlignVertical: textAlignVertical, expands: expands, minLines: minLines, diff --git a/lib/core/widgets/diet_info_row.dart b/lib/core/widgets/info_row.dart similarity index 100% rename from lib/core/widgets/diet_info_row.dart rename to lib/core/widgets/info_row.dart diff --git a/lib/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart b/lib/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart index 48b7474d..1ee52bf2 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 @@ -28,7 +28,7 @@ class BottomNavigationBarWidget extends StatefulWidget { } class _BottomNavigationBarWidgetState extends State { - void _onItemTapped(int index) { + void _onItemTapped(int index) async { if (currentBottomNavBarIndex != index) { setState(() { currentBottomNavBarIndex = index; 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 index b190cb27..bc8f7a96 100644 --- 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 @@ -12,9 +12,12 @@ class ChatSelectCubit extends Cubit { getChatRooms({String? query}) async { emit(ChatSelectState.loading()); try { - emit(ChatSelectState.loaded( - chatRooms: (await chatRepository.getChatRoomPreviews(query: query)), - ),); + 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/create_diet/presentation/create_diet_screen.dart b/lib/features/create_diet/presentation/create_diet_screen.dart index a4425e10..49b23577 100644 --- a/lib/features/create_diet/presentation/create_diet_screen.dart +++ b/lib/features/create_diet/presentation/create_diet_screen.dart @@ -1,10 +1,9 @@ -import 'dart:ui'; - import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/utils/localization_utils.dart'; import 'package:eatvio_app/core/widgets/bottom_sheet_handler_bar.dart'; +import 'package:eatvio_app/core/widgets/bottom_sheet_with_blur.dart'; import 'package:eatvio_app/core/widgets/custom_outlined_button.dart'; import 'package:eatvio_app/core/widgets/custom_text_form_field.dart'; import 'package:eatvio_app/core/widgets/main_background_bouble.dart'; @@ -47,17 +46,92 @@ class CreateDietScreenContent extends StatelessWidget { if (state.published) { WidgetsBinding.instance.addPostFrameCallback((_) { context.pop(); - showPublishSuccessBottomSheet( + showBottomSheetWithBlur( context: context, - text: '${state.dietData.name ?? 'Диета'}', + child: Column( + children: [ + SizedBox( + height: 20, + ), + BottomSheetHandlerBar(), + SizedBox( + height: 20, + ), + Text( + '${state.dietData.name ?? 'Диета'}', + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), + ), + Text( + 'опубликованна', + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), + ), + SizedBox( + height: 20, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: CustomOutlinedButton( + onPressed: () { + context.pop(); + }, + text: 'Хорошо', + height: 52, + buttonTextStyle: TextStyles.textSSemibold.copyWith(color: appTheme.whiteA700), + buttonStyle: ButtonStyle( + side: WidgetStatePropertyAll(BorderSide.none), + backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), + ), + ), + ), + SizedBox( + height: 20, + ), + ], + ), ); }); } else if (state.drafted) { WidgetsBinding.instance.addPostFrameCallback((_) { context.pop(); - showDraftSuccessBottomSheet( + showBottomSheetWithBlur( context: context, - text: '${state.dietData.name ?? 'Диета'} сохраненна в черновиках', + child: Column( + children: [ + SizedBox( + height: 20, + ), + BottomSheetHandlerBar(), + SizedBox( + height: 20, + ), + Text( + '${state.dietData.name ?? 'Диета'} сохраненна в черновиках', + textAlign: TextAlign.center, + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), + ), + SizedBox( + height: 20, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: CustomOutlinedButton( + onPressed: () { + context.pop(); + }, + text: 'Хорошо', + height: 52, + buttonTextStyle: TextStyles.textSSemibold.copyWith(color: appTheme.whiteA700), + buttonStyle: ButtonStyle( + side: WidgetStatePropertyAll(BorderSide.none), + backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), + ), + ), + ), + SizedBox( + height: 20, + ), + ], + ), ); }); } @@ -208,160 +282,5 @@ class CreateDietScreenContent extends StatelessWidget { ); }, ); - } - - Future showDraftSuccessBottomSheet({ - required BuildContext context, - required String text, - bool hasBlur = true, - bool enableDrag = true, - }) { - return showModalBottomSheet( - context: context, - isScrollControlled: true, - barrierColor: appTheme.transparentBlack, - backgroundColor: Colors.transparent, - elevation: 0, - useSafeArea: true, - enableDrag: enableDrag, - builder: (context) => GestureDetector( - behavior: HitTestBehavior.opaque, - child: BackdropFilter( - filter: ImageFilter.blur( - sigmaX: hasBlur ? 24 : 0, - sigmaY: hasBlur ? 24 : 0, - tileMode: TileMode.mirror, - ), - child: SizedBox( - child: Container( - height: 200, - decoration: BoxDecoration( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(20), - topRight: Radius.circular(20), - ), - color: appTheme.whiteA700, - ), - child: Column( - children: [ - SizedBox( - height: 20, - ), - BottomSheetHandlerBar(), - SizedBox( - height: 20, - ), - Text( - text, - textAlign: TextAlign.center, - style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), - ), - SizedBox( - height: 20, - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: CustomOutlinedButton( - onPressed: () { - context.pop(); - }, - text: 'Хорошо', - height: 52, - buttonTextStyle: TextStyles.textSSemibold.copyWith(color: appTheme.whiteA700), - buttonStyle: ButtonStyle( - side: WidgetStatePropertyAll(BorderSide.none), - backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), - ), - ), - ), - SizedBox( - height: 20, - ), - ], - ), - ), - ), - ), - ), - ); - } - - Future showPublishSuccessBottomSheet({ - required BuildContext context, - required String text, - bool hasBlur = true, - bool enableDrag = true, - }) { - return showModalBottomSheet( - context: context, - isScrollControlled: true, - barrierColor: appTheme.transparentBlack, - backgroundColor: Colors.transparent, - elevation: 0, - useSafeArea: true, - enableDrag: enableDrag, - builder: (context) => GestureDetector( - behavior: HitTestBehavior.opaque, - child: BackdropFilter( - filter: ImageFilter.blur( - sigmaX: hasBlur ? 24 : 0, - sigmaY: hasBlur ? 24 : 0, - tileMode: TileMode.mirror, - ), - child: SizedBox( - child: Container( - height: 200, - decoration: BoxDecoration( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(20), - topRight: Radius.circular(20), - ), - color: appTheme.whiteA700, - ), - child: Column( - children: [ - SizedBox( - height: 20, - ), - BottomSheetHandlerBar(), - SizedBox( - height: 20, - ), - Text( - text, - style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), - ), - Text( - 'опубликованна', - style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), - ), - SizedBox( - height: 20, - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: CustomOutlinedButton( - onPressed: () { - context.pop(); - }, - text: 'Хорошо', - height: 52, - buttonTextStyle: TextStyles.textSSemibold.copyWith(color: appTheme.whiteA700), - buttonStyle: ButtonStyle( - side: WidgetStatePropertyAll(BorderSide.none), - backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), - ), - ), - ), - SizedBox( - height: 20, - ), - ], - ), - ), - ), - ), - ), - ); - } + } } diff --git a/lib/features/create_diet/presentation/cubit/create_diet_state.dart b/lib/features/create_diet/presentation/cubit/create_diet_state.dart index 16bec064..b8b85b79 100644 --- a/lib/features/create_diet/presentation/cubit/create_diet_state.dart +++ b/lib/features/create_diet/presentation/cubit/create_diet_state.dart @@ -29,7 +29,7 @@ class CreateDietState with _$CreateDietState { isLoading: false, errorMessage: null, dietData: CreateDietModel( - type: Map.fromIterable(typeValues, value: (element) => false), + type: Map.fromIterable(dietTypeValues, value: (element) => false), ), ); } diff --git a/lib/features/create_diet/presentation/diet_preview_screen.dart b/lib/features/create_diet/presentation/diet_preview_screen.dart index f06c247e..7459be0d 100644 --- a/lib/features/create_diet/presentation/diet_preview_screen.dart +++ b/lib/features/create_diet/presentation/diet_preview_screen.dart @@ -5,7 +5,7 @@ import 'package:eatvio_app/core/utils/localization_utils.dart'; import 'package:eatvio_app/core/utils/string_extentions.dart'; import 'package:eatvio_app/core/widgets/bookmark_button.dart'; import 'package:eatvio_app/core/widgets/custom_divider.dart'; -import 'package:eatvio_app/core/widgets/diet_info_row.dart'; +import 'package:eatvio_app/core/widgets/info_row.dart'; import 'package:eatvio_app/features/diets/presentation/widgets/author_card.dart'; import 'package:eatvio_app/core/widgets/cpfc_container.dart'; import 'package:eatvio_app/features/diets/presentation/widgets/expandable_text.dart'; @@ -190,7 +190,10 @@ class _DietPreviewScreenState extends State { actions: [ PortalTarget( visible: isMoreOptionsVisible, - portalFollower: MoreOptions(disableActions: true, id: widget.diet.id), + portalFollower: MoreOptions( + disableActions: true, + path: 'diet/${widget.diet.id}', + ), anchor: Aligned( shiftToWithinBound: AxisFlag(x: true, y: true), follower: Alignment.topRight, diff --git a/lib/features/create_diet/presentation/widgets/diet_type_choice_widget.dart b/lib/features/create_diet/presentation/widgets/diet_type_choice_widget.dart index fec8d5a2..184f43bd 100644 --- a/lib/features/create_diet/presentation/widgets/diet_type_choice_widget.dart +++ b/lib/features/create_diet/presentation/widgets/diet_type_choice_widget.dart @@ -23,7 +23,6 @@ class _DietTypeChoiceWidgetState extends State { super.didChangeDependencies(); final s = S.of(context); if (choiceContent.isEmpty) { - choiceContent.add( ( 'assets/icons/broccoli.svg', @@ -41,7 +40,7 @@ class _DietTypeChoiceWidgetState extends State { 'assets/icons/bread.svg', s.carb_type, ), - ); + ); choiceContent.add( ( 'assets/icons/meat2.svg', @@ -61,7 +60,7 @@ class _DietTypeChoiceWidgetState extends State { return Expanded( child: GestureDetector( onTap: () { - final key = typeValues[index]; + final key = dietTypeValues[index]; final value = !(state.dietData.type[key] ?? true); context.read().setDietType(key: key, value: value); }, @@ -69,10 +68,10 @@ class _DietTypeChoiceWidgetState extends State { margin: EdgeInsets.only(right: index == 3 ? 0 : 4), decoration: BoxDecoration( borderRadius: BorderRadius.circular(5), - color: state.dietData.type[typeValues[index]] == true ? appTheme.cardBackground : appTheme.whiteA700, + color: state.dietData.type[dietTypeValues[index]] == true ? appTheme.cardBackground : appTheme.whiteA700, border: Border.all( width: 1, - color: state.dietData.type[typeValues[index]] == true ? appTheme.blueA400 : appTheme.lightGray00, + color: state.dietData.type[dietTypeValues[index]] == true ? appTheme.blueA400 : appTheme.lightGray00, ), ), height: 80, diff --git a/lib/features/diets/presentation/cubit/about_diet_cubit.dart b/lib/features/diets/presentation/cubit/about_diet_cubit.dart index b2780516..54215fe9 100644 --- a/lib/features/diets/presentation/cubit/about_diet_cubit.dart +++ b/lib/features/diets/presentation/cubit/about_diet_cubit.dart @@ -14,7 +14,7 @@ class AboutDietCubit extends Cubit { try { final diet = await dietsRepository.getDiet(id); if (index != null) { - dietsRepository.emitCommentsStateToFeed(DietCommentsState(index: index, totalComments: diet.totalComments)); + dietsRepository.emitCommentsStateToFeed(CommentsState(index: index, totalComments: diet.totalComments)); } emit(AboutDietState.loaded(diet)); } catch (e) { @@ -32,7 +32,7 @@ class AboutDietCubit extends Cubit { } } - Future likeDiet(int id, int dietIndexInFeed) async { + Future likeDiet(int id, int? dietIndexInFeed) async { if (state is! DietStateLoaded) return; final currentDiet = (state as DietStateLoaded).diet; @@ -42,15 +42,18 @@ class AboutDietCubit extends Cubit { totalLikes: currentDiet.totalLikes - 1, wasLikedByMe: false, ); - dietsRepository.emitLikesStateToFeed(DietLikesState(wasLikedByMe: updatedDiet.wasLikedByMe, totalLikes: updatedDiet.totalLikes, index: dietIndexInFeed)); + if (dietIndexInFeed != null) { + dietsRepository.emitLikesStateToFeed(LikesState(wasLikedByMe: updatedDiet.wasLikedByMe, totalLikes: updatedDiet.totalLikes, index: dietIndexInFeed)); + } emit(AboutDietState.loaded(updatedDiet)); } else { final updatedDiet = currentDiet.copyWith( totalLikes: currentDiet.totalLikes + 1, wasLikedByMe: true, ); - - dietsRepository.emitLikesStateToFeed(DietLikesState(wasLikedByMe: updatedDiet.wasLikedByMe, totalLikes: updatedDiet.totalLikes, index: dietIndexInFeed)); + if (dietIndexInFeed != null) { + dietsRepository.emitLikesStateToFeed(LikesState(wasLikedByMe: updatedDiet.wasLikedByMe, totalLikes: updatedDiet.totalLikes, index: dietIndexInFeed)); + } emit(AboutDietState.loaded(updatedDiet)); } @@ -62,15 +65,17 @@ class AboutDietCubit extends Cubit { await dietsRepository.likeDiet(id); } catch (e) { log(e.toString()); - dietsRepository - .emitLikesStateToFeed(DietLikesState(wasLikedByMe: currentDiet.wasLikedByMe, totalLikes: currentDiet.totalLikes, index: dietIndexInFeed)); + if (dietIndexInFeed != null) { + dietsRepository + .emitLikesStateToFeed(LikesState(wasLikedByMe: currentDiet.wasLikedByMe, totalLikes: currentDiet.totalLikes, index: dietIndexInFeed)); + } emit(AboutDietState.loaded(currentDiet)); } }, ); } - Future addToFavorite(int id, int dietIndexInFeed) async { + Future addToFavorite(int id, int? dietIndex) async { if (state is! DietStateLoaded) return; final currentDiet = (state as DietStateLoaded).diet; @@ -78,11 +83,13 @@ class AboutDietCubit extends Cubit { final updatedDiet = currentDiet.copyWith( isFavorite: true, ); - dietsRepository.emitFavoriteStateToFeed(DietFavoriteState(isFavorite: updatedDiet.isFavorite, index: dietIndexInFeed)); + if (dietIndex != null) { + dietsRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: updatedDiet.isFavorite, index: dietIndex)); + } emit(AboutDietState.loaded(updatedDiet)); EasyDebounce.debounce( - 'add_diet_to_favorites', + 'add_or_remove_diet_to_favorites', Duration(seconds: 1), () async { try { @@ -90,14 +97,16 @@ class AboutDietCubit extends Cubit { } catch (e) { log(e.toString()); showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время'); - dietsRepository.emitFavoriteStateToFeed(DietFavoriteState(isFavorite: currentDiet.isFavorite, index: dietIndexInFeed)); + if (dietIndex != null) { + dietsRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: currentDiet.isFavorite, index: dietIndex)); + } emit(AboutDietState.loaded(currentDiet)); } }, ); } - Future removeFromFavorite(int id, int dietIndexInFeed) async { + Future removeFromFavorite(int id, int? dietIndex) async { if (state is! DietStateLoaded) return; final currentDiet = (state as DietStateLoaded).diet; @@ -105,11 +114,13 @@ class AboutDietCubit extends Cubit { final updatedDiet = currentDiet.copyWith( isFavorite: false, ); - dietsRepository.emitFavoriteStateToFeed(DietFavoriteState(isFavorite: updatedDiet.isFavorite, index: dietIndexInFeed)); + if (dietIndex != null) { + dietsRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: updatedDiet.isFavorite, index: dietIndex)); + } emit(AboutDietState.loaded(updatedDiet)); EasyDebounce.debounce( - 'remove_diet_from_favorites', + 'add_or_remove_diet_to_favorites', Duration(seconds: 1), () async { try { @@ -117,7 +128,9 @@ class AboutDietCubit extends Cubit { } catch (e) { log(e.toString()); showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время'); - dietsRepository.emitFavoriteStateToFeed(DietFavoriteState(isFavorite: currentDiet.isFavorite, index: dietIndexInFeed)); + if (dietIndex != null) { + dietsRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: currentDiet.isFavorite, index: dietIndex)); + } emit(AboutDietState.loaded(currentDiet)); } }, diff --git a/lib/features/diets/presentation/pages/about_diet_screen.dart b/lib/features/diets/presentation/pages/about_diet_screen.dart index 1ce56fb5..8cd3fd84 100644 --- a/lib/features/diets/presentation/pages/about_diet_screen.dart +++ b/lib/features/diets/presentation/pages/about_diet_screen.dart @@ -6,7 +6,7 @@ import 'package:eatvio_app/core/utils/localization_utils.dart'; import 'package:eatvio_app/core/utils/string_extentions.dart'; import 'package:eatvio_app/core/widgets/bookmark_button.dart'; import 'package:eatvio_app/core/widgets/custom_divider.dart'; -import 'package:eatvio_app/core/widgets/diet_info_row.dart'; +import 'package:eatvio_app/core/widgets/info_row.dart'; import 'package:eatvio_app/core/widgets/loader.dart'; import 'package:eatvio_app/features/diets/presentation/cubit/about_diet_cubit.dart'; import 'package:eatvio_app/features/diets/presentation/cubit/about_diet_state.dart'; @@ -32,7 +32,7 @@ import 'package:flutter_svg/svg.dart'; class AboutDietScreen extends StatefulWidget { final Diet diet; - final int index; + final int? index; final bool navigateToComments; late final List selectedIndexes; @@ -128,14 +128,15 @@ class _AboutDietScreenState extends State { elevation: 0, focusElevation: 0, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), + borderRadius: BorderRadius.circular(20), ), backgroundColor: appTheme.greenA700, onPressed: () { + final cubit = context.read(); showModalBottomSheet( context: context, builder: (context) => StartDietBottomSheet( - aboutDietCubit: context.read(), + aboutDietCubit: cubit, ), ); }, @@ -159,7 +160,8 @@ class _AboutDietScreenState extends State { width: 52, height: 52, size: 24, - shape: BoxShape.circle, + shape: BoxShape.rectangle, + borderRadius: BorderRadius.circular(20), color: appTheme.gray50, ), ], @@ -289,7 +291,9 @@ class _AboutDietScreenState extends State { actions: [ PortalTarget( visible: isMoreOptionsVisible, - portalFollower: MoreOptions(id: widget.diet.id), + portalFollower: MoreOptions( + path: 'diet/${widget.diet.id}', + ), anchor: Aligned( shiftToWithinBound: AxisFlag(x: true, y: true), follower: Alignment.topRight, diff --git a/lib/features/diets/presentation/widgets/author_card.dart b/lib/features/diets/presentation/widgets/author_card.dart index d35557a1..1a8cfcbe 100644 --- a/lib/features/diets/presentation/widgets/author_card.dart +++ b/lib/features/diets/presentation/widgets/author_card.dart @@ -11,8 +11,7 @@ class AuthorCard extends StatelessWidget { final Author author; final String title; @override - Widget build(BuildContext context) { - // final s = S.of(context); + Widget build(BuildContext context) { return Container( width: MediaQuery.sizeOf(context).width, decoration: BoxDecoration(borderRadius: BorderRadius.circular(12), color: appTheme.cardBackground), diff --git a/lib/features/diets/presentation/widgets/expandable_text.dart b/lib/features/diets/presentation/widgets/expandable_text.dart index ca4b51ad..ab490df4 100644 --- a/lib/features/diets/presentation/widgets/expandable_text.dart +++ b/lib/features/diets/presentation/widgets/expandable_text.dart @@ -32,39 +32,42 @@ class _ExpandableTextState extends State { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Stack( - children: [ - Html( - data: widget.text, - style: { - "*": Style( - color: Colors.black87, - maxLines: isExpanded ? null : widget.maxLines, - textOverflow: TextOverflow.clip, - ), - }, - ), - if (!isExpanded) - Positioned.fill( - child: Align( - alignment: Alignment.bottomCenter, - child: Container( - height: 35, - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - Colors.white.withValues(alpha: 0.0), - Colors.white.withValues(alpha: 0.6), - Colors.white, - ], + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Stack( + children: [ + Html( + data: widget.text, + style: { + "*": Style( + color: Colors.black87, + maxLines: isExpanded ? null : widget.maxLines, + textOverflow: TextOverflow.clip, + ), + }, + ), + if (!isExpanded) + Positioned.fill( + child: Align( + alignment: Alignment.bottomCenter, + child: Container( + height: 35, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.white.withValues(alpha: 0.0), + Colors.white.withValues(alpha: 0.6), + Colors.white, + ], + ), ), ), ), ), - ), - ], + ], + ), ), Align( alignment: Alignment.bottomCenter, diff --git a/lib/features/diets/presentation/widgets/info_bouble.dart b/lib/features/diets/presentation/widgets/info_bouble.dart index 7f339420..fc27b4b2 100644 --- a/lib/features/diets/presentation/widgets/info_bouble.dart +++ b/lib/features/diets/presentation/widgets/info_bouble.dart @@ -2,14 +2,15 @@ import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:flutter/material.dart'; class InfoBouble extends StatelessWidget { - const InfoBouble({super.key, required this.children, this.backgroundColor, this.radius = 100}); + const InfoBouble({super.key, required this.children, this.backgroundColor, this.radius = 100, this.padding}); final List children; final Color? backgroundColor; final double radius; + final EdgeInsetsGeometry? padding; @override Widget build(BuildContext context) { return Container( - padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8.5), + padding: padding ?? EdgeInsets.symmetric(horizontal: 12, vertical: 8.5), decoration: BoxDecoration( color: backgroundColor ?? appTheme.gray50, borderRadius: BorderRadius.circular(radius), diff --git a/lib/features/diets/presentation/widgets/more_options.dart b/lib/features/diets/presentation/widgets/more_options.dart index b087f9f0..8a948a7c 100644 --- a/lib/features/diets/presentation/widgets/more_options.dart +++ b/lib/features/diets/presentation/widgets/more_options.dart @@ -12,8 +12,8 @@ import 'package:go_router/go_router.dart'; import 'package:share_plus/share_plus.dart'; class MoreOptions extends StatelessWidget { - const MoreOptions({super.key, required this.id, this.disableActions}); - final int id; + const MoreOptions({super.key, this.disableActions, required this.path}); + final String path; final bool? disableActions; @override Widget build(BuildContext context) { @@ -26,10 +26,12 @@ class MoreOptions extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ GestureDetector( - onTap:disableActions == true ? null : () { - Clipboard.setData(ClipboardData(text: baseUrl + '/ru/diet/' + id.toString())); - showInfoToast(s.link_was_copied); - }, + onTap: disableActions == true + ? null + : () { + Clipboard.setData(ClipboardData(text: '$baseUrl/ru/$path')); + showInfoToast(s.link_was_copied); + }, child: Row( children: [ Text( @@ -48,11 +50,13 @@ class MoreOptions extends StatelessWidget { child: CustomDivider(), ), GestureDetector( - onTap: disableActions == true ? null : () => Share.shareUri( - Uri.parse( - baseUrl + '/ru/diet/' + id.toString(), - ), - ), + onTap: disableActions == true + ? null + : () => Share.shareUri( + Uri.parse( + '$baseUrl/ru/$path', + ), + ), child: Row( children: [ Text( @@ -71,9 +75,11 @@ class MoreOptions extends StatelessWidget { child: CustomDivider(), ), GestureDetector( - onTap: disableActions == true ? null : () { - context.push('complainingScreen'); - }, + onTap: disableActions == true + ? null + : () { + context.push('complainingScreen'); + }, child: Row( children: [ Text( diff --git a/lib/features/diets/repositories/diet_repository.dart b/lib/features/diets/repositories/diet_repository.dart index 142ce800..d0a48e38 100644 --- a/lib/features/diets/repositories/diet_repository.dart +++ b/lib/features/diets/repositories/diet_repository.dart @@ -9,9 +9,9 @@ class DietRepository { DietRepository({required this.dietsService}); final DietService dietsService; - final _dietLikeStateStreamController = StreamController.broadcast(); - final _dietFavoriteStateStreamController = StreamController.broadcast(); - final _dietCommentsStateStreamController = StreamController.broadcast(); + final _dietLikeStateStreamController = StreamController.broadcast(); + final _dietFavoriteStateStreamController = StreamController.broadcast(); + final _dietCommentsStateStreamController = StreamController.broadcast(); Stream get dietLikeState => _dietLikeStateStreamController.stream; Stream get dietFavoriteState => _dietFavoriteStateStreamController.stream; Stream get dietCommentsState => _dietCommentsStateStreamController.stream; @@ -49,17 +49,17 @@ class DietRepository { } } - void emitLikesStateToFeed(DietLikesState state) => _dietLikeStateStreamController.add(state); - void emitFavoriteStateToFeed(DietFavoriteState state) => _dietFavoriteStateStreamController.add(state); - void emitCommentsStateToFeed(DietCommentsState state) => _dietCommentsStateStreamController.add(state); + void emitLikesStateToFeed(LikesState state) => _dietLikeStateStreamController.add(state); + void emitFavoriteStateToFeed(FavoriteState state) => _dietFavoriteStateStreamController.add(state); + void emitCommentsStateToFeed(CommentsState state) => _dietCommentsStateStreamController.add(state); } -class DietLikesState extends Equatable { +class LikesState extends Equatable { final bool wasLikedByMe; final int totalLikes; final int index; - DietLikesState({ + LikesState({ required this.wasLikedByMe, required this.totalLikes, required this.index, @@ -73,11 +73,11 @@ class DietLikesState extends Equatable { ]; } -class DietFavoriteState extends Equatable { +class FavoriteState extends Equatable { final bool isFavorite; final int index; - DietFavoriteState({ + FavoriteState({ required this.isFavorite, required this.index, }); @@ -89,11 +89,11 @@ class DietFavoriteState extends Equatable { ]; } -class DietCommentsState extends Equatable { +class CommentsState extends Equatable { final int index; final int totalComments; - DietCommentsState({ + CommentsState({ required this.index, required this.totalComments, }); @@ -103,4 +103,4 @@ class DietCommentsState extends Equatable { index, totalComments, ]; -} \ No newline at end of file +} diff --git a/lib/features/feed/models/feed_blog_post_model.dart b/lib/features/feed/models/feed_blog_post_model.dart index d8a21194..88eddbaf 100644 --- a/lib/features/feed/models/feed_blog_post_model.dart +++ b/lib/features/feed/models/feed_blog_post_model.dart @@ -13,8 +13,8 @@ class FeedBlogPostModel with _$FeedBlogPostModel { required String description, required Author author, @JsonKey(name: "video_url") required String videoUrl, - @JsonKey(name: "created_at") required DateTime createdAt, - @JsonKey(name: "updated_at") required DateTime updatedAt, + @JsonKey(name: "created_at", fromJson: parseDate) required DateTime createdAt, + @JsonKey(name: "updated_at", fromJson: parseDate) required DateTime updatedAt, required String title, @JsonKey(name: "total_likes") required int totalLikes, @JsonKey(name: "total_views") required int totalViews, @@ -24,3 +24,5 @@ class FeedBlogPostModel with _$FeedBlogPostModel { factory FeedBlogPostModel.fromJson(Map json) => _$FeedBlogPostModelFromJson(json); } + +DateTime parseDate(String date) => DateTime.parse(date).toLocal(); \ No newline at end of file diff --git a/lib/features/feed/models/feed_blog_post_model.freezed.dart b/lib/features/feed/models/feed_blog_post_model.freezed.dart index ad757d23..2d1870b0 100644 --- a/lib/features/feed/models/feed_blog_post_model.freezed.dart +++ b/lib/features/feed/models/feed_blog_post_model.freezed.dart @@ -27,9 +27,9 @@ mixin _$FeedBlogPostModel { Author get author => throw _privateConstructorUsedError; @JsonKey(name: "video_url") String get videoUrl => throw _privateConstructorUsedError; - @JsonKey(name: "created_at") + @JsonKey(name: "created_at", fromJson: parseDate) DateTime get createdAt => throw _privateConstructorUsedError; - @JsonKey(name: "updated_at") + @JsonKey(name: "updated_at", fromJson: parseDate) DateTime get updatedAt => throw _privateConstructorUsedError; String get title => throw _privateConstructorUsedError; @JsonKey(name: "total_likes") @@ -64,8 +64,8 @@ abstract class $FeedBlogPostModelCopyWith<$Res> { String description, Author author, @JsonKey(name: "video_url") String videoUrl, - @JsonKey(name: "created_at") DateTime createdAt, - @JsonKey(name: "updated_at") DateTime updatedAt, + @JsonKey(name: "created_at", fromJson: parseDate) DateTime createdAt, + @JsonKey(name: "updated_at", fromJson: parseDate) DateTime updatedAt, String title, @JsonKey(name: "total_likes") int totalLikes, @JsonKey(name: "total_views") int totalViews, @@ -186,8 +186,8 @@ abstract class _$$FeedBlogPostModelImplCopyWith<$Res> String description, Author author, @JsonKey(name: "video_url") String videoUrl, - @JsonKey(name: "created_at") DateTime createdAt, - @JsonKey(name: "updated_at") DateTime updatedAt, + @JsonKey(name: "created_at", fromJson: parseDate) DateTime createdAt, + @JsonKey(name: "updated_at", fromJson: parseDate) DateTime updatedAt, String title, @JsonKey(name: "total_likes") int totalLikes, @JsonKey(name: "total_views") int totalViews, @@ -292,8 +292,8 @@ class _$FeedBlogPostModelImpl implements _FeedBlogPostModel { required this.description, required this.author, @JsonKey(name: "video_url") required this.videoUrl, - @JsonKey(name: "created_at") required this.createdAt, - @JsonKey(name: "updated_at") required this.updatedAt, + @JsonKey(name: "created_at", fromJson: parseDate) required this.createdAt, + @JsonKey(name: "updated_at", fromJson: parseDate) required this.updatedAt, required this.title, @JsonKey(name: "total_likes") required this.totalLikes, @JsonKey(name: "total_views") required this.totalViews, @@ -317,10 +317,10 @@ class _$FeedBlogPostModelImpl implements _FeedBlogPostModel { @JsonKey(name: "video_url") final String videoUrl; @override - @JsonKey(name: "created_at") + @JsonKey(name: "created_at", fromJson: parseDate) final DateTime createdAt; @override - @JsonKey(name: "updated_at") + @JsonKey(name: "updated_at", fromJson: parseDate) final DateTime updatedAt; @override final String title; @@ -413,8 +413,10 @@ abstract class _FeedBlogPostModel implements FeedBlogPostModel { required final String description, required final Author author, @JsonKey(name: "video_url") required final String videoUrl, - @JsonKey(name: "created_at") required final DateTime createdAt, - @JsonKey(name: "updated_at") required final DateTime updatedAt, + @JsonKey(name: "created_at", fromJson: parseDate) + required final DateTime createdAt, + @JsonKey(name: "updated_at", fromJson: parseDate) + required final DateTime updatedAt, required final String title, @JsonKey(name: "total_likes") required final int totalLikes, @JsonKey(name: "total_views") required final int totalViews, @@ -439,10 +441,10 @@ abstract class _FeedBlogPostModel implements FeedBlogPostModel { @JsonKey(name: "video_url") String get videoUrl; @override - @JsonKey(name: "created_at") + @JsonKey(name: "created_at", fromJson: parseDate) DateTime get createdAt; @override - @JsonKey(name: "updated_at") + @JsonKey(name: "updated_at", fromJson: parseDate) DateTime get updatedAt; @override String get title; diff --git a/lib/features/feed/models/feed_blog_post_model.g.dart b/lib/features/feed/models/feed_blog_post_model.g.dart index 6abc8a23..95da2b2b 100644 --- a/lib/features/feed/models/feed_blog_post_model.g.dart +++ b/lib/features/feed/models/feed_blog_post_model.g.dart @@ -15,8 +15,8 @@ _$FeedBlogPostModelImpl _$$FeedBlogPostModelImplFromJson( description: json['description'] as String, author: Author.fromJson(json['author'] as Map), videoUrl: json['video_url'] as String, - createdAt: DateTime.parse(json['created_at'] as String), - updatedAt: DateTime.parse(json['updated_at'] as String), + createdAt: parseDate(json['created_at'] as String), + updatedAt: parseDate(json['updated_at'] as String), title: json['title'] as String, totalLikes: (json['total_likes'] as num).toInt(), totalViews: (json['total_views'] as num).toInt(), diff --git a/lib/features/feed/models/plan_pagination_model.dart b/lib/features/feed/models/plan_pagination_model.dart index 13d57c32..083bb162 100644 --- a/lib/features/feed/models/plan_pagination_model.dart +++ b/lib/features/feed/models/plan_pagination_model.dart @@ -26,7 +26,7 @@ class PlanModel with _$PlanModel { @JsonKey(name: "image") required String image, @JsonKey(name: "duration") required int duration, @JsonKey(name: "state") required String state, - @JsonKey(name: "type") required PlanType type, + @JsonKey(name: "type", fromJson: planTypeFromJson) required PlanType type, @JsonKey(name: "details") required String details, @JsonKey(name: "packets_count") required int packetsCount, @JsonKey(name: "packet1_options") PacketOptions? packet1Options, @@ -37,11 +37,21 @@ class PlanModel with _$PlanModel { @JsonKey(name: "total_views") required int totalViews, @JsonKey(name: "total_comments") required int totalComments, @JsonKey(name: "is_favorite") required bool isFavorite, + @JsonKey(name: "min_price_of_options") required double minPriceOfOptions, + @JsonKey(name: "max_price_of_options") required double maxPriceOfOptions, + @Default(false) bool wasLikedByMe, }) = _PlanModel; factory PlanModel.fromJson(Map json) => _$PlanModelFromJson(json); } +PlanType planTypeFromJson(dynamic json) { + if (json is List) { + return PlanType(fit: false, slim: false, behavior: false, wellness: false); + } + return PlanType.fromJson(json); +} + @freezed class PacketOptions with _$PacketOptions { const factory PacketOptions({ diff --git a/lib/features/feed/models/plan_pagination_model.freezed.dart b/lib/features/feed/models/plan_pagination_model.freezed.dart index a3348859..0a338d10 100644 --- a/lib/features/feed/models/plan_pagination_model.freezed.dart +++ b/lib/features/feed/models/plan_pagination_model.freezed.dart @@ -272,7 +272,7 @@ mixin _$PlanModel { int get duration => throw _privateConstructorUsedError; @JsonKey(name: "state") String get state => throw _privateConstructorUsedError; - @JsonKey(name: "type") + @JsonKey(name: "type", fromJson: planTypeFromJson) PlanType get type => throw _privateConstructorUsedError; @JsonKey(name: "details") String get details => throw _privateConstructorUsedError; @@ -294,6 +294,11 @@ mixin _$PlanModel { int get totalComments => throw _privateConstructorUsedError; @JsonKey(name: "is_favorite") bool get isFavorite => throw _privateConstructorUsedError; + @JsonKey(name: "min_price_of_options") + double get minPriceOfOptions => throw _privateConstructorUsedError; + @JsonKey(name: "max_price_of_options") + double get maxPriceOfOptions => throw _privateConstructorUsedError; + bool get wasLikedByMe => throw _privateConstructorUsedError; /// Serializes this PlanModel to a JSON map. Map toJson() => throw _privateConstructorUsedError; @@ -317,7 +322,7 @@ abstract class $PlanModelCopyWith<$Res> { @JsonKey(name: "image") String image, @JsonKey(name: "duration") int duration, @JsonKey(name: "state") String state, - @JsonKey(name: "type") PlanType type, + @JsonKey(name: "type", fromJson: planTypeFromJson) PlanType type, @JsonKey(name: "details") String details, @JsonKey(name: "packets_count") int packetsCount, @JsonKey(name: "packet1_options") PacketOptions? packet1Options, @@ -327,7 +332,10 @@ abstract class $PlanModelCopyWith<$Res> { @JsonKey(name: "total_likes") int totalLikes, @JsonKey(name: "total_views") int totalViews, @JsonKey(name: "total_comments") int totalComments, - @JsonKey(name: "is_favorite") bool isFavorite}); + @JsonKey(name: "is_favorite") bool isFavorite, + @JsonKey(name: "min_price_of_options") double minPriceOfOptions, + @JsonKey(name: "max_price_of_options") double maxPriceOfOptions, + bool wasLikedByMe}); $PlanTypeCopyWith<$Res> get type; $PacketOptionsCopyWith<$Res>? get packet1Options; @@ -368,6 +376,9 @@ class _$PlanModelCopyWithImpl<$Res, $Val extends PlanModel> Object? totalViews = null, Object? totalComments = null, Object? isFavorite = null, + Object? minPriceOfOptions = null, + Object? maxPriceOfOptions = null, + Object? wasLikedByMe = null, }) { return _then(_value.copyWith( id: null == id @@ -438,6 +449,18 @@ class _$PlanModelCopyWithImpl<$Res, $Val extends PlanModel> ? _value.isFavorite : isFavorite // ignore: cast_nullable_to_non_nullable as bool, + minPriceOfOptions: null == minPriceOfOptions + ? _value.minPriceOfOptions + : minPriceOfOptions // ignore: cast_nullable_to_non_nullable + as double, + maxPriceOfOptions: null == maxPriceOfOptions + ? _value.maxPriceOfOptions + : maxPriceOfOptions // ignore: cast_nullable_to_non_nullable + as double, + wasLikedByMe: null == wasLikedByMe + ? _value.wasLikedByMe + : wasLikedByMe // ignore: cast_nullable_to_non_nullable + as bool, ) as $Val); } @@ -519,7 +542,7 @@ abstract class _$$PlanModelImplCopyWith<$Res> @JsonKey(name: "image") String image, @JsonKey(name: "duration") int duration, @JsonKey(name: "state") String state, - @JsonKey(name: "type") PlanType type, + @JsonKey(name: "type", fromJson: planTypeFromJson) PlanType type, @JsonKey(name: "details") String details, @JsonKey(name: "packets_count") int packetsCount, @JsonKey(name: "packet1_options") PacketOptions? packet1Options, @@ -529,7 +552,10 @@ abstract class _$$PlanModelImplCopyWith<$Res> @JsonKey(name: "total_likes") int totalLikes, @JsonKey(name: "total_views") int totalViews, @JsonKey(name: "total_comments") int totalComments, - @JsonKey(name: "is_favorite") bool isFavorite}); + @JsonKey(name: "is_favorite") bool isFavorite, + @JsonKey(name: "min_price_of_options") double minPriceOfOptions, + @JsonKey(name: "max_price_of_options") double maxPriceOfOptions, + bool wasLikedByMe}); @override $PlanTypeCopyWith<$Res> get type; @@ -573,6 +599,9 @@ class __$$PlanModelImplCopyWithImpl<$Res> Object? totalViews = null, Object? totalComments = null, Object? isFavorite = null, + Object? minPriceOfOptions = null, + Object? maxPriceOfOptions = null, + Object? wasLikedByMe = null, }) { return _then(_$PlanModelImpl( id: null == id @@ -643,6 +672,18 @@ class __$$PlanModelImplCopyWithImpl<$Res> ? _value.isFavorite : isFavorite // ignore: cast_nullable_to_non_nullable as bool, + minPriceOfOptions: null == minPriceOfOptions + ? _value.minPriceOfOptions + : minPriceOfOptions // ignore: cast_nullable_to_non_nullable + as double, + maxPriceOfOptions: null == maxPriceOfOptions + ? _value.maxPriceOfOptions + : maxPriceOfOptions // ignore: cast_nullable_to_non_nullable + as double, + wasLikedByMe: null == wasLikedByMe + ? _value.wasLikedByMe + : wasLikedByMe // ignore: cast_nullable_to_non_nullable + as bool, )); } } @@ -657,7 +698,7 @@ class _$PlanModelImpl implements _PlanModel { @JsonKey(name: "image") required this.image, @JsonKey(name: "duration") required this.duration, @JsonKey(name: "state") required this.state, - @JsonKey(name: "type") required this.type, + @JsonKey(name: "type", fromJson: planTypeFromJson) required this.type, @JsonKey(name: "details") required this.details, @JsonKey(name: "packets_count") required this.packetsCount, @JsonKey(name: "packet1_options") this.packet1Options, @@ -667,7 +708,10 @@ class _$PlanModelImpl implements _PlanModel { @JsonKey(name: "total_likes") required this.totalLikes, @JsonKey(name: "total_views") required this.totalViews, @JsonKey(name: "total_comments") required this.totalComments, - @JsonKey(name: "is_favorite") required this.isFavorite}); + @JsonKey(name: "is_favorite") required this.isFavorite, + @JsonKey(name: "min_price_of_options") required this.minPriceOfOptions, + @JsonKey(name: "max_price_of_options") required this.maxPriceOfOptions, + this.wasLikedByMe = false}); factory _$PlanModelImpl.fromJson(Map json) => _$$PlanModelImplFromJson(json); @@ -691,7 +735,7 @@ class _$PlanModelImpl implements _PlanModel { @JsonKey(name: "state") final String state; @override - @JsonKey(name: "type") + @JsonKey(name: "type", fromJson: planTypeFromJson) final PlanType type; @override @JsonKey(name: "details") @@ -723,10 +767,19 @@ class _$PlanModelImpl implements _PlanModel { @override @JsonKey(name: "is_favorite") final bool isFavorite; + @override + @JsonKey(name: "min_price_of_options") + final double minPriceOfOptions; + @override + @JsonKey(name: "max_price_of_options") + final double maxPriceOfOptions; + @override + @JsonKey() + final bool wasLikedByMe; @override String toString() { - return 'PlanModel(id: $id, slug: $slug, name: $name, image: $image, duration: $duration, state: $state, type: $type, details: $details, packetsCount: $packetsCount, packet1Options: $packet1Options, packet2Options: $packet2Options, packet3Options: $packet3Options, author: $author, totalLikes: $totalLikes, totalViews: $totalViews, totalComments: $totalComments, isFavorite: $isFavorite)'; + return 'PlanModel(id: $id, slug: $slug, name: $name, image: $image, duration: $duration, state: $state, type: $type, details: $details, packetsCount: $packetsCount, packet1Options: $packet1Options, packet2Options: $packet2Options, packet3Options: $packet3Options, author: $author, totalLikes: $totalLikes, totalViews: $totalViews, totalComments: $totalComments, isFavorite: $isFavorite, minPriceOfOptions: $minPriceOfOptions, maxPriceOfOptions: $maxPriceOfOptions, wasLikedByMe: $wasLikedByMe)'; } @override @@ -759,30 +812,40 @@ class _$PlanModelImpl implements _PlanModel { (identical(other.totalComments, totalComments) || other.totalComments == totalComments) && (identical(other.isFavorite, isFavorite) || - other.isFavorite == isFavorite)); + other.isFavorite == isFavorite) && + (identical(other.minPriceOfOptions, minPriceOfOptions) || + other.minPriceOfOptions == minPriceOfOptions) && + (identical(other.maxPriceOfOptions, maxPriceOfOptions) || + other.maxPriceOfOptions == maxPriceOfOptions) && + (identical(other.wasLikedByMe, wasLikedByMe) || + other.wasLikedByMe == wasLikedByMe)); } @JsonKey(includeFromJson: false, includeToJson: false) @override - int get hashCode => Object.hash( - runtimeType, - id, - slug, - name, - image, - duration, - state, - type, - details, - packetsCount, - packet1Options, - packet2Options, - packet3Options, - author, - totalLikes, - totalViews, - totalComments, - isFavorite); + int get hashCode => Object.hashAll([ + runtimeType, + id, + slug, + name, + image, + duration, + state, + type, + details, + packetsCount, + packet1Options, + packet2Options, + packet3Options, + author, + totalLikes, + totalViews, + totalComments, + isFavorite, + minPriceOfOptions, + maxPriceOfOptions, + wasLikedByMe + ]); /// Create a copy of PlanModel /// with the given fields replaced by the non-null parameter values. @@ -802,24 +865,29 @@ class _$PlanModelImpl implements _PlanModel { abstract class _PlanModel implements PlanModel { const factory _PlanModel( - {@JsonKey(name: "id") required final int id, - @JsonKey(name: "slug") required final String slug, - @JsonKey(name: "name") required final String name, - @JsonKey(name: "image") required final String image, - @JsonKey(name: "duration") required final int duration, - @JsonKey(name: "state") required final String state, - @JsonKey(name: "type") required final PlanType type, - @JsonKey(name: "details") required final String details, - @JsonKey(name: "packets_count") required final int packetsCount, - @JsonKey(name: "packet1_options") final PacketOptions? packet1Options, - @JsonKey(name: "packet2_options") final PacketOptions? packet2Options, - @JsonKey(name: "packet3_options") final PacketOptions? packet3Options, - @JsonKey(name: "author") required final Author author, - @JsonKey(name: "total_likes") required final int totalLikes, - @JsonKey(name: "total_views") required final int totalViews, - @JsonKey(name: "total_comments") required final int totalComments, - @JsonKey(name: "is_favorite") required final bool isFavorite}) = - _$PlanModelImpl; + {@JsonKey(name: "id") required final int id, + @JsonKey(name: "slug") required final String slug, + @JsonKey(name: "name") required final String name, + @JsonKey(name: "image") required final String image, + @JsonKey(name: "duration") required final int duration, + @JsonKey(name: "state") required final String state, + @JsonKey(name: "type", fromJson: planTypeFromJson) + required final PlanType type, + @JsonKey(name: "details") required final String details, + @JsonKey(name: "packets_count") required final int packetsCount, + @JsonKey(name: "packet1_options") final PacketOptions? packet1Options, + @JsonKey(name: "packet2_options") final PacketOptions? packet2Options, + @JsonKey(name: "packet3_options") final PacketOptions? packet3Options, + @JsonKey(name: "author") required final Author author, + @JsonKey(name: "total_likes") required final int totalLikes, + @JsonKey(name: "total_views") required final int totalViews, + @JsonKey(name: "total_comments") required final int totalComments, + @JsonKey(name: "is_favorite") required final bool isFavorite, + @JsonKey(name: "min_price_of_options") + required final double minPriceOfOptions, + @JsonKey(name: "max_price_of_options") + required final double maxPriceOfOptions, + final bool wasLikedByMe}) = _$PlanModelImpl; factory _PlanModel.fromJson(Map json) = _$PlanModelImpl.fromJson; @@ -843,7 +911,7 @@ abstract class _PlanModel implements PlanModel { @JsonKey(name: "state") String get state; @override - @JsonKey(name: "type") + @JsonKey(name: "type", fromJson: planTypeFromJson) PlanType get type; @override @JsonKey(name: "details") @@ -875,6 +943,14 @@ abstract class _PlanModel implements PlanModel { @override @JsonKey(name: "is_favorite") bool get isFavorite; + @override + @JsonKey(name: "min_price_of_options") + double get minPriceOfOptions; + @override + @JsonKey(name: "max_price_of_options") + double get maxPriceOfOptions; + @override + bool get wasLikedByMe; /// Create a copy of PlanModel /// with the given fields replaced by the non-null parameter values. diff --git a/lib/features/feed/models/plan_pagination_model.g.dart b/lib/features/feed/models/plan_pagination_model.g.dart index b1fb11b1..abfe3930 100644 --- a/lib/features/feed/models/plan_pagination_model.g.dart +++ b/lib/features/feed/models/plan_pagination_model.g.dart @@ -32,7 +32,7 @@ _$PlanModelImpl _$$PlanModelImplFromJson(Map json) => image: json['image'] as String, duration: (json['duration'] as num).toInt(), state: json['state'] as String, - type: PlanType.fromJson(json['type'] as Map), + type: planTypeFromJson(json['type']), details: json['details'] as String, packetsCount: (json['packets_count'] as num).toInt(), packet1Options: json['packet1_options'] == null @@ -52,6 +52,9 @@ _$PlanModelImpl _$$PlanModelImplFromJson(Map json) => totalViews: (json['total_views'] as num).toInt(), totalComments: (json['total_comments'] as num).toInt(), isFavorite: json['is_favorite'] as bool, + minPriceOfOptions: (json['min_price_of_options'] as num).toDouble(), + maxPriceOfOptions: (json['max_price_of_options'] as num).toDouble(), + wasLikedByMe: json['wasLikedByMe'] as bool? ?? false, ); Map _$$PlanModelImplToJson(_$PlanModelImpl instance) => @@ -73,6 +76,9 @@ Map _$$PlanModelImplToJson(_$PlanModelImpl instance) => 'total_views': instance.totalViews, 'total_comments': instance.totalComments, 'is_favorite': instance.isFavorite, + 'min_price_of_options': instance.minPriceOfOptions, + 'max_price_of_options': instance.maxPriceOfOptions, + 'wasLikedByMe': instance.wasLikedByMe, }; _$PacketOptionsImpl _$$PacketOptionsImplFromJson(Map json) => 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 c299cdc0..87cdd95d 100644 --- a/lib/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart +++ b/lib/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart @@ -9,7 +9,7 @@ import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_sta import 'package:eatvio_app/features/feed/repositories/feed_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -class DietsCubit extends Cubit { +class DietsCubit extends Cubit{ final FeedRepository feedRepository; final DietRepository dietsRepository; int currentPage = 1; @@ -25,7 +25,7 @@ class DietsCubit extends Cubit { 'name:desc', ]; static const List _durationValues = [null, ...durationValues]; - static const List _types = [null, ...typeValues]; + static const List _types = [null, ...dietTypeValues]; late final StreamSubscription likesStateSubscription; late final StreamSubscription favoriteStateSubscription; late final StreamSubscription commentsStateSubscription; @@ -52,19 +52,40 @@ class DietsCubit extends Cubit { super.close(); } + void changeTypeFilter(int index) { emit(state.copyWith(filterState: state.filterState.copyWith(typeIndex: index), errorMessage: null)); - loadDiets(query: _query); + EasyDebounce.debounce( + 'loadDiets', + Duration(seconds: 1), + () => loadDiets( + query: _query, + ), + ); } + void changeSortingType(int index) { emit(state.copyWith(filterState: state.filterState.copyWith(sortingIndex: index), errorMessage: null)); - loadDiets(query: _query); + EasyDebounce.debounce( + 'loadDiets', + Duration(seconds: 1), + () => loadDiets( + query: _query, + ), + ); } + void changeDurationFilter(int index) { emit(state.copyWith(filterState: state.filterState.copyWith(durationIndex: index), errorMessage: null)); - loadDiets(query: _query); + EasyDebounce.debounce( + 'loadDiets', + Duration(seconds: 1), + () => loadDiets( + query: _query, + ), + ); } loadDiets({ @@ -82,9 +103,12 @@ class DietsCubit extends Cubit { sorting: sorting, ); lastPage = dietsPagination.meta.lastPage; - emit( - state.copyWith(dietsList: dietsPagination.data, isLoading: isLoading, errorMessage: null), + state.copyWith( + dietsList: dietsPagination.data, + isLoading: isLoading, + errorMessage: null, + ), ); } catch (e) { emit( @@ -181,7 +205,7 @@ class DietsCubit extends Cubit { ); } - void _changeDietLikeState(DietLikesState likesState) { + void _changeDietLikeState(LikesState likesState) { final currentDiets = state.dietsList; final dietsForModification = [...currentDiets]; final currentDiet = currentDiets[likesState.index]; @@ -201,7 +225,7 @@ class DietsCubit extends Cubit { ); } - void _changeDietFavoriteState(DietFavoriteState favoriteState) { + void _changeDietFavoriteState(FavoriteState favoriteState) { final currentDiets = state.dietsList; final dietsForModification = [...currentDiets]; final currentDiet = currentDiets[favoriteState.index]; @@ -220,7 +244,7 @@ class DietsCubit extends Cubit { ); } - void _changeDietCommentsState(DietCommentsState commentsState) { + void _changeDietCommentsState(CommentsState commentsState) { final currentDiets = state.dietsList; final dietsForModification = [...currentDiets]; final currentDiet = currentDiets[commentsState.index]; @@ -261,7 +285,7 @@ class DietsCubit extends Cubit { Duration(milliseconds: 200), () async { try { - await dietsRepository.addToFavorite(id); + await dietsRepository.addToFavorite(id); } catch (e) { log(e.toString()); showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время'); @@ -301,7 +325,7 @@ class DietsCubit extends Cubit { Duration(milliseconds: 200), () async { try { - await dietsRepository.removeFromFavorite(id); + await dietsRepository.removeFromFavorite(id); } catch (e) { log(e.toString()); showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время'); diff --git a/lib/features/feed/presentation/bloc/plans_cubit/plans_cubit.dart b/lib/features/feed/presentation/bloc/plans_cubit/plans_cubit.dart index 352d04fb..10f5b8ab 100644 --- a/lib/features/feed/presentation/bloc/plans_cubit/plans_cubit.dart +++ b/lib/features/feed/presentation/bloc/plans_cubit/plans_cubit.dart @@ -1,41 +1,176 @@ +import 'dart:async'; import 'dart:developer'; +import 'package:easy_debounce/easy_debounce.dart'; +import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/utils/toasts.dart'; +import 'package:eatvio_app/features/diets/repositories/diet_repository.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_state.dart'; import 'package:eatvio_app/features/feed/repositories/feed_repository.dart'; +import 'package:eatvio_app/features/plans/repositories/plan_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class PlansCubit extends Cubit { final FeedRepository feedRepository; + final PlanRepository planRepository; + int currentPage = 1; late int lastPage; bool isLoading = false; String? _query; + static const List _durationValues = [null, ...durationValues]; + static const List _types = [null, ...planTypeValues]; + + late final StreamSubscription likesStateSubscription; + late final StreamSubscription favoriteStateSubscription; + late final StreamSubscription commentsStateSubscription; PlansCubit({ required this.feedRepository, - }) : super(PlansState.initial()); + required this.planRepository, + }) : super(PlansState.initial()) { + likesStateSubscription = planRepository.planLikeState.listen((likesState) { + _changePlanLikeState(likesState); + }); + favoriteStateSubscription = planRepository.planFavoriteState.listen((favoriteState) { + _changePlanFavoriteState(favoriteState); + }); + commentsStateSubscription = planRepository.planCommentsState.listen((commentsState) { + _changePlanCommentsState(commentsState); + }); + } + + void changeDurationFilter(int index) { + emit(state.copyWith(filterState: state.filterState.copyWith(durationIndex: index), errorMessage: null)); + EasyDebounce.debounce( + 'loadPlans', + Duration(seconds: 1), + () => loadPlans( + query: _query, + ), + ); + } + + void changeTypeFilter(int index) { + emit(state.copyWith(filterState: state.filterState.copyWith(typeIndex: index), errorMessage: null)); + EasyDebounce.debounce( + 'loadPlans', + Duration(seconds: 1), + () => loadPlans( + query: _query, + ), + ); + } + + void changePriceFilter(int minPrice, int maxPrice, {bool disableDebounce = false}) { + emit(state.copyWith(filterState: state.filterState.copyWith(priceFilter: (minPrice, maxPrice)), errorMessage: null)); + EasyDebounce.debounce( + 'loadPlans', + Duration(seconds: disableDebounce ? 0 : 1), + () => loadPlans( + query: _query, + ), + ); + } + + void _changePlanLikeState(LikesState likesState) { + final currentPlans = state.plans; + final plansForModification = [...currentPlans]; + final currentPlan = currentPlans[likesState.index]; + + plansForModification[likesState.index] = currentPlan.copyWith( + totalLikes: likesState.totalLikes, + wasLikedByMe: likesState.wasLikedByMe, + ); + emit( + state.copyWith( + plans: [ + ...plansForModification, + ], + isLoading: isLoading, + errorMessage: null, + ), + ); + } + + void _changePlanFavoriteState(FavoriteState favoriteState) { + final currentPlans = state.plans; + final plansForModification = [...currentPlans]; + final currentPlan = currentPlans[favoriteState.index]; + + plansForModification[favoriteState.index] = currentPlan.copyWith( + isFavorite: favoriteState.isFavorite, + ); + emit( + state.copyWith( + plans: [ + ...plansForModification, + ], + isLoading: isLoading, + errorMessage: null, + ), + ); + } + + void _changePlanCommentsState(CommentsState commentsState) { + final currentPlans = state.plans; + final plansForModification = [...currentPlans]; + final currentPlan = currentPlans[commentsState.index]; + + plansForModification[commentsState.index] = currentPlan.copyWith( + totalComments: commentsState.totalComments, + ); + emit( + state.copyWith( + plans: [ + ...plansForModification, + ], + isLoading: isLoading, + errorMessage: null, + ), + ); + } loadPlans({ String? query, }) async { try { _query = query; + final type = _types[state.filterState.typeIndex]; + final duration = _durationValues[state.filterState.durationIndex]; + final minPrice = state.filterState.priceFilter.$1; + final maxPrice = state.filterState.priceFilter.$2; final plansPagination = await feedRepository.getPlans( query: query, + duration: duration, + maxPrice: maxPrice, + minPrice: minPrice, + types: type, ); lastPage = plansPagination.meta.lastPage; + final minPlanPrice = plansPagination.meta.totalMinPrice ?? 0; + final maxPlanPrice = plansPagination.meta.totalMaxPrice ?? 0; + if (state.filterState.priceFilter.$1 == null || state.filterState.priceFilter.$2 == null) { + emit( + state.copyWith( + filterState: state.filterState.copyWith(priceFilter: (minPlanPrice, maxPlanPrice)), + ), + ); + } emit( state.copyWith( plans: plansPagination.data, + isLoading: isLoading, errorMessage: null, - isLoading: false, + minPlanPrice: minPlanPrice, + maxPlanPrice: maxPlanPrice, ), ); } catch (e, stackTrace) { log(e.toString()); log(stackTrace.toString()); - emit(state.copyWith(errorMessage: e.toString())); + emit(state.copyWith(isLoading: isLoading, errorMessage: e.toString())); } } @@ -69,4 +204,84 @@ class PlansCubit extends Cubit { } } } + + Future addPlanToFavorite(String slug, int index) async { + final currentPlans = state.plans; + final plansForModification = [...currentPlans]; + final currentPlan = currentPlans[index]; + + plansForModification[index] = currentPlan.copyWith( + isFavorite: true, + ); + emit( + state.copyWith( + plans: [ + ...plansForModification, + ], + isLoading: isLoading, + errorMessage: null, + ), + ); + EasyDebounce.debounce( + 'add_or_remove_plan_from_favorite', + Duration(milliseconds: 200), + () async { + try { + await planRepository.addToFavorite(slug); + } catch (e) { + log(e.toString()); + showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время'); + emit( + state.copyWith( + plans: [ + ...currentPlans, + ], + isLoading: isLoading, + errorMessage: null, + ), + ); + } + }, + ); + } + + Future removePlanFromFavorite(String slug, int index) async { + final currentPlans = state.plans; + final plansForModification = [...currentPlans]; + final currentPlan = currentPlans[index]; + + plansForModification[index] = currentPlan.copyWith( + isFavorite: false, + ); + emit( + state.copyWith( + plans: [ + ...plansForModification, + ], + isLoading: isLoading, + errorMessage: null, + ), + ); + EasyDebounce.debounce( + 'add_or_remove_plan_from_favorite', + Duration(milliseconds: 200), + () async { + try { + await planRepository.addToFavorite(slug); + } catch (e) { + log(e.toString()); + showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время'); + emit( + state.copyWith( + plans: [ + ...currentPlans, + ], + isLoading: isLoading, + errorMessage: null, + ), + ); + } + }, + ); + } } diff --git a/lib/features/feed/presentation/bloc/plans_cubit/plans_state.dart b/lib/features/feed/presentation/bloc/plans_cubit/plans_state.dart index 668cef22..c90c6e1a 100644 --- a/lib/features/feed/presentation/bloc/plans_cubit/plans_state.dart +++ b/lib/features/feed/presentation/bloc/plans_cubit/plans_state.dart @@ -9,11 +9,28 @@ abstract class PlansState with _$PlansState { required bool isLoading, required List plans, required String? errorMessage, + required PlansFilterState filterState, + @Default(0) int minPlanPrice, + @Default(0) int maxPlanPrice, }) = _PlansState; - factory PlansState.initial() => const _PlansState( + factory PlansState.initial() => const PlansState( isLoading: true, plans: [], errorMessage: null, - ); + filterState: PlansFilterState( + priceFilter: (null, null), + durationIndex: 0, + typeIndex: 0, + ), + ); +} + +@freezed +class PlansFilterState with _$PlansFilterState { + const factory PlansFilterState({ + required (int?, int?) priceFilter, + required int durationIndex, + required int typeIndex, + }) = _PlansFilterState; } diff --git a/lib/features/feed/presentation/bloc/plans_cubit/plans_state.freezed.dart b/lib/features/feed/presentation/bloc/plans_cubit/plans_state.freezed.dart index 664e3b2e..03d3ab67 100644 --- a/lib/features/feed/presentation/bloc/plans_cubit/plans_state.freezed.dart +++ b/lib/features/feed/presentation/bloc/plans_cubit/plans_state.freezed.dart @@ -19,6 +19,9 @@ mixin _$PlansState { bool get isLoading => throw _privateConstructorUsedError; List get plans => throw _privateConstructorUsedError; String? get errorMessage => throw _privateConstructorUsedError; + PlansFilterState get filterState => throw _privateConstructorUsedError; + int get minPlanPrice => throw _privateConstructorUsedError; + int get maxPlanPrice => throw _privateConstructorUsedError; /// Create a copy of PlansState /// with the given fields replaced by the non-null parameter values. @@ -33,7 +36,15 @@ abstract class $PlansStateCopyWith<$Res> { PlansState value, $Res Function(PlansState) then) = _$PlansStateCopyWithImpl<$Res, PlansState>; @useResult - $Res call({bool isLoading, List plans, String? errorMessage}); + $Res call( + {bool isLoading, + List plans, + String? errorMessage, + PlansFilterState filterState, + int minPlanPrice, + int maxPlanPrice}); + + $PlansFilterStateCopyWith<$Res> get filterState; } /// @nodoc @@ -54,6 +65,9 @@ class _$PlansStateCopyWithImpl<$Res, $Val extends PlansState> Object? isLoading = null, Object? plans = null, Object? errorMessage = freezed, + Object? filterState = null, + Object? minPlanPrice = null, + Object? maxPlanPrice = null, }) { return _then(_value.copyWith( isLoading: null == isLoading @@ -68,8 +82,30 @@ class _$PlansStateCopyWithImpl<$Res, $Val extends PlansState> ? _value.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable as String?, + filterState: null == filterState + ? _value.filterState + : filterState // ignore: cast_nullable_to_non_nullable + as PlansFilterState, + minPlanPrice: null == minPlanPrice + ? _value.minPlanPrice + : minPlanPrice // ignore: cast_nullable_to_non_nullable + as int, + maxPlanPrice: null == maxPlanPrice + ? _value.maxPlanPrice + : maxPlanPrice // ignore: cast_nullable_to_non_nullable + as int, ) as $Val); } + + /// Create a copy of PlansState + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $PlansFilterStateCopyWith<$Res> get filterState { + return $PlansFilterStateCopyWith<$Res>(_value.filterState, (value) { + return _then(_value.copyWith(filterState: value) as $Val); + }); + } } /// @nodoc @@ -80,7 +116,16 @@ abstract class _$$PlansStateImplCopyWith<$Res> __$$PlansStateImplCopyWithImpl<$Res>; @override @useResult - $Res call({bool isLoading, List plans, String? errorMessage}); + $Res call( + {bool isLoading, + List plans, + String? errorMessage, + PlansFilterState filterState, + int minPlanPrice, + int maxPlanPrice}); + + @override + $PlansFilterStateCopyWith<$Res> get filterState; } /// @nodoc @@ -99,6 +144,9 @@ class __$$PlansStateImplCopyWithImpl<$Res> Object? isLoading = null, Object? plans = null, Object? errorMessage = freezed, + Object? filterState = null, + Object? minPlanPrice = null, + Object? maxPlanPrice = null, }) { return _then(_$PlansStateImpl( isLoading: null == isLoading @@ -113,6 +161,18 @@ class __$$PlansStateImplCopyWithImpl<$Res> ? _value.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable as String?, + filterState: null == filterState + ? _value.filterState + : filterState // ignore: cast_nullable_to_non_nullable + as PlansFilterState, + minPlanPrice: null == minPlanPrice + ? _value.minPlanPrice + : minPlanPrice // ignore: cast_nullable_to_non_nullable + as int, + maxPlanPrice: null == maxPlanPrice + ? _value.maxPlanPrice + : maxPlanPrice // ignore: cast_nullable_to_non_nullable + as int, )); } } @@ -123,7 +183,10 @@ class _$PlansStateImpl implements _PlansState { const _$PlansStateImpl( {required this.isLoading, required final List plans, - required this.errorMessage}) + required this.errorMessage, + required this.filterState, + this.minPlanPrice = 0, + this.maxPlanPrice = 0}) : _plans = plans; @override @@ -138,10 +201,18 @@ class _$PlansStateImpl implements _PlansState { @override final String? errorMessage; + @override + final PlansFilterState filterState; + @override + @JsonKey() + final int minPlanPrice; + @override + @JsonKey() + final int maxPlanPrice; @override String toString() { - return 'PlansState(isLoading: $isLoading, plans: $plans, errorMessage: $errorMessage)'; + return 'PlansState(isLoading: $isLoading, plans: $plans, errorMessage: $errorMessage, filterState: $filterState, minPlanPrice: $minPlanPrice, maxPlanPrice: $maxPlanPrice)'; } @override @@ -153,12 +224,24 @@ class _$PlansStateImpl implements _PlansState { other.isLoading == isLoading) && const DeepCollectionEquality().equals(other._plans, _plans) && (identical(other.errorMessage, errorMessage) || - other.errorMessage == errorMessage)); + other.errorMessage == errorMessage) && + (identical(other.filterState, filterState) || + other.filterState == filterState) && + (identical(other.minPlanPrice, minPlanPrice) || + other.minPlanPrice == minPlanPrice) && + (identical(other.maxPlanPrice, maxPlanPrice) || + other.maxPlanPrice == maxPlanPrice)); } @override - int get hashCode => Object.hash(runtimeType, isLoading, - const DeepCollectionEquality().hash(_plans), errorMessage); + int get hashCode => Object.hash( + runtimeType, + isLoading, + const DeepCollectionEquality().hash(_plans), + errorMessage, + filterState, + minPlanPrice, + maxPlanPrice); /// Create a copy of PlansState /// with the given fields replaced by the non-null parameter values. @@ -173,7 +256,10 @@ abstract class _PlansState implements PlansState { const factory _PlansState( {required final bool isLoading, required final List plans, - required final String? errorMessage}) = _$PlansStateImpl; + required final String? errorMessage, + required final PlansFilterState filterState, + final int minPlanPrice, + final int maxPlanPrice}) = _$PlansStateImpl; @override bool get isLoading; @@ -181,6 +267,12 @@ abstract class _PlansState implements PlansState { List get plans; @override String? get errorMessage; + @override + PlansFilterState get filterState; + @override + int get minPlanPrice; + @override + int get maxPlanPrice; /// Create a copy of PlansState /// with the given fields replaced by the non-null parameter values. @@ -189,3 +281,174 @@ abstract class _PlansState implements PlansState { _$$PlansStateImplCopyWith<_$PlansStateImpl> get copyWith => throw _privateConstructorUsedError; } + +/// @nodoc +mixin _$PlansFilterState { + (int?, int?) get priceFilter => throw _privateConstructorUsedError; + int get durationIndex => throw _privateConstructorUsedError; + int get typeIndex => throw _privateConstructorUsedError; + + /// Create a copy of PlansFilterState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $PlansFilterStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $PlansFilterStateCopyWith<$Res> { + factory $PlansFilterStateCopyWith( + PlansFilterState value, $Res Function(PlansFilterState) then) = + _$PlansFilterStateCopyWithImpl<$Res, PlansFilterState>; + @useResult + $Res call({(int?, int?) priceFilter, int durationIndex, int typeIndex}); +} + +/// @nodoc +class _$PlansFilterStateCopyWithImpl<$Res, $Val extends PlansFilterState> + implements $PlansFilterStateCopyWith<$Res> { + _$PlansFilterStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of PlansFilterState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? priceFilter = null, + Object? durationIndex = null, + Object? typeIndex = null, + }) { + return _then(_value.copyWith( + priceFilter: null == priceFilter + ? _value.priceFilter + : priceFilter // ignore: cast_nullable_to_non_nullable + as (int?, int?), + durationIndex: null == durationIndex + ? _value.durationIndex + : durationIndex // ignore: cast_nullable_to_non_nullable + as int, + typeIndex: null == typeIndex + ? _value.typeIndex + : typeIndex // ignore: cast_nullable_to_non_nullable + as int, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$PlansFilterStateImplCopyWith<$Res> + implements $PlansFilterStateCopyWith<$Res> { + factory _$$PlansFilterStateImplCopyWith(_$PlansFilterStateImpl value, + $Res Function(_$PlansFilterStateImpl) then) = + __$$PlansFilterStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({(int?, int?) priceFilter, int durationIndex, int typeIndex}); +} + +/// @nodoc +class __$$PlansFilterStateImplCopyWithImpl<$Res> + extends _$PlansFilterStateCopyWithImpl<$Res, _$PlansFilterStateImpl> + implements _$$PlansFilterStateImplCopyWith<$Res> { + __$$PlansFilterStateImplCopyWithImpl(_$PlansFilterStateImpl _value, + $Res Function(_$PlansFilterStateImpl) _then) + : super(_value, _then); + + /// Create a copy of PlansFilterState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? priceFilter = null, + Object? durationIndex = null, + Object? typeIndex = null, + }) { + return _then(_$PlansFilterStateImpl( + priceFilter: null == priceFilter + ? _value.priceFilter + : priceFilter // ignore: cast_nullable_to_non_nullable + as (int?, int?), + durationIndex: null == durationIndex + ? _value.durationIndex + : durationIndex // ignore: cast_nullable_to_non_nullable + as int, + typeIndex: null == typeIndex + ? _value.typeIndex + : typeIndex // ignore: cast_nullable_to_non_nullable + as int, + )); + } +} + +/// @nodoc + +class _$PlansFilterStateImpl implements _PlansFilterState { + const _$PlansFilterStateImpl( + {required this.priceFilter, + required this.durationIndex, + required this.typeIndex}); + + @override + final (int?, int?) priceFilter; + @override + final int durationIndex; + @override + final int typeIndex; + + @override + String toString() { + return 'PlansFilterState(priceFilter: $priceFilter, durationIndex: $durationIndex, typeIndex: $typeIndex)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$PlansFilterStateImpl && + (identical(other.priceFilter, priceFilter) || + other.priceFilter == priceFilter) && + (identical(other.durationIndex, durationIndex) || + other.durationIndex == durationIndex) && + (identical(other.typeIndex, typeIndex) || + other.typeIndex == typeIndex)); + } + + @override + int get hashCode => + Object.hash(runtimeType, priceFilter, durationIndex, typeIndex); + + /// Create a copy of PlansFilterState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$PlansFilterStateImplCopyWith<_$PlansFilterStateImpl> get copyWith => + __$$PlansFilterStateImplCopyWithImpl<_$PlansFilterStateImpl>( + this, _$identity); +} + +abstract class _PlansFilterState implements PlansFilterState { + const factory _PlansFilterState( + {required final (int?, int?) priceFilter, + required final int durationIndex, + required final int typeIndex}) = _$PlansFilterStateImpl; + + @override + (int?, int?) get priceFilter; + @override + int get durationIndex; + @override + int get typeIndex; + + /// Create a copy of PlansFilterState + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$PlansFilterStateImplCopyWith<_$PlansFilterStateImpl> 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 24ec166f..20573fc4 100644 --- a/lib/features/feed/presentation/pages/feed_screen.dart +++ b/lib/features/feed/presentation/pages/feed_screen.dart @@ -1,4 +1,6 @@ import 'package:easy_debounce/easy_debounce.dart'; +import 'package:eatvio_app/core/widgets/add_item_button.dart'; +import 'package:eatvio_app/core/widgets/custom_search_bar.dart'; import 'package:eatvio_app/features/create_diet/presentation/create_diet_screen.dart'; import 'package:eatvio_app/core/widgets/loader.dart'; import 'package:eatvio_app/features/create_plan/presentation/create_plan_screen.dart'; @@ -16,6 +18,7 @@ import 'package:eatvio_app/features/feed/presentation/widgets/diet_filter_bottom import 'package:eatvio_app/features/feed/presentation/widgets/diets_widget.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/filter_badge.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/plan_widget.dart'; +import 'package:eatvio_app/features/feed/presentation/widgets/plans_filter_bottom_sheet.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/recipes_widget.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'; @@ -94,8 +97,21 @@ class _FeedScreenState extends State { @override Widget build(BuildContext context) { - return BlocProvider( - create: (context) => sl()..getProfile(), + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => sl()..getProfile(), + ), + BlocProvider( + create: (context) => sl()..loadRecipesList(), + ), + BlocProvider( + create: (context) => sl()..loadDiets(), + ), + BlocProvider( + create: (context) => sl()..loadPlans(), + ), + ], child: Scaffold( floatingActionButton: _selectedChipIndex == 1 ? FloatingActionButton.small( @@ -149,19 +165,66 @@ class _FeedScreenState extends State { bottom: _chipGroup(), title: Row( children: [ - _searchBar(), + CustomSearchBar( + searchController: searchController, + onChanged: (query) { + if (query.length > 2) { + EasyDebounce.debounce( + 'searchFeed', + Duration(milliseconds: 500), + () { + switch (_selectedChipIndex) { + case 0: + feedCubit.loadFeedBlogPostList(query: query); + break; + case 1: + recipesCubit.loadRecipesList(query: query); + break; + case 2: + dietsCubit.loadDiets(query: query); + break; + case 3: + plansCubit.loadPlans(query: query); + break; + } + }, + ); + } else { + EasyDebounce.debounce( + 'searchFeed', + Duration(milliseconds: 500), + () { + switch (_selectedChipIndex) { + case 0: + feedCubit.loadFeedBlogPostList(); + break; + case 1: + recipesCubit.loadRecipesList(); + break; + case 2: + dietsCubit.loadDiets(); + break; + case 3: + plansCubit.loadPlans(); + break; + } + }, + ); + } + }, + ), SizedBox(width: 12), switch (_selectedChipIndex) { - 1 => _addItemButton( - onTap: () => GoRouter.of(context).pushNamed('recipeCreateScreen'), - ), - 2 => _addItemButton( + 1 => AddItemButton( + onTap: () => GoRouter.of(context).pushNamed('recipeCreateScreen'), + ), + 2 => AddItemButton( onTap: state.profile?.role == 'consultant' ? () => GoRouter.of(context).pushNamed(CreateDietScreen.name) : null, ), - 3 => _addItemButton( + 3 => AddItemButton( onTap: state.profile?.role == 'consultant' ? () => GoRouter.of(context).pushNamed(CreatePlanScreen.name) : null, ), - _ => _addItemButton() + _ => AddItemButton() }, ], ), @@ -170,21 +233,13 @@ class _FeedScreenState extends State { ), ]; }, - body: Builder( - builder: (BuildContext context) { - if (_selectedChipIndex == 0) { - return _buildAllBlogPosts(); - } else if (_selectedChipIndex == 1) { - return _buildAllRecipes(); - } else if (_selectedChipIndex == 2) { - return _buildDiets(); - } else if (_selectedChipIndex == 3) { - return _buildPlans(); - } else { - return Center(child: Text(chipsLabelList[_selectedChipIndex])); - } - }, - ), + body: switch (_selectedChipIndex) { + 0 => _buildAllBlogPosts(), + 1 => _buildAllRecipes(), + 2 => _buildDiets(), + 3 => _buildPlans(), + _ => Center(child: Text(chipsLabelList[_selectedChipIndex])), + }, ), ), ); @@ -233,140 +288,151 @@ class _FeedScreenState extends State { } _buildAllRecipes() { - return BlocProvider( - create: (context) => recipesCubit, - child: RefreshIndicator( - onRefresh: () => recipesCubit.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]); - }, - 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 { - return Scaffold( - backgroundColor: Colors.grey.shade200, - body: const Loader(), - ); - } - }, - ), - ), - ); - } - - _buildDiets() { - return BlocProvider( - create: (context) => dietsCubit, - child: RefreshIndicator( - onRefresh: () => dietsCubit.loadDiets(), - child: BlocBuilder( - builder: (BuildContext context, DietsState state) { - if (state.isLoading) { - return Scaffold( - backgroundColor: Colors.grey.shade200, - body: const Loader(), - ); - } else if (state.errorMessage != null) { - return Scaffold( - backgroundColor: Colors.grey.shade200, - body: Padding( - padding: const EdgeInsets.all(16), - child: Center( - child: Text(state.errorMessage ?? ''), - ), - ), - ); - } else { - return Stack( - children: [ - SizedBox.expand(), - ListView.separated( - padding: EdgeInsets.symmetric(vertical: 8), - shrinkWrap: true, - physics: BouncingScrollPhysics(), - itemBuilder: (context, index) { - return DietsWidget(diet: state.dietsList[index], index: index); - }, - separatorBuilder: (context, index) { - return SizedBox(height: 8); - }, - itemCount: state.dietsList.length, - ), - Positioned( - right: 0, - bottom: 74, - child: FilterBadge( - filterBottomSheet: DietFilterBottomSheet( - dietsCubit: dietsCubit, - ), - ), - ), - ], - ); - } - }, - ), - ), - ); - } - - _buildPlans() { - return BlocProvider( - create: (context) => plansCubit, - child: RefreshIndicator( - onRefresh: () => plansCubit.loadPlans(), - child: BlocBuilder( - builder: (BuildContext context, PlansState state) { - if (state.isLoading) { - return Scaffold( - backgroundColor: Colors.grey.shade200, - body: const Loader(), - ); - } else if (state.errorMessage != null) { - return Scaffold( - backgroundColor: Colors.grey.shade200, - body: Padding( - padding: const EdgeInsets.all(16), - child: Center( - child: Text(state.errorMessage ?? ''), - ), - ), - ); - } + return RefreshIndicator( + onRefresh: () => recipesCubit.loadRecipesList(), + child: BlocBuilder( + bloc: recipesCubit, + 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 PlanWidget(planModel: state.plans[index]); + return RecipesWidget(recipes: state.recipesList[index]); }, separatorBuilder: (context, index) { return SizedBox(height: 8); }, - itemCount: state.plans.length, + 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 { + return Scaffold( + backgroundColor: Colors.grey.shade200, + body: const Loader(), + ); + } + }, + ), + ); + } + + _buildDiets() { + return RefreshIndicator( + onRefresh: () => dietsCubit.loadDiets(), + child: BlocBuilder( + bloc: dietsCubit, + builder: (BuildContext context, DietsState state) { + if (state.isLoading) { + return Scaffold( + backgroundColor: Colors.grey.shade200, + body: const Loader(), + ); + } else if (state.errorMessage != null) { + return Scaffold( + backgroundColor: Colors.grey.shade200, + body: Padding( + padding: const EdgeInsets.all(16), + child: Center( + child: Text(state.errorMessage ?? ''), + ), + ), + ); + } else { + return Stack( + children: [ + SizedBox.expand(), + ListView.separated( + padding: EdgeInsets.symmetric(vertical: 8), + shrinkWrap: true, + physics: BouncingScrollPhysics(), + itemBuilder: (context, index) { + return DietsWidget(diet: state.dietsList[index], index: index); + }, + separatorBuilder: (context, index) { + return SizedBox(height: 8); + }, + itemCount: state.dietsList.length, + ), + Positioned( + right: 0, + bottom: 74, + child: FilterBadge( + filterBottomSheet: DietFilterBottomSheet( + dietsCubit: dietsCubit, + ), + ), + ), + ], + ); + } + }, + ), + ); + } + + _buildPlans() { + return RefreshIndicator( + onRefresh: () => plansCubit.loadPlans(), + child: BlocBuilder( + bloc: plansCubit, + builder: (BuildContext context, PlansState state) { + if (state.isLoading) { + return Scaffold( + backgroundColor: Colors.grey.shade200, + body: const Loader(), + ); + } else if (state.errorMessage != null) { + return Scaffold( + backgroundColor: Colors.grey.shade200, + body: Padding( + padding: const EdgeInsets.all(16), + child: Center( + child: Text(state.errorMessage ?? ''), + ), + ), + ); + } + return Stack( + children: [ + SizedBox.expand(), + ListView.separated( + padding: EdgeInsets.symmetric(vertical: 8), + shrinkWrap: true, + physics: BouncingScrollPhysics(), + itemBuilder: (context, index) { + return PlanWidget( + plan: state.plans[index], + index: index, + ); + }, + separatorBuilder: (context, index) { + return SizedBox(height: 8); + }, + itemCount: state.plans.length, + ), + Positioned( + right: 0, + bottom: 74, + child: FilterBadge( + filterBottomSheet: PlansFilterBottomSheet( + plansCubit: plansCubit, + ), + ), + ), + ], + ); + }, ), ); } @@ -451,108 +517,4 @@ class _FeedScreenState extends State { ), ); } - - _addItemButton({void Function()? onTap}) { - return InkWell( - onTap: onTap ?? - () { - GoRouter.of(context).pushNamed('blogPostCreateScreen'); - }, - borderRadius: BorderRadius.circular(64), - child: Ink( - decoration: BoxDecoration( - color: appTheme.blueA400, - shape: BoxShape.circle, - ), - child: Icon( - Icons.add_rounded, - color: appTheme.gray100, - ), - width: 40, - height: 40, - ), - ); - } - - _searchBar() { - return SizedBox( - height: 40, - width: MediaQuery.of(context).size.width - 84, - child: TextFormField( - controller: searchController, - autofocus: false, - style: TextStyle( - fontSize: 12, - ), - onChanged: (query) { - if (query.length > 2) { - EasyDebounce.debounce( - 'searchFeed', - Duration(milliseconds: 500), - () { - switch (_selectedChipIndex) { - case 0: - feedCubit.loadFeedBlogPostList(query: query); - break; - case 1: - recipesCubit.loadRecipesList(query: query); - break; - case 2: - dietsCubit.loadDiets(query: query); - break; - } - }, - ); - } else { - EasyDebounce.debounce( - 'searchFeed', - Duration(milliseconds: 500), - () { - switch (_selectedChipIndex) { - case 0: - feedCubit.loadFeedBlogPostList(); - break; - case 1: - recipesCubit.loadRecipesList(); - break; - case 2: - dietsCubit.loadDiets(query: query == '' ? null : query); - break; - } - }, - ); - } - }, - decoration: InputDecoration( - hintText: 'Поиск', - hintStyle: TextStyle( - fontSize: 12, - color: appTheme.gray600, - fontWeight: FontWeight.w400, - ), - prefixIcon: Icon( - Icons.search, - color: appTheme.gray600, - size: 16, - ), - isDense: false, - contentPadding: EdgeInsets.all(8), - fillColor: appTheme.gray100, - filled: true, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(16), - borderSide: BorderSide.none, - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(16), - borderSide: BorderSide.none, - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(16), - borderSide: BorderSide.none, - ), - ), - ), - ); - } } diff --git a/lib/features/feed/presentation/widgets/diet_filter_bottom_sheet.dart b/lib/features/feed/presentation/widgets/diet_filter_bottom_sheet.dart index cdd87f8a..e055ea4f 100644 --- a/lib/features/feed/presentation/widgets/diet_filter_bottom_sheet.dart +++ b/lib/features/feed/presentation/widgets/diet_filter_bottom_sheet.dart @@ -4,14 +4,16 @@ import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_cub import 'package:eatvio_app/features/feed/presentation/widgets/duration_choice_filter_widget.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/sorting_drop_down_filter.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/type_choice_filter_widget.dart'; +import 'package:eatvio_app/features/my_diets/presentation/cubit/my_diets_cubit.dart'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:flutter/material.dart'; class DietFilterBottomSheet extends StatelessWidget { - const DietFilterBottomSheet({super.key, required this.dietsCubit}); - final DietsCubit dietsCubit; + const DietFilterBottomSheet({super.key, this.dietsCubit, this.myDietsCubit}); + final DietsCubit? dietsCubit; + final MyDietsCubit? myDietsCubit; @override Widget build(BuildContext context) { final s = S.of(context); @@ -31,22 +33,10 @@ class DietFilterBottomSheet extends StatelessWidget { style: TextStyles.textMSemibold.copyWith(color: appTheme.black), ), SizedBox(height: 12), - SortingDropDownFilter(cubit: dietsCubit,), - SizedBox( - height: 16, + SortingDropDownFilter( + dietsCubit: dietsCubit, + myDietsCubit: myDietsCubit, ), - CustomDivider(), - SizedBox( - height: 16, - ), - Text( - s.diet_time, - style: TextStyles.textMSemibold.copyWith(color: appTheme.black), - ), - SizedBox( - height: 12, - ), - TypeChoiceFilterWidget(cubit: dietsCubit,), SizedBox( height: 16, ), @@ -61,7 +51,28 @@ class DietFilterBottomSheet extends StatelessWidget { SizedBox( height: 12, ), - DurationChoiceFilterWidget(cubit: dietsCubit,), + TypeChoiceFilterWidget( + dietsCubit: dietsCubit, + myDietsCubit: myDietsCubit, + ), + SizedBox( + height: 16, + ), + CustomDivider(), + SizedBox( + height: 16, + ), + Text( + s.time, + style: TextStyles.textMSemibold.copyWith(color: appTheme.black), + ), + SizedBox( + height: 12, + ), + DurationChoiceFilterWidget( + dietCubit: dietsCubit, + myDietsCubit: myDietsCubit, + ), SizedBox( height: 24, ), diff --git a/lib/features/feed/presentation/widgets/diets_widget.dart b/lib/features/feed/presentation/widgets/diets_widget.dart index f18c61f7..6597f72e 100644 --- a/lib/features/feed/presentation/widgets/diets_widget.dart +++ b/lib/features/feed/presentation/widgets/diets_widget.dart @@ -1,7 +1,7 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/widgets/bookmark_button.dart'; -import 'package:eatvio_app/core/widgets/diet_info_row.dart'; +import 'package:eatvio_app/core/widgets/info_row.dart'; import 'package:eatvio_app/core/widgets/feed_icon_button.dart'; import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart'; diff --git a/lib/features/feed/presentation/widgets/duration_choice_filter_widget.dart b/lib/features/feed/presentation/widgets/duration_choice_filter_widget.dart index d3a5ec9b..45c966d8 100644 --- a/lib/features/feed/presentation/widgets/duration_choice_filter_widget.dart +++ b/lib/features/feed/presentation/widgets/duration_choice_filter_widget.dart @@ -2,21 +2,29 @@ import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/utils/localization_utils.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_state.dart'; +import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_cubit.dart'; +import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_state.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/choice_chip_widget.dart'; +import 'package:eatvio_app/features/my_diets/presentation/cubit/my_diets_cubit.dart'; +import 'package:eatvio_app/features/my_diets/presentation/cubit/my_diets_state.dart'; +import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_cubit.dart'; +import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_state.dart'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class DurationChoiceFilterWidget extends StatefulWidget { - const DurationChoiceFilterWidget({super.key, required this.cubit}); - final DietsCubit cubit; + const DurationChoiceFilterWidget({super.key, this.dietCubit, this.plansCubit, this.myDietsCubit, this.myPlansCubit}); + final DietsCubit? dietCubit; + final PlansCubit? plansCubit; + final MyDietsCubit? myDietsCubit; + final MyPlansCubit? myPlansCubit; @override State createState() => _DurationChoiceFilterWidgetState(); } class _DurationChoiceFilterWidgetState extends State { final List chipsLabelList = []; - @override void didChangeDependencies() { final s = S.of(context); @@ -31,51 +39,104 @@ class _DurationChoiceFilterWidgetState extends State @override Widget build(BuildContext context) { - return BlocBuilder( - bloc: widget.cubit, + if (widget.dietCubit != null) { + return BlocBuilder( + bloc: widget.dietCubit!, + builder: (context, state) { + return Wrap( + children: [ + ...List.generate( + chipsLabelList.length, + (index) => Padding( + padding: EdgeInsets.only(right: index != chipsLabelList.length - 1 ? 8 : 0), + child: ChoiceChipWidget( + chipsLabelList: chipsLabelList, + index: index, + selectedChipIndex: state.filterState.durationIndex, + onSelected: (selectedValue) { + widget.dietCubit?.changeDurationFilter(index); + }, + ), + ), + ), + ], + ); + }, + ); + } else if (widget.plansCubit != null) { + return BlocBuilder( + bloc: widget.plansCubit, + builder: (context, state) { + return Wrap( + children: [ + ...List.generate( + chipsLabelList.length, + (index) => Padding( + padding: EdgeInsets.only(right: index != chipsLabelList.length - 1 ? 8 : 0), + child: ChoiceChipWidget( + chipsLabelList: chipsLabelList, + index: index, + selectedChipIndex: state.filterState.durationIndex, + onSelected: (selectedValue) { + widget.plansCubit?.changeDurationFilter(index); + }, + ), + ), + ), + ], + ); + }, + ); + } else if (widget.myPlansCubit != null) { + return BlocBuilder( + bloc: widget.myPlansCubit, + builder: (context, state) { + return Wrap( + children: [ + ...List.generate( + chipsLabelList.length, + (index) => Padding( + padding: EdgeInsets.only(right: index != chipsLabelList.length - 1 ? 8 : 0), + child: ChoiceChipWidget( + chipsLabelList: chipsLabelList, + index: index, + selectedChipIndex: switch (state.selectedStateIndex) { + 1 => state.draftFilterState.durationIndex, + _ => state.activeFilterState.durationIndex + }, + onSelected: (selectedValue) { + widget.myPlansCubit?.changeDurationFilter(index); + }, + ), + ), + ), + ], + ); + }, + ); + } + return BlocBuilder( + bloc: widget.myDietsCubit, builder: (context, state) { return Wrap( children: [ - ChoiceChipWidget( - chipsLabelList: chipsLabelList, - index: 0, - selectedChipIndex: state.filterState.durationIndex, - onSelected: (selectedValue) { - widget.cubit.changeDurationFilter(0); - }, - ), - SizedBox( - width: 8, - ), - ChoiceChipWidget( - chipsLabelList: chipsLabelList, - index: 1, - selectedChipIndex: state.filterState.durationIndex, - onSelected: (selectedValue) { - widget.cubit.changeDurationFilter(1); - }, - ), - SizedBox( - width: 8, - ), - ChoiceChipWidget( - chipsLabelList: chipsLabelList, - index: 2, - selectedChipIndex: state.filterState.durationIndex, - onSelected: (selectedValue) { - widget.cubit.changeDurationFilter(2); - }, - ), - SizedBox( - width: 8, - ), - ChoiceChipWidget( - chipsLabelList: chipsLabelList, - index: 3, - selectedChipIndex: state.filterState.durationIndex, - onSelected: (selectedValue) { - widget.cubit.changeDurationFilter(3); - }, + ...List.generate( + chipsLabelList.length, + (index) => Padding( + padding: EdgeInsets.only(right: index != chipsLabelList.length - 1 ? 8 : 0), + child: ChoiceChipWidget( + chipsLabelList: chipsLabelList, + index: index, + selectedChipIndex: switch (state.selectedStateIndex) { + 1 => state.draftFilterState.durationIndex, + 2 => state.personalFilterState.durationIndex, + _ => state.activeFilterState.durationIndex + }, + onSelected: (selectedValue) { + widget.myDietsCubit?.changeDurationFilter(index); + }, + ), + ), ), ], ); diff --git a/lib/features/feed/presentation/widgets/filter_badge.dart b/lib/features/feed/presentation/widgets/filter_badge.dart index 6f7f5d9f..687d326a 100644 --- a/lib/features/feed/presentation/widgets/filter_badge.dart +++ b/lib/features/feed/presentation/widgets/filter_badge.dart @@ -10,11 +10,13 @@ class FilterBadge extends StatelessWidget { return GestureDetector( onTap: () { showModalBottomSheet( - context: context, - builder: (context) => filterBottomSheet, - useRootNavigator: true, - barrierColor: Colors.transparent, - backgroundColor: appTheme.whiteA700,); + context: context, + isScrollControlled: true, + builder: (context) => filterBottomSheet, + useRootNavigator: true, + barrierColor: Colors.transparent, + backgroundColor: appTheme.whiteA700, + ); }, child: Container( padding: EdgeInsets.only(left: 20, top: 12, bottom: 12, right: 24), diff --git a/lib/features/feed/presentation/widgets/plan_widget.dart b/lib/features/feed/presentation/widgets/plan_widget.dart index 4b06166a..f51dac71 100644 --- a/lib/features/feed/presentation/widgets/plan_widget.dart +++ b/lib/features/feed/presentation/widgets/plan_widget.dart @@ -1,174 +1,228 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/widgets/bookmark_button.dart'; +import 'package:eatvio_app/core/widgets/info_row.dart'; +import 'package:eatvio_app/core/widgets/feed_icon_button.dart'; +import 'package:eatvio_app/features/diets/presentation/widgets/info_bouble.dart'; import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart'; +import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart'; +import 'package:eatvio_app/core/theme/custom_text_style.dart'; +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_cubit.dart'; +import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_html/flutter_html.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:go_router/go_router.dart'; +import 'package:share_plus/share_plus.dart'; -class PlanWidget extends StatelessWidget { - final PlanModel planModel; - +class PlanWidget extends StatefulWidget { + final PlanModel plan; + final int index; const PlanWidget({ super.key, - required this.planModel, + required this.plan, + required this.index, }); + @override + State createState() => _PlanWidgetState(); +} + +class _PlanWidgetState extends State { @override Widget build(BuildContext context) { - return InkWell( - borderRadius: BorderRadius.circular(12), - onTap: () => GoRouter.of(context).pushNamed( - 'aboutPlanScreen', - extra: { - 'plan': planModel, - 'navigateToComments': false, - }, + final s = S.of(context); + return DecoratedBox( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: Colors.white, ), - child: Ink( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - color: Colors.white, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Stack( - children: [ - ClipRRect( + 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( borderRadius: BorderRadius.vertical(top: Radius.circular(12)), child: CachedNetworkImage( - imageUrl: baseUrl + '/storage/' + planModel.image, + imageUrl: baseUrl + '/storage/' + widget.plan.image, fit: BoxFit.fitWidth, width: MediaQuery.of(context).size.width, ), ), - Positioned( - left: 8, - bottom: 8, - child: Wrap( - runSpacing: 8, - children: [ - _iconButton(Icons.timelapse_rounded, '${planModel.duration} дней'), - SizedBox(width: 8), - ], + ), + 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( - left: 8, - top: 8, - child: Container( - padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: Colors.white.withValues(alpha: 0.8), - borderRadius: BorderRadius.circular(16), - ), - child: Row( - children: [ - Icon( - Icons.favorite_rounded, - size: 16, - color: Colors.black, - ), - SizedBox(width: 4), - Text( - '0', - style: TextStyle( - fontSize: 12, - color: Colors.black, - fontWeight: FontWeight.w400, - ), - ), - ], - ), - ), + ), + ], + ), + 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), ), - Positioned( - right: 8, - top: 8, - child: Container( - padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: Colors.white.withValues(alpha: 0.8), - borderRadius: BorderRadius.circular(16), - ), - child: Row( - children: [ - Icon( - Icons.visibility_rounded, - size: 16, - color: Colors.black, - ), - SizedBox(width: 4), - Text( - '0', - style: TextStyle( - fontSize: 12, - color: Colors.black, - fontWeight: FontWeight.w400, - ), - ), - ], - ), + ), + 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: 12), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 12), - child: Text( - planModel.name, - style: TextStyle( - fontWeight: FontWeight.w600, - color: Colors.black87, + ], + ), + 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, + ), ), ), ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 4), - child: Html( - data: planModel.details, - style: { - "*": Style( - color: Colors.black87, - ), - }, - ), - ), - SizedBox(height: 12), - ], - ), - ), - ); - } - - _iconButton(IconData icon, [String? text]) { - return Container( - padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4), - decoration: BoxDecoration( - color: Colors.white.withValues(alpha: 0.8), - borderRadius: BorderRadius.circular(16), - ), - height: 32, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - icon, - size: 16, - color: Colors.black, ), - if (text != null) SizedBox(width: 8), - if (text != null) - Text( - text, - style: TextStyle( - fontSize: 12, - color: Colors.black, - fontWeight: FontWeight.w500, - ), + 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().likeDiet(widget.plan.id, 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 result = await Share.shareUri( + Uri.parse( + baseUrl + '/ru/diet/' + widget.plan.id.toString(), + ), + ); + 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), ], ), ); diff --git a/lib/features/feed/presentation/widgets/plans_filter_bottom_sheet.dart b/lib/features/feed/presentation/widgets/plans_filter_bottom_sheet.dart new file mode 100644 index 00000000..9bc794ee --- /dev/null +++ b/lib/features/feed/presentation/widgets/plans_filter_bottom_sheet.dart @@ -0,0 +1,78 @@ +import 'package:eatvio_app/core/widgets/bottom_sheet_handler_bar.dart'; +import 'package:eatvio_app/core/widgets/custom_divider.dart'; +import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_cubit.dart'; +import 'package:eatvio_app/features/feed/presentation/widgets/duration_choice_filter_widget.dart'; +import 'package:eatvio_app/features/feed/presentation/widgets/price_choice_filter_widget.dart'; +import 'package:eatvio_app/features/feed/presentation/widgets/type_choice_filter_widget.dart'; +import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_cubit.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:eatvio_app/core/theme/custom_text_style.dart'; +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:flutter/material.dart'; + +class PlansFilterBottomSheet extends StatelessWidget { + const PlansFilterBottomSheet({super.key, this.plansCubit, this.myPlansCubit}); + final PlansCubit? plansCubit; + final MyPlansCubit? myPlansCubit; + @override + Widget build(BuildContext context) { + final s = S.of(context); + return SizedBox( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(height: 12), + Center(child: BottomSheetHandlerBar()), + SizedBox(height: 12), + Text( + s.price, + style: TextStyles.textMSemibold.copyWith(color: appTheme.black), + ), + SizedBox(height: 12), + PriceChoiceFilterWidget(plansCubit: plansCubit, myPlansCubit: myPlansCubit,), + SizedBox(height: 16), + CustomDivider(), + SizedBox( + height: 16, + ), + Text( + s.time, + style: TextStyles.textMSemibold.copyWith(color: appTheme.black), + ), + SizedBox( + height: 12, + ), + DurationChoiceFilterWidget( + plansCubit: plansCubit, + myPlansCubit: myPlansCubit, + ), + SizedBox( + height: 16, + ), + CustomDivider(), + SizedBox( + height: 16, + ), + Text( + s.plan_type, + style: TextStyles.textMSemibold.copyWith(color: appTheme.black), + ), + SizedBox( + height: 12, + ), + TypeChoiceFilterWidget( + plansCubit: plansCubit, + myPlansCubit: myPlansCubit, + ), + SizedBox( + height: 24, + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/feed/presentation/widgets/price_choice_filter_widget.dart b/lib/features/feed/presentation/widgets/price_choice_filter_widget.dart new file mode 100644 index 00000000..b01a5bbd --- /dev/null +++ b/lib/features/feed/presentation/widgets/price_choice_filter_widget.dart @@ -0,0 +1,348 @@ +import 'package:easy_debounce/easy_debounce.dart'; +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/core/widgets/custom_text_form_field.dart'; +import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_cubit.dart'; +import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_state.dart'; +import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_cubit.dart'; +import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_state.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class PriceChoiceFilterWidget extends StatefulWidget { + PriceChoiceFilterWidget({ + super.key, + required this.plansCubit, + this.myPlansCubit, + }); + final PlansCubit? plansCubit; + final MyPlansCubit? myPlansCubit; + + @override + State createState() => _PriceChoiceFilterWidgetState(); +} + +class _PriceChoiceFilterWidgetState extends State { + final minPriceController = TextEditingController()..text = '₽'; + final maxPriceController = TextEditingController()..text = '₽'; + int? prevMinPrice; + int? prevMaxPrice; + @override + void initState() { + super.initState(); + minPriceController.addListener(() { + if (!minPriceController.text.endsWith('₽')) { + final textWithoutCurrency = minPriceController.text.replaceAll('₽', ''); + minPriceController.value = TextEditingValue( + text: "$textWithoutCurrency₽", + selection: TextSelection.collapsed(offset: textWithoutCurrency.length), + ); + } + }); + maxPriceController.addListener(() { + if (!maxPriceController.text.endsWith('₽')) { + final textWithoutCurrency = maxPriceController.text.replaceAll('₽', ''); + maxPriceController.value = TextEditingValue( + text: "$textWithoutCurrency₽", + selection: TextSelection.collapsed(offset: textWithoutCurrency.length), + ); + } + }); + } + + @override + Widget build(BuildContext context) { + if (widget.myPlansCubit != null) { + return BlocBuilder( + bloc: widget.myPlansCubit, + builder: (context, state) { + final minPlanPrice = switch (state.selectedStateIndex) { + 1 => state.minDraftPlanPrice, + _ => state.minActivePlanPrice, + }; + + final maxPlanPrice = switch (state.selectedStateIndex) { + 1 => state.maxDraftPlanPrice, + _ => state.maxActivePlanPrice, + }; + + final minPriceFilter = switch (state.selectedStateIndex) { + 1 => state.draftFilterState.priceFilter.$1, + _ => state.activeFilterState.priceFilter.$1, + }; + + final maxPriceFilter = switch (state.selectedStateIndex) { + 1 => state.draftFilterState.priceFilter.$2, + _ => state.activeFilterState.priceFilter.$2, + }; + + prevMinPrice ??= minPlanPrice; + prevMaxPrice ??= maxPlanPrice; + + minPriceController.text = minPriceFilter?.toStringAsFixed(0) ?? '₽'; + maxPriceController.text = maxPriceFilter?.toStringAsFixed(0) ?? '₽'; + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + Expanded( + child: CustomTextFormField( + keyboardType: TextInputType.number, + controller: minPriceController, + onChanged: (value) { + final price = int.tryParse(value.replaceAll('₽', '')); + if (price == null) { + EasyDebounce.cancel('changeMinPriceFilter'); + minPriceController.value = TextEditingValue( + text: '₽', + selection: TextSelection.collapsed(offset: 0), + ); + return; + } + if (price < minPlanPrice) { + minPriceController.value = TextEditingValue( + text: '${prevMinPrice ?? ''}₽', + selection: TextSelection.collapsed(offset: prevMinPrice.toString().length), + ); + return; + } + if (price > (maxPriceFilter ?? maxPlanPrice)) { + minPriceController.value = TextEditingValue( + text: '${prevMinPrice ?? ''}₽', + selection: TextSelection.collapsed(offset: prevMinPrice.toString().length), + ); + EasyDebounce.cancel('changeMinPriceFilter'); + widget.myPlansCubit?.changePriceFilter( + prevMinPrice ?? minPlanPrice, + maxPriceFilter ?? maxPlanPrice, + ); + return; + } + prevMinPrice = price; + EasyDebounce.debounce( + 'changeMinPriceFilter', + Duration(seconds: 2), + () => widget.myPlansCubit?.changePriceFilter( + price, + maxPriceFilter ?? maxPlanPrice, + disableDebounce: true, + ), + ); + }, + ), + ), + SizedBox(width: 8), + Expanded( + child: CustomTextFormField( + onChanged: (value) { + final price = int.tryParse(value.replaceAll('₽', '')); + if (price == null) { + EasyDebounce.cancel('changeMaxPriceFilter'); + maxPriceController.value = TextEditingValue( + text: '₽', + selection: TextSelection.collapsed(offset: 0), + ); + return; + } + if (price > maxPlanPrice) { + maxPriceController.value = TextEditingValue( + text: '${prevMaxPrice ?? ''}₽', + selection: TextSelection.collapsed(offset: prevMaxPrice.toString().length), + ); + return; + } + if (price < (minPriceFilter ?? minPlanPrice)) { + maxPriceController.value = TextEditingValue( + text: '${prevMaxPrice ?? ''}₽', + selection: TextSelection.collapsed(offset: prevMaxPrice.toString().length), + ); + EasyDebounce.cancel('changeMaxPriceFilter'); + widget.myPlansCubit?.changePriceFilter( + minPriceFilter ?? minPlanPrice, + prevMaxPrice ?? maxPlanPrice, + ); + return; + } + prevMaxPrice = price; + EasyDebounce.debounce( + 'changeMaxPriceFilter', + Duration(seconds: 2), + () => widget.myPlansCubit?.changePriceFilter( + minPriceFilter ?? minPlanPrice, + price, + disableDebounce: true, + ), + ); + }, + keyboardType: TextInputType.number, + controller: maxPriceController, + ), + ), + ], + ), + SizedBox(height: 12), + Theme( + data: Theme.of(context).copyWith( + sliderTheme: SliderThemeData( + overlayShape: SliderComponentShape.noOverlay, + ), + ), + child: RangeSlider( + activeColor: appTheme.blueA400, + inactiveColor: appTheme.blueA400, + min: minPlanPrice.toDouble(), + max: maxPlanPrice.toDouble(), + onChanged: (values) { + if (values.start <= values.end) { + widget.myPlansCubit?.changePriceFilter(values.start.toInt(), values.end.toInt()); + } + }, + values: RangeValues( + minPriceFilter?.toDouble() ?? minPlanPrice.toDouble(), + maxPriceFilter?.toDouble() ?? maxPlanPrice.toDouble(), + ), + ), + ), + ], + ); + }, + ); + } + return BlocBuilder( + bloc: widget.plansCubit, + builder: (context, state) { + prevMinPrice ??= state.minPlanPrice; + prevMaxPrice ??= state.maxPlanPrice; + + minPriceController.text = state.filterState.priceFilter.$1?.toStringAsFixed(0) ?? '₽'; + maxPriceController.text = state.filterState.priceFilter.$2?.toStringAsFixed(0) ?? '₽'; + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + Expanded( + child: CustomTextFormField( + keyboardType: TextInputType.number, + controller: minPriceController, + onChanged: (value) { + final price = int.tryParse(value.replaceAll('₽', '')); + if (price == null) { + EasyDebounce.cancel('changeMinPriceFilter'); + minPriceController.value = TextEditingValue( + text: '₽', + selection: TextSelection.collapsed(offset: 0), + ); + return; + } + if (price < state.minPlanPrice) { + minPriceController.value = TextEditingValue( + text: '${prevMinPrice ?? ''}₽', + selection: TextSelection.collapsed(offset: prevMinPrice.toString().length), + ); + return; + } + if (price > (state.filterState.priceFilter.$2 ?? state.maxPlanPrice)) { + minPriceController.value = TextEditingValue( + text: '${prevMinPrice ?? ''}₽', + selection: TextSelection.collapsed(offset: prevMinPrice.toString().length), + ); + EasyDebounce.cancel('changeMinPriceFilter'); + widget.plansCubit?.changePriceFilter( + prevMinPrice ?? state.minPlanPrice, + state.filterState.priceFilter.$2 ?? state.maxPlanPrice, + ); + return; + } + prevMinPrice = price; + EasyDebounce.debounce( + 'changeMinPriceFilter', + Duration(seconds: 2), + () => widget.plansCubit?.changePriceFilter( + price, + state.filterState.priceFilter.$2 ?? state.maxPlanPrice, + disableDebounce: true, + ), + ); + }, + ), + ), + SizedBox(width: 8), + Expanded( + child: CustomTextFormField( + onChanged: (value) { + final price = int.tryParse(value.replaceAll('₽', '')); + if (price == null) { + EasyDebounce.cancel('changeMaxPriceFilter'); + maxPriceController.value = TextEditingValue( + text: '₽', + selection: TextSelection.collapsed(offset: 0), + ); + return; + } + if (price > state.maxPlanPrice) { + maxPriceController.value = TextEditingValue( + text: '${prevMaxPrice ?? ''}₽', + selection: TextSelection.collapsed(offset: prevMaxPrice.toString().length), + ); + return; + } + if (price < (state.filterState.priceFilter.$1 ?? state.minPlanPrice)) { + maxPriceController.value = TextEditingValue( + text: '${prevMaxPrice ?? ''}₽', + selection: TextSelection.collapsed(offset: prevMaxPrice.toString().length), + ); + EasyDebounce.cancel('changeMaxPriceFilter'); + widget.plansCubit?.changePriceFilter( + state.filterState.priceFilter.$1 ?? state.minPlanPrice, + prevMaxPrice ?? state.maxPlanPrice, + ); + return; + } + prevMaxPrice = price; + EasyDebounce.debounce( + 'changeMaxPriceFilter', + Duration(seconds: 2), + () => widget.plansCubit?.changePriceFilter( + state.filterState.priceFilter.$1 ?? state.minPlanPrice, + price, + disableDebounce: true, + ), + ); + }, + keyboardType: TextInputType.number, + controller: maxPriceController, + ), + ), + ], + ), + SizedBox(height: 12), + Theme( + data: Theme.of(context).copyWith( + sliderTheme: SliderThemeData( + overlayShape: SliderComponentShape.noOverlay, + ), + ), + child: RangeSlider( + activeColor: appTheme.blueA400, + inactiveColor: appTheme.blueA400, + min: state.minPlanPrice.toDouble(), + max: state.maxPlanPrice.toDouble(), + onChanged: (values) { + if (values.start <= values.end) { + widget.plansCubit?.changePriceFilter(values.start.toInt(), values.end.toInt()); + } + }, + values: RangeValues( + state.filterState.priceFilter.$1?.toDouble() ?? state.minPlanPrice.toDouble(), + state.filterState.priceFilter.$2?.toDouble() ?? state.maxPlanPrice.toDouble(), + ), + ), + ), + ], + ); + }, + ); + } +} diff --git a/lib/features/feed/presentation/widgets/sorting_drop_down_filter.dart b/lib/features/feed/presentation/widgets/sorting_drop_down_filter.dart index ec85409d..f03605ef 100644 --- a/lib/features/feed/presentation/widgets/sorting_drop_down_filter.dart +++ b/lib/features/feed/presentation/widgets/sorting_drop_down_filter.dart @@ -2,6 +2,8 @@ import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/widgets/custom_drop_down_button.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_state.dart'; +import 'package:eatvio_app/features/my_diets/presentation/cubit/my_diets_cubit.dart'; +import 'package:eatvio_app/features/my_diets/presentation/cubit/my_diets_state.dart'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:flutter/material.dart'; @@ -9,8 +11,9 @@ import 'package:collection/collection.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class SortingDropDownFilter extends StatefulWidget { - const SortingDropDownFilter({super.key, required this.cubit}); - final DietsCubit cubit; + const SortingDropDownFilter({super.key, this.dietsCubit, this.myDietsCubit}); + final DietsCubit? dietsCubit; + final MyDietsCubit? myDietsCubit; @override State createState() => _SortingDropDownFilterState(); } @@ -33,9 +36,40 @@ class _SortingDropDownFilterState extends State { @override Widget build(BuildContext context) { - return BlocBuilder( - bloc: widget.cubit, + if (widget.dietsCubit != null) { + return BlocBuilder( + bloc: widget.dietsCubit!, + builder: (context, state) { + return CustomDropDownButton( + items: sortingTypes + .mapIndexed( + (index, value) => DropdownMenuItem( + child: Text( + sortingTypes[index], + style: TextStyles.textMMedium.copyWith(color: appTheme.black), + ), + value: sortingTypes[index], + ), + ) + .toList(), + value: sortingTypes[state.filterState.sortingIndex], + onChanged: (value) { + if (value != null) { + widget.dietsCubit!.changeSortingType(sortingTypes.indexOf(value)); + } + }, + ); + }, + ); + } + return BlocBuilder( + bloc: widget.myDietsCubit!, builder: (context, state) { + final sortingIndex = switch (state.selectedStateIndex) { + 1 => state.draftFilterState.sortingIndex, + 2 => state.personalFilterState.sortingIndex, + _ => state.activeFilterState.sortingIndex + }; return CustomDropDownButton( items: sortingTypes .mapIndexed( @@ -48,10 +82,10 @@ class _SortingDropDownFilterState extends State { ), ) .toList(), - value: sortingTypes[state.filterState.sortingIndex], + value: sortingTypes[sortingIndex], onChanged: (value) { if (value != null) { - widget.cubit.changeSortingType(sortingTypes.indexOf(value)); + widget.myDietsCubit!.changeSortingType(sortingTypes.indexOf(value)); } }, ); diff --git a/lib/features/feed/presentation/widgets/type_choice_filter_widget.dart b/lib/features/feed/presentation/widgets/type_choice_filter_widget.dart index b4cd6bca..d6009470 100644 --- a/lib/features/feed/presentation/widgets/type_choice_filter_widget.dart +++ b/lib/features/feed/presentation/widgets/type_choice_filter_widget.dart @@ -1,13 +1,28 @@ import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_state.dart'; +import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_cubit.dart'; +import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_state.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/choice_chip_widget.dart'; +import 'package:eatvio_app/features/my_diets/presentation/cubit/my_diets_cubit.dart'; +import 'package:eatvio_app/features/my_diets/presentation/cubit/my_diets_state.dart'; +import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_cubit.dart'; +import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_state.dart'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class TypeChoiceFilterWidget extends StatefulWidget { - const TypeChoiceFilterWidget({super.key, required this.cubit}); - final DietsCubit cubit; + const TypeChoiceFilterWidget({ + super.key, + this.dietsCubit, + this.plansCubit, + this.myDietsCubit, + this.myPlansCubit, + }); + final DietsCubit? dietsCubit; + final PlansCubit? plansCubit; + final MyDietsCubit? myDietsCubit; + final MyPlansCubit? myPlansCubit; @override State createState() => _TypeChoiceFilterWidgetState(); } @@ -18,73 +33,125 @@ class _TypeChoiceFilterWidgetState extends State { void didChangeDependencies() { final s = S.of(context); super.didChangeDependencies(); - if (chipsLabelList.isEmpty) { - chipsLabelList.add(s.all); - chipsLabelList.add(s.vegan_type); - chipsLabelList.add(s.light_type); - chipsLabelList.add(s.carb_type); - chipsLabelList.add(s.meat_type); + if (widget.dietsCubit != null || widget.myDietsCubit != null) { + if (chipsLabelList.isEmpty) { + chipsLabelList.add(s.all); + chipsLabelList.add(s.vegan_type); + chipsLabelList.add(s.light_type); + chipsLabelList.add(s.carb_type); + chipsLabelList.add(s.meat_type); + } + } else { + if (chipsLabelList.isEmpty) { + chipsLabelList.add(s.all); + chipsLabelList.add(s.fit_type); + chipsLabelList.add(s.slim_type); + chipsLabelList.add(s.wellness_type); + chipsLabelList.add(s.eating_behavior_type); + } } } @override Widget build(BuildContext context) { - return BlocBuilder( - bloc: widget.cubit, + if (widget.plansCubit != null) { + return BlocBuilder( + bloc: widget.plansCubit, + builder: (context, state) { + return Wrap( + children: [ + ...List.generate( + chipsLabelList.length, + (index) => Padding( + padding: EdgeInsets.only(right: index != chipsLabelList.length - 1 ? 8.0 : 0), + child: ChoiceChipWidget( + chipsLabelList: chipsLabelList, + index: index, + selectedChipIndex: state.filterState.typeIndex, + onSelected: (selectedValue) { + widget.plansCubit?.changeTypeFilter(index); + }, + ), + ), + ), + ], + ); + }, + ); + } else if (widget.dietsCubit != null) { + return BlocBuilder( + bloc: widget.dietsCubit, + builder: (context, state) { + return Wrap( + children: [ + ...List.generate( + chipsLabelList.length, + (index) => Padding( + padding: EdgeInsets.only(right: index != chipsLabelList.length - 1 ? 8.0 : 0), + child: ChoiceChipWidget( + chipsLabelList: chipsLabelList, + index: index, + selectedChipIndex: state.filterState.typeIndex, + onSelected: (selectedValue) { + widget.dietsCubit?.changeTypeFilter(index); + }, + ), + ), + ), + ], + ); + }, + ); + } else if (widget.myPlansCubit != null) { + return BlocBuilder( + bloc: widget.myPlansCubit, + builder: (context, state) { + return Wrap( + children: [ + ...List.generate( + chipsLabelList.length, + (index) => Padding( + padding: EdgeInsets.only(right: index != chipsLabelList.length - 1 ? 8.0 : 0), + child: ChoiceChipWidget( + chipsLabelList: chipsLabelList, + index: index, + selectedChipIndex: switch (state.selectedStateIndex) { + 1 => state.draftFilterState.typeIndex, + _ => state.activeFilterState.typeIndex + }, + onSelected: (selectedValue) { + widget.myPlansCubit?.changeTypeFilter(index); + }, + ), + ), + ), + ], + ); + }, + ); + } + return BlocBuilder( + bloc: widget.myDietsCubit, builder: (context, state) { return Wrap( children: [ - ChoiceChipWidget( - chipsLabelList: chipsLabelList, - index: 0, - selectedChipIndex: state.filterState.typeIndex, - onSelected: (selectedValue) { - widget.cubit.changeTypeFilter(0); - }, - ), - SizedBox( - width: 8, - ), - ChoiceChipWidget( - chipsLabelList: chipsLabelList, - index: 1, - selectedChipIndex: state.filterState.typeIndex, - onSelected: (selectedValue) { - widget.cubit.changeTypeFilter(1); - }, - ), - SizedBox( - width: 8, - ), - ChoiceChipWidget( - chipsLabelList: chipsLabelList, - index: 2, - selectedChipIndex: state.filterState.typeIndex, - onSelected: (selectedValue) { - widget.cubit.changeTypeFilter(2); - }, - ), - SizedBox( - width: 8, - ), - ChoiceChipWidget( - chipsLabelList: chipsLabelList, - index: 3, - selectedChipIndex: state.filterState.typeIndex, - onSelected: (selectedValue) { - widget.cubit.changeTypeFilter(3); - }, - ), - SizedBox( - width: 8, - ), - ChoiceChipWidget( - chipsLabelList: chipsLabelList, - index: 4, - selectedChipIndex: state.filterState.typeIndex, - onSelected: (selectedValue) { - widget.cubit.changeTypeFilter(4); - }, + ...List.generate( + chipsLabelList.length, + (index) => Padding( + padding: EdgeInsets.only(right: index != chipsLabelList.length - 1 ? 8.0 : 0), + child: ChoiceChipWidget( + chipsLabelList: chipsLabelList, + index: index, + selectedChipIndex: switch (state.selectedStateIndex) { + 1 => state.draftFilterState.typeIndex, + 2 => state.personalFilterState.typeIndex, + _ => state.activeFilterState.typeIndex + }, + onSelected: (selectedValue) { + widget.myDietsCubit?.changeTypeFilter(index); + }, + ), + ), ), ], ); diff --git a/lib/features/feed/repositories/feed_repository.dart b/lib/features/feed/repositories/feed_repository.dart index 6f858ac7..9c3360fc 100644 --- a/lib/features/feed/repositories/feed_repository.dart +++ b/lib/features/feed/repositories/feed_repository.dart @@ -68,11 +68,17 @@ class FeedRepository { String? query, String? types, int? duration, - String? sorting, + int? minPrice, + int? maxPrice, }) async { try { return (await feedService.getPlans( page: page, + query: query, + types: types, + duration: duration, + minPrice: minPrice, + maxPrice: maxPrice, )); } catch (e) { rethrow; diff --git a/lib/features/feed/services/feed_service.dart b/lib/features/feed/services/feed_service.dart index 7643fa49..585d8217 100644 --- a/lib/features/feed/services/feed_service.dart +++ b/lib/features/feed/services/feed_service.dart @@ -37,6 +37,11 @@ abstract class FeedService { @GET('/api/plans/index') Future getPlans({ @Query('page') required int page, + @Query('filters[name][\$startsWith]') String? query, + @Query('filters[type][\$contains]') String? types, + @Query('filters[duration][\$eq]') int? duration, + @Query('filters[min_price_of_options][\$between][0]') int? minPrice, + @Query('filters[min_price_of_options][\$between][1]') int? maxPrice, }); @GET('/api/blogPosts/{id}/comments/show') diff --git a/lib/features/feed/services/feed_service.g.dart b/lib/features/feed/services/feed_service.g.dart index 13c72ded..89279ebb 100644 --- a/lib/features/feed/services/feed_service.g.dart +++ b/lib/features/feed/services/feed_service.g.dart @@ -150,9 +150,24 @@ class _FeedService implements FeedService { } @override - Future getPlans({required int page}) async { + Future getPlans({ + required int page, + String? query, + String? types, + int? duration, + int? minPrice, + int? maxPrice, + }) async { final _extra = {}; - final queryParameters = {r'page': page}; + final queryParameters = { + r'page': page, + r'filters[name][$startsWith]': query, + r'filters[type][$contains]': types, + r'filters[duration][$eq]': duration, + r'filters[min_price_of_options][$between][0]': minPrice, + r'filters[min_price_of_options][$between][1]': maxPrice, + }; + queryParameters.removeWhere((k, v) => v == null); final _headers = {}; const Map? _data = null; final _options = _setStreamType(Options( diff --git a/lib/features/menu/presentation/pages/menu_screen.dart b/lib/features/menu/presentation/pages/menu_screen.dart index f4e27fa1..ad974e54 100644 --- a/lib/features/menu/presentation/pages/menu_screen.dart +++ b/lib/features/menu/presentation/pages/menu_screen.dart @@ -4,6 +4,8 @@ import 'package:eatvio_app/core/token_storage.dart'; import 'package:eatvio_app/core/utils/image_constant.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/features/my_diets/presentation/my_diets_screen.dart'; +import 'package:eatvio_app/features/my_plans/presentation/my_plans_screen.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:go_router/go_router.dart'; @@ -36,13 +38,13 @@ class MenuScreen extends StatelessWidget { _tile( leading: 'assets/icons/diets.svg', title: 'Мои диеты', - onTap: () => GoRouter.of(context).push('/diet-select-screen'), + onTap: () => GoRouter.of(context).push(MyDietsScreen.path), ), _divider(context), _tile( leading: ImageConstant.icnPlans, title: 'Мои планы', - onTap: () => GoRouter.of(context).push('/plan-select-screen'), + onTap: () => GoRouter.of(context).push(MyPlansScreen.path), ), _divider(context), _tile( diff --git a/lib/features/my_diets/presentation/cubit/my_diets_cubit.dart b/lib/features/my_diets/presentation/cubit/my_diets_cubit.dart new file mode 100644 index 00000000..7a998163 --- /dev/null +++ b/lib/features/my_diets/presentation/cubit/my_diets_cubit.dart @@ -0,0 +1,250 @@ +import 'package:easy_debounce/easy_debounce.dart'; +import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/features/my_diets/presentation/cubit/my_diets_state.dart'; +import 'package:eatvio_app/features/my_diets/repositories/my_diets_repository.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class MyDietsCubit extends Cubit { + final MyDietsRepository myDietsRepository; + + int currentDraftPage = 1; + int currentPersonalPage = 1; + int currentActivePage = 1; + late int lastDraftPage; + late int lastPersonalPage; + late int lastActivePage; + bool isLoadingDraft = false; + bool isLoadingPersonal = false; + bool isLoadingActive = false; + String? _activeQuery; + String? _personalQuery; + String? _draftQuery; + + static const List _sortingValues = [ + null, + 'chosen_diets_count:desc', + 'created_at:desc', + 'created_at', + 'name', + 'name:desc', + ]; + static const List _durationValues = [null, ...durationValues]; + static const List _types = [null, ...dietTypeValues]; + + MyDietsCubit({required this.myDietsRepository}) : super(MyDietsState()); + + void changeTypeFilter(int index) { + if (state.selectedStateIndex == 0) { + emit(state.copyWith(activeFilterState: state.activeFilterState.copyWith(typeIndex: index), error: null)); + EasyDebounce.debounce( + 'loadDiets', + Duration(seconds: 1), + () => loadDiets( + query: _activeQuery, + ), + ); + } else if (state.selectedStateIndex == 1) { + emit(state.copyWith(draftFilterState: state.draftFilterState.copyWith(typeIndex: index), error: null)); + EasyDebounce.debounce( + 'loadDiets', + Duration(seconds: 1), + () => loadDiets( + query: _draftQuery, + ), + ); + } else if (state.selectedStateIndex == 2) { + emit(state.copyWith(personalFilterState: state.personalFilterState.copyWith(typeIndex: index), error: null)); + EasyDebounce.debounce( + 'loadDiets', + Duration(seconds: 1), + () => loadDiets( + query: _personalQuery, + ), + ); + } + } + + void changeSortingType(int index) { + if (state.selectedStateIndex == 0) { + emit(state.copyWith(activeFilterState: state.activeFilterState.copyWith(sortingIndex: index), error: null)); + EasyDebounce.debounce( + 'loadDiets', + Duration(seconds: 1), + () => loadDiets( + query: _activeQuery, + ), + ); + } else if (state.selectedStateIndex == 1) { + emit(state.copyWith(draftFilterState: state.draftFilterState.copyWith(sortingIndex: index), error: null)); + EasyDebounce.debounce( + 'loadDiets', + Duration(seconds: 1), + () => loadDiets( + query: _draftQuery, + ), + ); + } else if (state.selectedStateIndex == 2) { + emit(state.copyWith(personalFilterState: state.personalFilterState.copyWith(sortingIndex: index), error: null)); + EasyDebounce.debounce( + 'loadDiets', + Duration(seconds: 1), + () => loadDiets( + query: _personalQuery, + ), + ); + } + } + + void changeDurationFilter(int index) { + if (state.selectedStateIndex == 0) { + emit(state.copyWith(activeFilterState: state.activeFilterState.copyWith(durationIndex: index), error: null)); + EasyDebounce.debounce( + 'loadDiets', + Duration(seconds: 1), + () => loadDiets( + query: _activeQuery, + ), + ); + } else if (state.selectedStateIndex == 1) { + emit(state.copyWith(draftFilterState: state.draftFilterState.copyWith(durationIndex: index), error: null)); + EasyDebounce.debounce( + 'loadDiets', + Duration(seconds: 1), + () => loadDiets( + query: _draftQuery, + ), + ); + } else if (state.selectedStateIndex == 2) { + emit(state.copyWith(personalFilterState: state.personalFilterState.copyWith(durationIndex: index), error: null)); + EasyDebounce.debounce( + 'loadDiets', + Duration(seconds: 1), + () => loadDiets( + query: _personalQuery, + ), + ); + } + } + + void changeSelectedStateIndex(int index) { + emit(state.copyWith(selectedStateIndex: index)); + } + + Future loadDiets({ + String? query, + }) async { + switch (state.selectedStateIndex) { + case 0: + await _loadActiveDiets(query: query); + break; + case 1: + await _loadDraftDiets(query: query); + break; + case 2: + await _loadPersonalDiets(query: query); + break; + } + } + + Future loadAllDiets() async { + emit(state.copyWith(isLoading: true)); + await _loadActiveDiets(); + await _loadDraftDiets(); + await _loadPersonalDiets(); + emit(state.copyWith(isLoading: false)); + } + + Future _loadActiveDiets({String? query}) async { + try { + _activeQuery = query; + final type = _types[state.activeFilterState.typeIndex]; + final duration = _durationValues[state.activeFilterState.durationIndex]; + final sorting = _sortingValues[state.activeFilterState.sortingIndex]; + final dietsPagination = await myDietsRepository.getDiets( + query: query, + types: type, + duration: duration, + sorting: sorting, + ); + lastActivePage = dietsPagination.meta.lastPage; + emit( + state.copyWith( + activeDiets: dietsPagination.data, + isLoading: isLoadingActive, + error: null, + ), + ); + } catch (e) { + emit( + state.copyWith(error: e.toString()), + ); + } + } + + Future _loadDraftDiets({String? query}) async { + try { + _draftQuery = query; + final type = _types[state.draftFilterState.typeIndex]; + final duration = _durationValues[state.draftFilterState.durationIndex]; + final sorting = _sortingValues[state.draftFilterState.sortingIndex]; + final dietsPagination = await myDietsRepository.getDiets( + query: query, + types: type, + duration: duration, + sorting: sorting, + ); + lastActivePage = dietsPagination.meta.lastPage; + emit( + state.copyWith( + draftDiets: dietsPagination.data, + isLoading: isLoadingDraft, + error: null, + ), + ); + } catch (e) { + emit( + state.copyWith(error: e.toString()), + ); + } + } + + Future _loadPersonalDiets({String? query}) async { + try { + _personalQuery = query; + final type = _types[state.personalFilterState.typeIndex]; + final duration = _durationValues[state.personalFilterState.durationIndex]; + final sorting = _sortingValues[state.personalFilterState.sortingIndex]; + final dietsPagination = await myDietsRepository.getDiets( + query: query, + types: type, + duration: duration, + sorting: sorting, + ); + lastActivePage = dietsPagination.meta.lastPage; + emit( + state.copyWith( + personalDiets: dietsPagination.data, + isLoading: isLoadingPersonal, + error: null, + ), + ); + } catch (e) { + emit( + state.copyWith(error: e.toString()), + ); + } + } + + Future deleteDiet(int dietId) async { + try { + await myDietsRepository.deleteDiet(dietId); + await loadAllDiets(); + return true; + } catch (e) { + emit( + state.copyWith(error: e.toString()), + ); + return false; + } + } +} diff --git a/lib/features/my_diets/presentation/cubit/my_diets_state.dart b/lib/features/my_diets/presentation/cubit/my_diets_state.dart new file mode 100644 index 00000000..36b0ec38 --- /dev/null +++ b/lib/features/my_diets/presentation/cubit/my_diets_state.dart @@ -0,0 +1,41 @@ +import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; +import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_state.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'my_diets_state.freezed.dart'; + +@freezed +class MyDietsState with _$MyDietsState { + factory MyDietsState({ + @Default([]) List activeDiets, + @Default([]) List draftDiets, + @Default([]) List personalDiets, + @Default(false) bool isLoading, + @Default(null) String? error, + @Default(0) int selectedStateIndex, + @Default( + const DietsFilterState( + durationIndex: 0, + sortingIndex: 0, + typeIndex: 0, + ), + ) + DietsFilterState activeFilterState, + @Default( + const DietsFilterState( + durationIndex: 0, + sortingIndex: 0, + typeIndex: 0, + ), + ) + DietsFilterState draftFilterState, + @Default( + const DietsFilterState( + durationIndex: 0, + sortingIndex: 0, + typeIndex: 0, + ), + ) + DietsFilterState personalFilterState, + }) = _MyDietsState; +} diff --git a/lib/features/my_diets/presentation/cubit/my_diets_state.freezed.dart b/lib/features/my_diets/presentation/cubit/my_diets_state.freezed.dart new file mode 100644 index 00000000..b15a73ae --- /dev/null +++ b/lib/features/my_diets/presentation/cubit/my_diets_state.freezed.dart @@ -0,0 +1,400 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'my_diets_state.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$MyDietsState { + List get activeDiets => throw _privateConstructorUsedError; + List get draftDiets => throw _privateConstructorUsedError; + List get personalDiets => throw _privateConstructorUsedError; + bool get isLoading => throw _privateConstructorUsedError; + String? get error => throw _privateConstructorUsedError; + int get selectedStateIndex => throw _privateConstructorUsedError; + DietsFilterState get activeFilterState => throw _privateConstructorUsedError; + DietsFilterState get draftFilterState => throw _privateConstructorUsedError; + DietsFilterState get personalFilterState => + throw _privateConstructorUsedError; + + /// Create a copy of MyDietsState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $MyDietsStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $MyDietsStateCopyWith<$Res> { + factory $MyDietsStateCopyWith( + MyDietsState value, $Res Function(MyDietsState) then) = + _$MyDietsStateCopyWithImpl<$Res, MyDietsState>; + @useResult + $Res call( + {List activeDiets, + List draftDiets, + List personalDiets, + bool isLoading, + String? error, + int selectedStateIndex, + DietsFilterState activeFilterState, + DietsFilterState draftFilterState, + DietsFilterState personalFilterState}); + + $DietsFilterStateCopyWith<$Res> get activeFilterState; + $DietsFilterStateCopyWith<$Res> get draftFilterState; + $DietsFilterStateCopyWith<$Res> get personalFilterState; +} + +/// @nodoc +class _$MyDietsStateCopyWithImpl<$Res, $Val extends MyDietsState> + implements $MyDietsStateCopyWith<$Res> { + _$MyDietsStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of MyDietsState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? activeDiets = null, + Object? draftDiets = null, + Object? personalDiets = null, + Object? isLoading = null, + Object? error = freezed, + Object? selectedStateIndex = null, + Object? activeFilterState = null, + Object? draftFilterState = null, + Object? personalFilterState = null, + }) { + return _then(_value.copyWith( + activeDiets: null == activeDiets + ? _value.activeDiets + : activeDiets // ignore: cast_nullable_to_non_nullable + as List, + draftDiets: null == draftDiets + ? _value.draftDiets + : draftDiets // ignore: cast_nullable_to_non_nullable + as List, + personalDiets: null == personalDiets + ? _value.personalDiets + : personalDiets // ignore: cast_nullable_to_non_nullable + as List, + isLoading: null == isLoading + ? _value.isLoading + : isLoading // ignore: cast_nullable_to_non_nullable + as bool, + error: freezed == error + ? _value.error + : error // ignore: cast_nullable_to_non_nullable + as String?, + selectedStateIndex: null == selectedStateIndex + ? _value.selectedStateIndex + : selectedStateIndex // ignore: cast_nullable_to_non_nullable + as int, + activeFilterState: null == activeFilterState + ? _value.activeFilterState + : activeFilterState // ignore: cast_nullable_to_non_nullable + as DietsFilterState, + draftFilterState: null == draftFilterState + ? _value.draftFilterState + : draftFilterState // ignore: cast_nullable_to_non_nullable + as DietsFilterState, + personalFilterState: null == personalFilterState + ? _value.personalFilterState + : personalFilterState // ignore: cast_nullable_to_non_nullable + as DietsFilterState, + ) as $Val); + } + + /// Create a copy of MyDietsState + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $DietsFilterStateCopyWith<$Res> get activeFilterState { + return $DietsFilterStateCopyWith<$Res>(_value.activeFilterState, (value) { + return _then(_value.copyWith(activeFilterState: value) as $Val); + }); + } + + /// Create a copy of MyDietsState + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $DietsFilterStateCopyWith<$Res> get draftFilterState { + return $DietsFilterStateCopyWith<$Res>(_value.draftFilterState, (value) { + return _then(_value.copyWith(draftFilterState: value) as $Val); + }); + } + + /// Create a copy of MyDietsState + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $DietsFilterStateCopyWith<$Res> get personalFilterState { + return $DietsFilterStateCopyWith<$Res>(_value.personalFilterState, (value) { + return _then(_value.copyWith(personalFilterState: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$MyDietsStateImplCopyWith<$Res> + implements $MyDietsStateCopyWith<$Res> { + factory _$$MyDietsStateImplCopyWith( + _$MyDietsStateImpl value, $Res Function(_$MyDietsStateImpl) then) = + __$$MyDietsStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {List activeDiets, + List draftDiets, + List personalDiets, + bool isLoading, + String? error, + int selectedStateIndex, + DietsFilterState activeFilterState, + DietsFilterState draftFilterState, + DietsFilterState personalFilterState}); + + @override + $DietsFilterStateCopyWith<$Res> get activeFilterState; + @override + $DietsFilterStateCopyWith<$Res> get draftFilterState; + @override + $DietsFilterStateCopyWith<$Res> get personalFilterState; +} + +/// @nodoc +class __$$MyDietsStateImplCopyWithImpl<$Res> + extends _$MyDietsStateCopyWithImpl<$Res, _$MyDietsStateImpl> + implements _$$MyDietsStateImplCopyWith<$Res> { + __$$MyDietsStateImplCopyWithImpl( + _$MyDietsStateImpl _value, $Res Function(_$MyDietsStateImpl) _then) + : super(_value, _then); + + /// Create a copy of MyDietsState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? activeDiets = null, + Object? draftDiets = null, + Object? personalDiets = null, + Object? isLoading = null, + Object? error = freezed, + Object? selectedStateIndex = null, + Object? activeFilterState = null, + Object? draftFilterState = null, + Object? personalFilterState = null, + }) { + return _then(_$MyDietsStateImpl( + activeDiets: null == activeDiets + ? _value._activeDiets + : activeDiets // ignore: cast_nullable_to_non_nullable + as List, + draftDiets: null == draftDiets + ? _value._draftDiets + : draftDiets // ignore: cast_nullable_to_non_nullable + as List, + personalDiets: null == personalDiets + ? _value._personalDiets + : personalDiets // ignore: cast_nullable_to_non_nullable + as List, + isLoading: null == isLoading + ? _value.isLoading + : isLoading // ignore: cast_nullable_to_non_nullable + as bool, + error: freezed == error + ? _value.error + : error // ignore: cast_nullable_to_non_nullable + as String?, + selectedStateIndex: null == selectedStateIndex + ? _value.selectedStateIndex + : selectedStateIndex // ignore: cast_nullable_to_non_nullable + as int, + activeFilterState: null == activeFilterState + ? _value.activeFilterState + : activeFilterState // ignore: cast_nullable_to_non_nullable + as DietsFilterState, + draftFilterState: null == draftFilterState + ? _value.draftFilterState + : draftFilterState // ignore: cast_nullable_to_non_nullable + as DietsFilterState, + personalFilterState: null == personalFilterState + ? _value.personalFilterState + : personalFilterState // ignore: cast_nullable_to_non_nullable + as DietsFilterState, + )); + } +} + +/// @nodoc + +class _$MyDietsStateImpl implements _MyDietsState { + _$MyDietsStateImpl( + {final List activeDiets = const [], + final List draftDiets = const [], + final List personalDiets = const [], + this.isLoading = false, + this.error = null, + this.selectedStateIndex = 0, + this.activeFilterState = const DietsFilterState( + durationIndex: 0, sortingIndex: 0, typeIndex: 0), + this.draftFilterState = const DietsFilterState( + durationIndex: 0, sortingIndex: 0, typeIndex: 0), + this.personalFilterState = const DietsFilterState( + durationIndex: 0, sortingIndex: 0, typeIndex: 0)}) + : _activeDiets = activeDiets, + _draftDiets = draftDiets, + _personalDiets = personalDiets; + + final List _activeDiets; + @override + @JsonKey() + List get activeDiets { + if (_activeDiets is EqualUnmodifiableListView) return _activeDiets; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_activeDiets); + } + + final List _draftDiets; + @override + @JsonKey() + List get draftDiets { + if (_draftDiets is EqualUnmodifiableListView) return _draftDiets; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_draftDiets); + } + + final List _personalDiets; + @override + @JsonKey() + List get personalDiets { + if (_personalDiets is EqualUnmodifiableListView) return _personalDiets; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_personalDiets); + } + + @override + @JsonKey() + final bool isLoading; + @override + @JsonKey() + final String? error; + @override + @JsonKey() + final int selectedStateIndex; + @override + @JsonKey() + final DietsFilterState activeFilterState; + @override + @JsonKey() + final DietsFilterState draftFilterState; + @override + @JsonKey() + final DietsFilterState personalFilterState; + + @override + String toString() { + return 'MyDietsState(activeDiets: $activeDiets, draftDiets: $draftDiets, personalDiets: $personalDiets, isLoading: $isLoading, error: $error, selectedStateIndex: $selectedStateIndex, activeFilterState: $activeFilterState, draftFilterState: $draftFilterState, personalFilterState: $personalFilterState)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$MyDietsStateImpl && + const DeepCollectionEquality() + .equals(other._activeDiets, _activeDiets) && + const DeepCollectionEquality() + .equals(other._draftDiets, _draftDiets) && + const DeepCollectionEquality() + .equals(other._personalDiets, _personalDiets) && + (identical(other.isLoading, isLoading) || + other.isLoading == isLoading) && + (identical(other.error, error) || other.error == error) && + (identical(other.selectedStateIndex, selectedStateIndex) || + other.selectedStateIndex == selectedStateIndex) && + (identical(other.activeFilterState, activeFilterState) || + other.activeFilterState == activeFilterState) && + (identical(other.draftFilterState, draftFilterState) || + other.draftFilterState == draftFilterState) && + (identical(other.personalFilterState, personalFilterState) || + other.personalFilterState == personalFilterState)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + const DeepCollectionEquality().hash(_activeDiets), + const DeepCollectionEquality().hash(_draftDiets), + const DeepCollectionEquality().hash(_personalDiets), + isLoading, + error, + selectedStateIndex, + activeFilterState, + draftFilterState, + personalFilterState); + + /// Create a copy of MyDietsState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$MyDietsStateImplCopyWith<_$MyDietsStateImpl> get copyWith => + __$$MyDietsStateImplCopyWithImpl<_$MyDietsStateImpl>(this, _$identity); +} + +abstract class _MyDietsState implements MyDietsState { + factory _MyDietsState( + {final List activeDiets, + final List draftDiets, + final List personalDiets, + final bool isLoading, + final String? error, + final int selectedStateIndex, + final DietsFilterState activeFilterState, + final DietsFilterState draftFilterState, + final DietsFilterState personalFilterState}) = _$MyDietsStateImpl; + + @override + List get activeDiets; + @override + List get draftDiets; + @override + List get personalDiets; + @override + bool get isLoading; + @override + String? get error; + @override + int get selectedStateIndex; + @override + DietsFilterState get activeFilterState; + @override + DietsFilterState get draftFilterState; + @override + DietsFilterState get personalFilterState; + + /// Create a copy of MyDietsState + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$MyDietsStateImplCopyWith<_$MyDietsStateImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/features/my_diets/presentation/my_diets_screen.dart b/lib/features/my_diets/presentation/my_diets_screen.dart new file mode 100644 index 00000000..65ac8441 --- /dev/null +++ b/lib/features/my_diets/presentation/my_diets_screen.dart @@ -0,0 +1,194 @@ +import 'package:easy_debounce/easy_debounce.dart'; +import 'package:eatvio_app/core/locator_service.dart'; +import 'package:eatvio_app/core/theme/custom_text_style.dart'; +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/core/widgets/add_item_button.dart'; +import 'package:eatvio_app/core/widgets/custom_search_bar.dart'; +import 'package:eatvio_app/core/widgets/loader.dart'; +import 'package:eatvio_app/features/create_diet/presentation/create_diet_screen.dart'; +import 'package:eatvio_app/features/feed/presentation/widgets/diet_filter_bottom_sheet.dart'; +import 'package:eatvio_app/features/feed/presentation/widgets/filter_badge.dart'; +import 'package:eatvio_app/features/my_diets/presentation/cubit/my_diets_cubit.dart'; +import 'package:eatvio_app/features/my_diets/presentation/cubit/my_diets_state.dart'; +import 'package:eatvio_app/features/my_diets/presentation/widgets/my_diet_widget.dart'; +import 'package:eatvio_app/features/my_diets/presentation/widgets/my_diets_tab.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; + +class MyDietsScreen extends StatelessWidget { + const MyDietsScreen({super.key}); + static const name = 'myDietsScreen'; + static const path = '/my-diets-screen'; + @override + Widget build(BuildContext context) { + return BlocProvider(create: (context) => sl()..loadAllDiets(), child: MyDietsScreenContent()); + } +} + +class MyDietsScreenContent extends StatelessWidget { + MyDietsScreenContent({super.key}); + + final activeDietsScrollController = ScrollController(); + final draftDietsScrollController = ScrollController(); + final personalDietsScrollController = ScrollController(); + + final activeDietsSearchController = SearchController(); + final draftDietsSearchController = SearchController(); + final personalDietsSearchController = SearchController(); + + @override + Widget build(BuildContext context) { + final s = S.of(context); + return Scaffold( + backgroundColor: appTheme.gray100, + appBar: AppBar( + centerTitle: true, + title: Text( + s.my_diets, + maxLines: 2, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + softWrap: true, + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), + ), + ), + body: BlocBuilder( + buildWhen: (previous, current) => previous.selectedStateIndex != current.selectedStateIndex, + builder: (context, state) { + return NestedScrollView( + controller: switch (state.selectedStateIndex) { + 1 => draftDietsScrollController, + 2 => personalDietsScrollController, + _ => activeDietsScrollController + }, + headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { + return [ + SliverAppBar( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(bottom: Radius.circular(12)), + ), + backgroundColor: appTheme.gray100, + foregroundColor: appTheme.gray100, + automaticallyImplyLeading: false, + stretchTriggerOffset: 1, + floating: true, + primary: false, + snap: true, + scrolledUnderElevation: 0, + bottom: PreferredSize( + preferredSize: Size( + MediaQuery.sizeOf(context).width, + 64, + ), + child: Padding( + padding: const EdgeInsets.only(bottom: 0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(height: 8), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: MyDietsTab(), + ), + SizedBox(height: 8), + Row( + children: [ + SizedBox(width: 4), + Expanded( + child: CustomSearchBar( + enableIcon: false, + backgroundColor: appTheme.whiteA700, + borderRadius: 100, + searchController: switch (state.selectedStateIndex) { + 1 => draftDietsSearchController, + 2 => personalDietsSearchController, + _ => activeDietsSearchController + }, + onChanged: (query) { + EasyDebounce.debounce( + 'searchDiets', + Duration(milliseconds: 500), + () { + context.read().loadDiets(query: query == '' ? null : query); + }, + ); + }, + ), + ), + SizedBox(width: 12), + AddItemButton( + onTap: () => GoRouter.of(context).pushNamed(CreateDietScreen.name), + ), + SizedBox(width: 4), + ], + ), + SizedBox(height: 16), + ], + ), + ), + ), + ), + ]; + }, + body: RefreshIndicator( + onRefresh: () => context.read().loadDiets(), + child: BlocBuilder( + builder: (BuildContext context, MyDietsState state) { + if (state.isLoading) { + return Scaffold( + backgroundColor: Colors.grey.shade200, + body: const Loader(), + ); + } else if (state.error != null) { + return Scaffold( + backgroundColor: Colors.grey.shade200, + body: Padding( + padding: const EdgeInsets.all(16), + child: Center( + child: Text(state.error ?? ''), + ), + ), + ); + } else { + final diets = switch (state.selectedStateIndex) { + 1 => state.draftDiets, + 2 => state.personalDiets, + _ => state.activeDiets, + }; + return Stack( + children: [ + SizedBox.expand(), + ListView.separated( + shrinkWrap: true, + physics: BouncingScrollPhysics(), + itemBuilder: (context, index) { + return MyDietWidget(diet: diets[index]); + }, + separatorBuilder: (context, index) { + return SizedBox(height: 20); + }, + itemCount: diets.length, + ), + Positioned( + right: 0, + bottom: 74, + child: FilterBadge( + filterBottomSheet: DietFilterBottomSheet( + myDietsCubit: context.read(), + ), + ), + ), + ], + ); + } + }, + ), + ), + ); + }, + ), + ); + } +} diff --git a/lib/features/my_diets/presentation/widgets/my_diet_widget.dart b/lib/features/my_diets/presentation/widgets/my_diet_widget.dart new file mode 100644 index 00000000..41655991 --- /dev/null +++ b/lib/features/my_diets/presentation/widgets/my_diet_widget.dart @@ -0,0 +1,252 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/theme/custom_text_style.dart'; +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/core/widgets/bottom_sheet_handler_bar.dart'; +import 'package:eatvio_app/core/widgets/bottom_sheet_with_blur.dart'; +import 'package:eatvio_app/core/widgets/custom_outlined_button.dart'; +import 'package:eatvio_app/core/widgets/info_row.dart'; +import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; +import 'package:eatvio_app/features/my_diets/presentation/cubit/my_diets_cubit.dart'; +import 'package:eatvio_app/features/my_diets/presentation/cubit/my_diets_state.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_html/flutter_html.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:go_router/go_router.dart'; +import 'package:share_plus/share_plus.dart'; + +class MyDietWidget extends StatefulWidget { + final Diet diet; + const MyDietWidget({ + super.key, + required this.diet, + }); + + @override + State createState() => _MyDietWidgetState(); +} + +class _MyDietWidgetState extends State { + @override + Widget build(BuildContext context) { + final s = S.of(context); + return DecoratedBox( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: Colors.white, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Stack( + children: [ + InkWell( + onTap: () => GoRouter.of(context).pushNamed('aboutDietScreen', extra: {'diet': widget.diet, 'navigateToComments': false}), + child: ClipRRect( + borderRadius: BorderRadius.vertical(top: Radius.circular(12)), + child: CachedNetworkImage( + imageUrl: baseUrl + '/storage/' + widget.diet.image, + fit: BoxFit.fitWidth, + width: MediaQuery.of(context).size.width, + ), + ), + ), + Positioned( + bottom: 16, + child: SizedBox( + width: MediaQuery.sizeOf(context).width, + child: InfoRow( + diet: widget.diet, + itemsBackgroundColor: appTheme.whiteA700, + disableDescriptionOfTypes: true, + ), + ), + ), + ], + ), + SizedBox(height: 12), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Text( + widget.diet.name, + style: TextStyles.textMSemibold.copyWith(color: appTheme.black), + ), + ), + if (widget.diet.details.isNotEmpty) ...[ + SizedBox( + height: 12, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Html( + data: widget.diet.details.length > 200 ? widget.diet.details.substring(0, 200) + '...' : widget.diet.details, + style: { + "*": Style( + margin: Margins.zero, + color: appTheme.black, + fontSize: FontSize(TextStyles.textSMedium.fontSize ?? 14), + fontWeight: TextStyles.textSMedium.fontWeight ?? FontWeight.w400, + ), + }, + ), + ), + ], + ], + ), + SizedBox(height: 20), + BlocBuilder( + builder: (context, state) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + children: [ + Row( + children: [ + Expanded( + child: CustomOutlinedButton( + text: switch (state.selectedStateIndex) { + 1 => s.publish, + 2 => s.toOffer, + _ => s.edit, + }, + height: 52, + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), + buttonStyle: ButtonStyle( + shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))), + backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), + side: WidgetStatePropertyAll(BorderSide.none), + ), + ), + ), + if (state.selectedStateIndex == 0) ...[ + SizedBox(width: 6), + CustomOutlinedButton( + onPressed: () async { + await Share.shareUri( + Uri.parse( + baseUrl + '/ru/diet/' + widget.diet.id.toString(), + ), + ); + }, + height: 52, + width: 52, + leftIcon: SvgPicture.asset( + 'assets/icons/share2.svg', + width: 20, + height: 20, + colorFilter: ColorFilter.mode(appTheme.whiteA700, BlendMode.srcIn), + ), + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.red), + buttonStyle: ButtonStyle( + shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))), + backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), + side: WidgetStatePropertyAll(BorderSide.none), + ), + text: '', + ), + ], + ], + ), + SizedBox(height: 8), + Row( + children: [ + Expanded( + child: CustomOutlinedButton( + text: switch (state.selectedStateIndex) { + 0 => s.removeFromActive, + _ => s.edit, + }, + height: 52, + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.black), + buttonStyle: ButtonStyle( + shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))), + backgroundColor: WidgetStatePropertyAll(appTheme.lightGray0), + side: WidgetStatePropertyAll(BorderSide.none), + ), + ), + ), + SizedBox(width: 6), + CustomOutlinedButton( + onPressed: () async { + final result = await context.read().deleteDiet(widget.diet.id); + if (result) + Future.delayed(Duration(seconds: 3), () { + showBottomSheetWithBlur( + context: context, + child: Column( + children: [ + SizedBox( + height: 20, + ), + BottomSheetHandlerBar(), + SizedBox( + height: 20, + ), + Text( + '${widget.diet.name}', + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), + ), + Text( + s.deleted, + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), + ), + SizedBox( + height: 20, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: CustomOutlinedButton( + onPressed: () { + context.pop(); + }, + text: 'Хорошо', + height: 52, + buttonTextStyle: TextStyles.textSSemibold.copyWith(color: appTheme.whiteA700), + buttonStyle: ButtonStyle( + side: WidgetStatePropertyAll(BorderSide.none), + backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), + ), + ), + ), + SizedBox( + height: 20, + ), + ], + ), + ); + }); + }, + height: 52, + width: 52, + leftIcon: SvgPicture.asset( + 'assets/icons/trash_can.svg', + width: 20, + height: 20, + ), + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.red), + buttonStyle: ButtonStyle( + shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))), + backgroundColor: WidgetStatePropertyAll(appTheme.lightGray0), + side: WidgetStatePropertyAll(BorderSide.none), + ), + text: '', + ), + ], + ), + SizedBox(height: 8), + ], + ), + ); + }, + ), + ], + ), + ); + } +} diff --git a/lib/features/my_diets/presentation/widgets/my_diets_tab.dart b/lib/features/my_diets/presentation/widgets/my_diets_tab.dart new file mode 100644 index 00000000..a653441c --- /dev/null +++ b/lib/features/my_diets/presentation/widgets/my_diets_tab.dart @@ -0,0 +1,74 @@ +import 'package:eatvio_app/core/theme/custom_text_style.dart'; +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/core/widgets/animated_option_button.dart'; +import 'package:eatvio_app/features/my_diets/presentation/cubit/my_diets_cubit.dart'; +import 'package:eatvio_app/features/my_diets/presentation/cubit/my_diets_state.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class MyDietsTab extends StatefulWidget { + const MyDietsTab({super.key, this.additionalOnTap}); + final void Function()? additionalOnTap; + @override + State createState() => _MyDietsTabState(); +} + +class _MyDietsTabState extends State { + final List tabTitles = []; + @override + void didChangeDependencies() { + final s = S.of(context); + if (tabTitles.isEmpty) { + tabTitles.add(s.published); + tabTitles.add(s.drafted); + tabTitles.add(s.personal); + } + super.didChangeDependencies(); + } + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return Align( + alignment: Alignment.centerLeft, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + Container( + width: 484, + height: 48, + decoration: BoxDecoration( + color: appTheme.whiteA700, + borderRadius: BorderRadius.circular(100), + ), + padding: const EdgeInsets.all(4), + child: Row( + children: [ + ...List.generate( + 3, + (index) => Expanded( + child: AnimatedOptionButton( + textStyle: TextStyles.textSMedium, + isSelected: state.selectedStateIndex == index, + text: tabTitles[index], + onTap: () { + widget.additionalOnTap?.call(); + context.read().changeSelectedStateIndex(index); + }, + ), + ), + ), + ], + ), + ), + ], + ), + ), + ); + }, + ); + } +} diff --git a/lib/features/my_diets/repositories/my_diets_repository.dart b/lib/features/my_diets/repositories/my_diets_repository.dart new file mode 100644 index 00000000..8c15a546 --- /dev/null +++ b/lib/features/my_diets/repositories/my_diets_repository.dart @@ -0,0 +1,36 @@ +import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; +import 'package:eatvio_app/features/my_diets/services/my_diets_service.dart'; + +class MyDietsRepository { + final MyDietsService myDietsService; + + MyDietsRepository({required this.myDietsService}); + + Future deleteDiet(int dietId) async { + try { + await myDietsService.deleteDiet(dietId: dietId); + } catch (e) { + rethrow; + } + } + + Future getDiets({ + int page = 1, + String? query, + String? types, + int? duration, + String? sorting, + }) async { + try { + return (await myDietsService.getDiets( + page: page, + query: query, + types: types, + duration: duration, + sorting: sorting, + )); + } catch (e) { + rethrow; + } + } +} diff --git a/lib/features/my_diets/services/my_diets_service.dart b/lib/features/my_diets/services/my_diets_service.dart new file mode 100644 index 00000000..e2bba8a0 --- /dev/null +++ b/lib/features/my_diets/services/my_diets_service.dart @@ -0,0 +1,27 @@ +import 'package:dio/dio.dart'; +import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; +import 'package:retrofit/retrofit.dart'; + +part 'my_diets_service.g.dart'; + +@RestApi() +abstract class MyDietsService { + + factory MyDietsService(Dio dio, {String baseUrl}) = _MyDietsService; + + @GET('/api/diets/index') + Future getDiets({ + @Query('page') required int page, + @Query('filters[name][\$startsWith]') String? query, + @Query('filters[type][\$contains]') String? types, + @Query('filters[duration][\$eq]') int? duration, + @Query('sort') String? sorting, + }); + + @POST('/api/diets/delete/{dietId}') + Future deleteDiet({ + @Path() required int dietId, + }); + + +} \ No newline at end of file diff --git a/lib/features/my_diets/services/my_diets_service.g.dart b/lib/features/my_diets/services/my_diets_service.g.dart new file mode 100644 index 00000000..fec66099 --- /dev/null +++ b/lib/features/my_diets/services/my_diets_service.g.dart @@ -0,0 +1,124 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'my_diets_service.dart'; + +// ************************************************************************** +// RetrofitGenerator +// ************************************************************************** + +// ignore_for_file: unnecessary_brace_in_string_interps,no_leading_underscores_for_local_identifiers,unused_element,unnecessary_string_interpolations + +class _MyDietsService implements MyDietsService { + _MyDietsService( + this._dio, { + this.baseUrl, + this.errorLogger, + }); + + final Dio _dio; + + String? baseUrl; + + final ParseErrorLogger? errorLogger; + + @override + Future getDiets({ + required int page, + String? query, + String? types, + int? duration, + String? sorting, + }) async { + final _extra = {}; + final queryParameters = { + r'page': page, + r'filters[name][$startsWith]': query, + r'filters[type][$contains]': types, + r'filters[duration][$eq]': duration, + r'sort': sorting, + }; + queryParameters.removeWhere((k, v) => v == null); + final _headers = {}; + const Map? _data = null; + final _options = _setStreamType(Options( + method: 'GET', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/api/diets/index', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + ))); + final _result = await _dio.fetch>(_options); + late DietsPagination _value; + try { + _value = DietsPagination.fromJson(_result.data!); + } on Object catch (e, s) { + errorLogger?.logError(e, s, _options); + rethrow; + } + return _value; + } + + @override + Future deleteDiet({required int dietId}) async { + final _extra = {}; + final queryParameters = {}; + final _headers = {}; + const Map? _data = null; + final _options = _setStreamType(Options( + method: 'POST', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/api/diets/delete/${dietId}', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + ))); + await _dio.fetch(_options); + } + + RequestOptions _setStreamType(RequestOptions requestOptions) { + if (T != dynamic && + !(requestOptions.responseType == ResponseType.bytes || + requestOptions.responseType == ResponseType.stream)) { + if (T == String) { + requestOptions.responseType = ResponseType.plain; + } else { + requestOptions.responseType = ResponseType.json; + } + } + return requestOptions; + } + + String _combineBaseUrls( + String dioBaseUrl, + String? baseUrl, + ) { + if (baseUrl == null || baseUrl.trim().isEmpty) { + return dioBaseUrl; + } + + final url = Uri.parse(baseUrl); + + if (url.isAbsolute) { + return url.toString(); + } + + return Uri.parse(dioBaseUrl).resolveUri(url).toString(); + } +} diff --git a/lib/features/my_plans/presentation/cubit/my_plans_cubit.dart b/lib/features/my_plans/presentation/cubit/my_plans_cubit.dart new file mode 100644 index 00000000..eb61d560 --- /dev/null +++ b/lib/features/my_plans/presentation/cubit/my_plans_cubit.dart @@ -0,0 +1,207 @@ +import 'package:easy_debounce/easy_debounce.dart'; +import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_state.dart'; +import 'package:eatvio_app/features/my_plans/repositories/my_diets_repository.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class MyPlansCubit extends Cubit { + final MyPlansRepository myPlansRepository; + + int currentDraftPage = 1; + int currentActivePage = 1; + late int lastDraftPage; + late int lastPersonalPage; + late int lastActivePage; + bool isLoadingDraft = false; + bool isLoadingActive = false; + String? _activeQuery; + String? _draftQuery; + + static const List _durationValues = [null, ...durationValues]; + static const List _types = [null, ...planTypeValues]; + + MyPlansCubit({required this.myPlansRepository}) : super(MyPlansState()); + + void changePriceFilter(int minPrice, int maxPrice, {bool disableDebounce = false}) { + if (state.selectedStateIndex == 0) { + emit(state.copyWith(activeFilterState: state.activeFilterState.copyWith(priceFilter: (minPrice, maxPrice)), error: null)); + EasyDebounce.debounce( + 'loadPlans', + Duration(seconds: disableDebounce ? 0 : 1), + () => loadPlans( + query: _activeQuery, + ), + ); + } else if (state.selectedStateIndex == 1) { + emit(state.copyWith(draftFilterState: state.draftFilterState.copyWith(priceFilter: (minPrice, maxPrice)), error: null)); + EasyDebounce.debounce( + 'loadPlans', + Duration(seconds: disableDebounce ? 0 : 1), + () => loadPlans( + query: _draftQuery, + ), + ); + } + } + + void changeTypeFilter(int index) { + if (state.selectedStateIndex == 0) { + emit(state.copyWith(activeFilterState: state.activeFilterState.copyWith(typeIndex: index), error: null)); + EasyDebounce.debounce( + 'loadPlans', + Duration(seconds: 1), + () => loadPlans( + query: _activeQuery, + ), + ); + } else if (state.selectedStateIndex == 1) { + emit(state.copyWith(draftFilterState: state.draftFilterState.copyWith(typeIndex: index), error: null)); + EasyDebounce.debounce( + 'loadPlans', + Duration(seconds: 1), + () => loadPlans( + query: _draftQuery, + ), + ); + } + } + + void changeDurationFilter(int index) { + if (state.selectedStateIndex == 0) { + emit(state.copyWith(activeFilterState: state.activeFilterState.copyWith(durationIndex: index), error: null)); + EasyDebounce.debounce( + 'loadPlans', + Duration(seconds: 1), + () => loadPlans( + query: _activeQuery, + ), + ); + } else if (state.selectedStateIndex == 1) { + emit(state.copyWith(draftFilterState: state.draftFilterState.copyWith(durationIndex: index), error: null)); + EasyDebounce.debounce( + 'loadPlans', + Duration(seconds: 1), + () => loadPlans( + query: _draftQuery, + ), + ); + } + } + + void changeSelectedStateIndex(int index) { + emit(state.copyWith(selectedStateIndex: index)); + } + + Future loadPlans({ + String? query, + }) async { + switch (state.selectedStateIndex) { + case 0: + await _loadActivePlans(query: query); + break; + case 1: + await _loadDraftPlans(query: query); + break; + } + } + + Future loadAllPlans() async { + emit(state.copyWith(isLoading: true)); + await _loadActivePlans(); + await _loadDraftPlans(); + emit(state.copyWith(isLoading: false)); + } + + Future _loadActivePlans({String? query}) async { + try { + _activeQuery = query; + final type = _types[state.activeFilterState.typeIndex]; + final duration = _durationValues[state.activeFilterState.durationIndex]; + final minPrice = state.activeFilterState.priceFilter.$1; + final maxPrice = state.activeFilterState.priceFilter.$2; + final plansPagination = await myPlansRepository.getPlans( + query: query, + duration: duration, + maxPrice: maxPrice, + minPrice: minPrice, + types: type, + ); + lastActivePage = plansPagination.meta.lastPage; + final minPlanPrice = plansPagination.meta.totalMinPrice ?? 0; + final maxPlanPrice = plansPagination.meta.totalMaxPrice ?? 0; + if (state.activeFilterState.priceFilter.$1 == null || state.activeFilterState.priceFilter.$2 == null) { + emit( + state.copyWith( + activeFilterState: state.activeFilterState.copyWith(priceFilter: (minPlanPrice, maxPlanPrice)), + ), + ); + } + emit( + state.copyWith( + activePlans: plansPagination.data, + isLoading: isLoadingActive, + error: null, + minActivePlanPrice: minPlanPrice, + maxActivePlanPrice: maxPlanPrice, + ), + ); + } catch (e) { + emit( + state.copyWith(error: e.toString()), + ); + } + } + + Future _loadDraftPlans({String? query}) async { + try { + _draftQuery = query; + final type = _types[state.draftFilterState.typeIndex]; + final duration = _durationValues[state.draftFilterState.durationIndex]; + final minPrice = state.draftFilterState.priceFilter.$1; + final maxPrice = state.draftFilterState.priceFilter.$2; + final plansPagination = await myPlansRepository.getPlans( + query: query, + duration: duration, + maxPrice: maxPrice, + minPrice: minPrice, + types: type, + ); + lastDraftPage = plansPagination.meta.lastPage; + final minPlanPrice = plansPagination.meta.totalMinPrice ?? 0; + final maxPlanPrice = plansPagination.meta.totalMaxPrice ?? 0; + if (state.draftFilterState.priceFilter.$1 == null || state.draftFilterState.priceFilter.$2 == null) { + emit( + state.copyWith( + draftFilterState: state.draftFilterState.copyWith(priceFilter: (minPlanPrice, maxPlanPrice)), + ), + ); + } + emit( + state.copyWith( + draftPlans: plansPagination.data, + isLoading: isLoadingDraft, + error: null, + minDraftPlanPrice: minPlanPrice, + maxDraftPlanPrice: maxPlanPrice, + ), + ); + } catch (e) { + emit( + state.copyWith(error: e.toString()), + ); + } + } + + Future deletePlan(String slug) async { + try { + await myPlansRepository.deletePlan(slug); + await loadAllPlans(); + return true; + } catch (e) { + emit( + state.copyWith(error: e.toString()), + ); + return false; + } + } +} diff --git a/lib/features/my_plans/presentation/cubit/my_plans_state.dart b/lib/features/my_plans/presentation/cubit/my_plans_state.dart new file mode 100644 index 00000000..7434a8a9 --- /dev/null +++ b/lib/features/my_plans/presentation/cubit/my_plans_state.dart @@ -0,0 +1,36 @@ +import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart'; +import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_state.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'my_plans_state.freezed.dart'; + +@freezed +class MyPlansState with _$MyPlansState { + factory MyPlansState({ + @Default([]) List activePlans, + @Default([]) List draftPlans, + @Default(false) bool isLoading, + @Default(null) String? error, + @Default(0) int selectedStateIndex, + @Default(0) int minActivePlanPrice, + @Default(0) int maxActivePlanPrice, + @Default(0) int minDraftPlanPrice, + @Default(0) int maxDraftPlanPrice, + @Default( + const PlansFilterState( + priceFilter: (null, null), + durationIndex: 0, + typeIndex: 0, + ), + ) + PlansFilterState activeFilterState, + @Default( + const PlansFilterState( + priceFilter: (null, null), + durationIndex: 0, + typeIndex: 0, + ), + ) + PlansFilterState draftFilterState, + }) = _MyPlansState; +} diff --git a/lib/features/my_plans/presentation/cubit/my_plans_state.freezed.dart b/lib/features/my_plans/presentation/cubit/my_plans_state.freezed.dart new file mode 100644 index 00000000..ea5d960b --- /dev/null +++ b/lib/features/my_plans/presentation/cubit/my_plans_state.freezed.dart @@ -0,0 +1,424 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'my_plans_state.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$MyPlansState { + List get activePlans => throw _privateConstructorUsedError; + List get draftPlans => throw _privateConstructorUsedError; + bool get isLoading => throw _privateConstructorUsedError; + String? get error => throw _privateConstructorUsedError; + int get selectedStateIndex => throw _privateConstructorUsedError; + int get minActivePlanPrice => throw _privateConstructorUsedError; + int get maxActivePlanPrice => throw _privateConstructorUsedError; + int get minDraftPlanPrice => throw _privateConstructorUsedError; + int get maxDraftPlanPrice => throw _privateConstructorUsedError; + PlansFilterState get activeFilterState => throw _privateConstructorUsedError; + PlansFilterState get draftFilterState => throw _privateConstructorUsedError; + + /// Create a copy of MyPlansState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $MyPlansStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $MyPlansStateCopyWith<$Res> { + factory $MyPlansStateCopyWith( + MyPlansState value, $Res Function(MyPlansState) then) = + _$MyPlansStateCopyWithImpl<$Res, MyPlansState>; + @useResult + $Res call( + {List activePlans, + List draftPlans, + bool isLoading, + String? error, + int selectedStateIndex, + int minActivePlanPrice, + int maxActivePlanPrice, + int minDraftPlanPrice, + int maxDraftPlanPrice, + PlansFilterState activeFilterState, + PlansFilterState draftFilterState}); + + $PlansFilterStateCopyWith<$Res> get activeFilterState; + $PlansFilterStateCopyWith<$Res> get draftFilterState; +} + +/// @nodoc +class _$MyPlansStateCopyWithImpl<$Res, $Val extends MyPlansState> + implements $MyPlansStateCopyWith<$Res> { + _$MyPlansStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of MyPlansState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? activePlans = null, + Object? draftPlans = null, + Object? isLoading = null, + Object? error = freezed, + Object? selectedStateIndex = null, + Object? minActivePlanPrice = null, + Object? maxActivePlanPrice = null, + Object? minDraftPlanPrice = null, + Object? maxDraftPlanPrice = null, + Object? activeFilterState = null, + Object? draftFilterState = null, + }) { + return _then(_value.copyWith( + activePlans: null == activePlans + ? _value.activePlans + : activePlans // ignore: cast_nullable_to_non_nullable + as List, + draftPlans: null == draftPlans + ? _value.draftPlans + : draftPlans // ignore: cast_nullable_to_non_nullable + as List, + isLoading: null == isLoading + ? _value.isLoading + : isLoading // ignore: cast_nullable_to_non_nullable + as bool, + error: freezed == error + ? _value.error + : error // ignore: cast_nullable_to_non_nullable + as String?, + selectedStateIndex: null == selectedStateIndex + ? _value.selectedStateIndex + : selectedStateIndex // ignore: cast_nullable_to_non_nullable + as int, + minActivePlanPrice: null == minActivePlanPrice + ? _value.minActivePlanPrice + : minActivePlanPrice // ignore: cast_nullable_to_non_nullable + as int, + maxActivePlanPrice: null == maxActivePlanPrice + ? _value.maxActivePlanPrice + : maxActivePlanPrice // ignore: cast_nullable_to_non_nullable + as int, + minDraftPlanPrice: null == minDraftPlanPrice + ? _value.minDraftPlanPrice + : minDraftPlanPrice // ignore: cast_nullable_to_non_nullable + as int, + maxDraftPlanPrice: null == maxDraftPlanPrice + ? _value.maxDraftPlanPrice + : maxDraftPlanPrice // ignore: cast_nullable_to_non_nullable + as int, + activeFilterState: null == activeFilterState + ? _value.activeFilterState + : activeFilterState // ignore: cast_nullable_to_non_nullable + as PlansFilterState, + draftFilterState: null == draftFilterState + ? _value.draftFilterState + : draftFilterState // ignore: cast_nullable_to_non_nullable + as PlansFilterState, + ) as $Val); + } + + /// Create a copy of MyPlansState + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $PlansFilterStateCopyWith<$Res> get activeFilterState { + return $PlansFilterStateCopyWith<$Res>(_value.activeFilterState, (value) { + return _then(_value.copyWith(activeFilterState: value) as $Val); + }); + } + + /// Create a copy of MyPlansState + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $PlansFilterStateCopyWith<$Res> get draftFilterState { + return $PlansFilterStateCopyWith<$Res>(_value.draftFilterState, (value) { + return _then(_value.copyWith(draftFilterState: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$MyPlansStateImplCopyWith<$Res> + implements $MyPlansStateCopyWith<$Res> { + factory _$$MyPlansStateImplCopyWith( + _$MyPlansStateImpl value, $Res Function(_$MyPlansStateImpl) then) = + __$$MyPlansStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {List activePlans, + List draftPlans, + bool isLoading, + String? error, + int selectedStateIndex, + int minActivePlanPrice, + int maxActivePlanPrice, + int minDraftPlanPrice, + int maxDraftPlanPrice, + PlansFilterState activeFilterState, + PlansFilterState draftFilterState}); + + @override + $PlansFilterStateCopyWith<$Res> get activeFilterState; + @override + $PlansFilterStateCopyWith<$Res> get draftFilterState; +} + +/// @nodoc +class __$$MyPlansStateImplCopyWithImpl<$Res> + extends _$MyPlansStateCopyWithImpl<$Res, _$MyPlansStateImpl> + implements _$$MyPlansStateImplCopyWith<$Res> { + __$$MyPlansStateImplCopyWithImpl( + _$MyPlansStateImpl _value, $Res Function(_$MyPlansStateImpl) _then) + : super(_value, _then); + + /// Create a copy of MyPlansState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? activePlans = null, + Object? draftPlans = null, + Object? isLoading = null, + Object? error = freezed, + Object? selectedStateIndex = null, + Object? minActivePlanPrice = null, + Object? maxActivePlanPrice = null, + Object? minDraftPlanPrice = null, + Object? maxDraftPlanPrice = null, + Object? activeFilterState = null, + Object? draftFilterState = null, + }) { + return _then(_$MyPlansStateImpl( + activePlans: null == activePlans + ? _value._activePlans + : activePlans // ignore: cast_nullable_to_non_nullable + as List, + draftPlans: null == draftPlans + ? _value._draftPlans + : draftPlans // ignore: cast_nullable_to_non_nullable + as List, + isLoading: null == isLoading + ? _value.isLoading + : isLoading // ignore: cast_nullable_to_non_nullable + as bool, + error: freezed == error + ? _value.error + : error // ignore: cast_nullable_to_non_nullable + as String?, + selectedStateIndex: null == selectedStateIndex + ? _value.selectedStateIndex + : selectedStateIndex // ignore: cast_nullable_to_non_nullable + as int, + minActivePlanPrice: null == minActivePlanPrice + ? _value.minActivePlanPrice + : minActivePlanPrice // ignore: cast_nullable_to_non_nullable + as int, + maxActivePlanPrice: null == maxActivePlanPrice + ? _value.maxActivePlanPrice + : maxActivePlanPrice // ignore: cast_nullable_to_non_nullable + as int, + minDraftPlanPrice: null == minDraftPlanPrice + ? _value.minDraftPlanPrice + : minDraftPlanPrice // ignore: cast_nullable_to_non_nullable + as int, + maxDraftPlanPrice: null == maxDraftPlanPrice + ? _value.maxDraftPlanPrice + : maxDraftPlanPrice // ignore: cast_nullable_to_non_nullable + as int, + activeFilterState: null == activeFilterState + ? _value.activeFilterState + : activeFilterState // ignore: cast_nullable_to_non_nullable + as PlansFilterState, + draftFilterState: null == draftFilterState + ? _value.draftFilterState + : draftFilterState // ignore: cast_nullable_to_non_nullable + as PlansFilterState, + )); + } +} + +/// @nodoc + +class _$MyPlansStateImpl implements _MyPlansState { + _$MyPlansStateImpl( + {final List activePlans = const [], + final List draftPlans = const [], + this.isLoading = false, + this.error = null, + this.selectedStateIndex = 0, + this.minActivePlanPrice = 0, + this.maxActivePlanPrice = 0, + this.minDraftPlanPrice = 0, + this.maxDraftPlanPrice = 0, + this.activeFilterState = const PlansFilterState( + priceFilter: (null, null), durationIndex: 0, typeIndex: 0), + this.draftFilterState = const PlansFilterState( + priceFilter: (null, null), durationIndex: 0, typeIndex: 0)}) + : _activePlans = activePlans, + _draftPlans = draftPlans; + + final List _activePlans; + @override + @JsonKey() + List get activePlans { + if (_activePlans is EqualUnmodifiableListView) return _activePlans; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_activePlans); + } + + final List _draftPlans; + @override + @JsonKey() + List get draftPlans { + if (_draftPlans is EqualUnmodifiableListView) return _draftPlans; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_draftPlans); + } + + @override + @JsonKey() + final bool isLoading; + @override + @JsonKey() + final String? error; + @override + @JsonKey() + final int selectedStateIndex; + @override + @JsonKey() + final int minActivePlanPrice; + @override + @JsonKey() + final int maxActivePlanPrice; + @override + @JsonKey() + final int minDraftPlanPrice; + @override + @JsonKey() + final int maxDraftPlanPrice; + @override + @JsonKey() + final PlansFilterState activeFilterState; + @override + @JsonKey() + final PlansFilterState draftFilterState; + + @override + String toString() { + return 'MyPlansState(activePlans: $activePlans, draftPlans: $draftPlans, isLoading: $isLoading, error: $error, selectedStateIndex: $selectedStateIndex, minActivePlanPrice: $minActivePlanPrice, maxActivePlanPrice: $maxActivePlanPrice, minDraftPlanPrice: $minDraftPlanPrice, maxDraftPlanPrice: $maxDraftPlanPrice, activeFilterState: $activeFilterState, draftFilterState: $draftFilterState)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$MyPlansStateImpl && + const DeepCollectionEquality() + .equals(other._activePlans, _activePlans) && + const DeepCollectionEquality() + .equals(other._draftPlans, _draftPlans) && + (identical(other.isLoading, isLoading) || + other.isLoading == isLoading) && + (identical(other.error, error) || other.error == error) && + (identical(other.selectedStateIndex, selectedStateIndex) || + other.selectedStateIndex == selectedStateIndex) && + (identical(other.minActivePlanPrice, minActivePlanPrice) || + other.minActivePlanPrice == minActivePlanPrice) && + (identical(other.maxActivePlanPrice, maxActivePlanPrice) || + other.maxActivePlanPrice == maxActivePlanPrice) && + (identical(other.minDraftPlanPrice, minDraftPlanPrice) || + other.minDraftPlanPrice == minDraftPlanPrice) && + (identical(other.maxDraftPlanPrice, maxDraftPlanPrice) || + other.maxDraftPlanPrice == maxDraftPlanPrice) && + (identical(other.activeFilterState, activeFilterState) || + other.activeFilterState == activeFilterState) && + (identical(other.draftFilterState, draftFilterState) || + other.draftFilterState == draftFilterState)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + const DeepCollectionEquality().hash(_activePlans), + const DeepCollectionEquality().hash(_draftPlans), + isLoading, + error, + selectedStateIndex, + minActivePlanPrice, + maxActivePlanPrice, + minDraftPlanPrice, + maxDraftPlanPrice, + activeFilterState, + draftFilterState); + + /// Create a copy of MyPlansState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$MyPlansStateImplCopyWith<_$MyPlansStateImpl> get copyWith => + __$$MyPlansStateImplCopyWithImpl<_$MyPlansStateImpl>(this, _$identity); +} + +abstract class _MyPlansState implements MyPlansState { + factory _MyPlansState( + {final List activePlans, + final List draftPlans, + final bool isLoading, + final String? error, + final int selectedStateIndex, + final int minActivePlanPrice, + final int maxActivePlanPrice, + final int minDraftPlanPrice, + final int maxDraftPlanPrice, + final PlansFilterState activeFilterState, + final PlansFilterState draftFilterState}) = _$MyPlansStateImpl; + + @override + List get activePlans; + @override + List get draftPlans; + @override + bool get isLoading; + @override + String? get error; + @override + int get selectedStateIndex; + @override + int get minActivePlanPrice; + @override + int get maxActivePlanPrice; + @override + int get minDraftPlanPrice; + @override + int get maxDraftPlanPrice; + @override + PlansFilterState get activeFilterState; + @override + PlansFilterState get draftFilterState; + + /// Create a copy of MyPlansState + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$MyPlansStateImplCopyWith<_$MyPlansStateImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/features/my_plans/presentation/my_plans_screen.dart b/lib/features/my_plans/presentation/my_plans_screen.dart new file mode 100644 index 00000000..efa423e0 --- /dev/null +++ b/lib/features/my_plans/presentation/my_plans_screen.dart @@ -0,0 +1,189 @@ +import 'package:easy_debounce/easy_debounce.dart'; +import 'package:eatvio_app/core/locator_service.dart'; +import 'package:eatvio_app/core/theme/custom_text_style.dart'; +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/core/widgets/add_item_button.dart'; +import 'package:eatvio_app/core/widgets/custom_search_bar.dart'; +import 'package:eatvio_app/core/widgets/loader.dart'; +import 'package:eatvio_app/features/create_plan/presentation/create_plan_screen.dart'; +import 'package:eatvio_app/features/feed/presentation/widgets/filter_badge.dart'; +import 'package:eatvio_app/features/feed/presentation/widgets/plans_filter_bottom_sheet.dart'; +import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_cubit.dart'; +import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_state.dart'; +import 'package:eatvio_app/features/my_plans/presentation/widgets/my_plan_widget.dart'; +import 'package:eatvio_app/features/my_plans/presentation/widgets/my_plans_tab.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; + +class MyPlansScreen extends StatelessWidget { + const MyPlansScreen({super.key}); + static const name = 'myPlansScreen'; + static const path = '/my-plans-screen'; + @override + Widget build(BuildContext context) { + return BlocProvider(create: (context) => sl()..loadAllPlans(), child: MyPlansScreenContent()); + } +} + +class MyPlansScreenContent extends StatelessWidget { + MyPlansScreenContent({super.key}); + + final activePlansScrollController = ScrollController(); + final draftPlansScrollController = ScrollController(); + + final activePlansSearchController = SearchController(); + final draftPlansSearchController = SearchController(); + + @override + Widget build(BuildContext context) { + final s = S.of(context); + return Scaffold( + backgroundColor: appTheme.gray100, + appBar: AppBar( + centerTitle: true, + title: Text( + s.my_plans, + maxLines: 2, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + softWrap: true, + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), + ), + ), + body: BlocBuilder( + buildWhen: (previous, current) => previous.selectedStateIndex != current.selectedStateIndex, + builder: (context, state) { + return NestedScrollView( + controller: switch (state.selectedStateIndex) { + 1 => draftPlansScrollController, + _ => activePlansScrollController, + }, + headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { + return [ + SliverAppBar( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(bottom: Radius.circular(12)), + ), + backgroundColor: appTheme.gray100, + foregroundColor: appTheme.gray100, + automaticallyImplyLeading: false, + stretchTriggerOffset: 1, + floating: true, + primary: false, + snap: true, + scrolledUnderElevation: 0, + bottom: PreferredSize( + preferredSize: Size( + MediaQuery.sizeOf(context).width, + 64, + ), + child: Padding( + padding: const EdgeInsets.only(bottom: 0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(height: 8), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: MyPlansTab(), + ), + SizedBox(height: 8), + Row( + children: [ + SizedBox(width: 4), + Expanded( + child: CustomSearchBar( + enableIcon: false, + backgroundColor: appTheme.whiteA700, + borderRadius: 100, + searchController: switch (state.selectedStateIndex) { + 1 => draftPlansSearchController, + _ => activePlansSearchController, + }, + onChanged: (query) { + EasyDebounce.debounce( + 'searchPlans', + Duration(milliseconds: 500), + () { + context.read().loadPlans(query: query == '' ? null : query); + }, + ); + }, + ), + ), + SizedBox(width: 12), + AddItemButton( + onTap: () => GoRouter.of(context).pushNamed(CreatePlanScreen.name), + ), + SizedBox(width: 4), + ], + ), + SizedBox(height: 16), + ], + ), + ), + ), + ), + ]; + }, + body: RefreshIndicator( + onRefresh: () => context.read().loadPlans(), + child: BlocBuilder( + builder: (BuildContext context, MyPlansState state) { + if (state.isLoading) { + return Scaffold( + backgroundColor: Colors.grey.shade200, + body: const Loader(), + ); + } else if (state.error != null) { + return Scaffold( + backgroundColor: Colors.grey.shade200, + body: Padding( + padding: const EdgeInsets.all(16), + child: Center( + child: Text(state.error ?? ''), + ), + ), + ); + } else { + final plans = switch (state.selectedStateIndex) { + 1 => state.draftPlans, + _ => state.activePlans, + }; + return Stack( + children: [ + SizedBox.expand(), + ListView.separated( + shrinkWrap: true, + physics: BouncingScrollPhysics(), + itemBuilder: (context, index) { + return MyPlanWidget(plan: plans[index]); + }, + separatorBuilder: (context, index) { + return SizedBox(height: 20); + }, + itemCount: plans.length, + ), + Positioned( + right: 0, + bottom: 74, + child: FilterBadge( + filterBottomSheet: PlansFilterBottomSheet( + myPlansCubit: context.read(), + ), + ), + ), + ], + ); + } + }, + ), + ), + ); + }, + ), + ); + } +} diff --git a/lib/features/my_plans/presentation/widgets/my_plan_widget.dart b/lib/features/my_plans/presentation/widgets/my_plan_widget.dart new file mode 100644 index 00000000..1040a207 --- /dev/null +++ b/lib/features/my_plans/presentation/widgets/my_plan_widget.dart @@ -0,0 +1,259 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/theme/custom_text_style.dart'; +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/core/widgets/bottom_sheet_handler_bar.dart'; +import 'package:eatvio_app/core/widgets/bottom_sheet_with_blur.dart'; +import 'package:eatvio_app/core/widgets/custom_outlined_button.dart'; +import 'package:eatvio_app/core/widgets/info_row.dart'; +import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart'; +import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_cubit.dart'; +import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_state.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_html/flutter_html.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:go_router/go_router.dart'; +import 'package:share_plus/share_plus.dart'; + +class MyPlanWidget extends StatefulWidget { + final PlanModel plan; + const MyPlanWidget({ + super.key, + required this.plan, + }); + + @override + State createState() => _MyPlanWidgetState(); +} + +class _MyPlanWidgetState extends State { + @override + Widget build(BuildContext context) { + final s = S.of(context); + return DecoratedBox( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: Colors.white, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Stack( + children: [ + InkWell( + onTap: () => GoRouter.of(context).pushNamed('aboutPlanScreen', extra: {'plan': widget.plan, 'navigateToComments': false}), + child: ClipRRect( + borderRadius: BorderRadius.vertical(top: Radius.circular(12)), + child: CachedNetworkImage( + imageUrl: baseUrl + '/storage/' + widget.plan.image, + fit: BoxFit.fitWidth, + width: MediaQuery.of(context).size.width, + ), + ), + ), + Positioned( + bottom: 16, + child: SizedBox( + width: MediaQuery.sizeOf(context).width, + child: InfoRow( + plan: widget.plan, + itemsBackgroundColor: appTheme.whiteA700, + disableDescriptionOfTypes: true, + ), + ), + ), + ], + ), + SizedBox(height: 12), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Text( + widget.plan.name, + style: TextStyles.textMSemibold.copyWith(color: appTheme.black), + ), + ), + if (widget.plan.details.isNotEmpty) ...[ + SizedBox( + height: 12, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Html( + data: widget.plan.details.length > 200 ? widget.plan.details.substring(0, 200) + '...' : widget.plan.details, + style: { + "*": Style( + margin: Margins.zero, + color: appTheme.black, + fontSize: FontSize(TextStyles.textSMedium.fontSize ?? 14), + fontWeight: TextStyles.textSMedium.fontWeight ?? FontWeight.w400, + ), + }, + ), + ), + ], + ], + ), + SizedBox(height: 16), + Padding( + padding: EdgeInsets.only(left: 16), + child: Text( + '${widget.plan.packetsCount == 1 ? widget.plan.packet1Options?.price : '${s.from} ${widget.plan.minPriceOfOptions.toStringAsFixed(0)}'}₽', + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.greenA700), + ), + ), + SizedBox(height: 20), + BlocBuilder( + builder: (context, state) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + children: [ + Row( + children: [ + Expanded( + child: CustomOutlinedButton( + text: switch (state.selectedStateIndex) { + 1 => s.publish, + _ => s.edit, + }, + height: 52, + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), + buttonStyle: ButtonStyle( + shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))), + backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), + side: WidgetStatePropertyAll(BorderSide.none), + ), + ), + ), + if (state.selectedStateIndex == 0) ...[ + SizedBox(width: 6), + CustomOutlinedButton( + onPressed: () async { + await Share.shareUri( + Uri.parse( + baseUrl + '/ru/plan/' + widget.plan.id.toString(), + ), + ); + }, + height: 52, + width: 52, + leftIcon: SvgPicture.asset( + 'assets/icons/share2.svg', + width: 20, + height: 20, + colorFilter: ColorFilter.mode(appTheme.whiteA700, BlendMode.srcIn), + ), + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.red), + buttonStyle: ButtonStyle( + shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))), + backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), + side: WidgetStatePropertyAll(BorderSide.none), + ), + text: '', + ), + ], + ], + ), + SizedBox(height: 8), + Row( + children: [ + Expanded( + child: CustomOutlinedButton( + text: switch (state.selectedStateIndex) { + 0 => s.removeFromActive, + _ => s.edit, + }, + height: 52, + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.black), + buttonStyle: ButtonStyle( + shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))), + backgroundColor: WidgetStatePropertyAll(appTheme.lightGray0), + side: WidgetStatePropertyAll(BorderSide.none), + ), + ), + ), + SizedBox(width: 6), + CustomOutlinedButton( + onPressed: () async { + final result = await context.read().deletePlan(widget.plan.slug); + if (result) + Future.delayed(Duration(seconds: 3), () { + showBottomSheetWithBlur( + context: context, + child: Column( + children: [ + SizedBox( + height: 20, + ), + BottomSheetHandlerBar(), + SizedBox( + height: 20, + ), + Text( + '${widget.plan.name}', + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), + ), + Text( + s.deleted, + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), + ), + SizedBox( + height: 20, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: CustomOutlinedButton( + onPressed: () { + context.pop(); + }, + text: 'Хорошо', + height: 52, + buttonTextStyle: TextStyles.textSSemibold.copyWith(color: appTheme.whiteA700), + buttonStyle: ButtonStyle( + side: WidgetStatePropertyAll(BorderSide.none), + backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), + ), + ), + ), + SizedBox( + height: 20, + ), + ], + ), + ); + }); + }, + height: 52, + width: 52, + leftIcon: SvgPicture.asset( + 'assets/icons/trash_can.svg', + width: 20, + height: 20, + ), + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.red), + buttonStyle: ButtonStyle( + shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))), + backgroundColor: WidgetStatePropertyAll(appTheme.lightGray0), + side: WidgetStatePropertyAll(BorderSide.none), + ), + text: '', + ), + ], + ), + SizedBox(height: 8), + ], + ), + ); + }, + ), + ], + ), + ); + } +} diff --git a/lib/features/my_plans/presentation/widgets/my_plans_tab.dart b/lib/features/my_plans/presentation/widgets/my_plans_tab.dart new file mode 100644 index 00000000..c681efd1 --- /dev/null +++ b/lib/features/my_plans/presentation/widgets/my_plans_tab.dart @@ -0,0 +1,71 @@ +import 'package:eatvio_app/core/theme/custom_text_style.dart'; +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/core/widgets/animated_option_button.dart'; +import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_cubit.dart'; +import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_state.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class MyPlansTab extends StatefulWidget { + const MyPlansTab({super.key, this.additionalOnTap}); + final void Function()? additionalOnTap; + @override + State createState() => _MyPlansTabState(); +} + +class _MyPlansTabState extends State { + final List tabTitles = []; + @override + void didChangeDependencies() { + final s = S.of(context); + if (tabTitles.isEmpty) { + tabTitles.add(s.published); + tabTitles.add(s.drafted); + } + super.didChangeDependencies(); + } + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return Align( + alignment: Alignment.centerLeft, + child: Row( + children: [ + Expanded( + child: Container( + height: 48, + decoration: BoxDecoration( + color: appTheme.whiteA700, + borderRadius: BorderRadius.circular(100), + ), + padding: const EdgeInsets.all(4), + child: Row( + children: [ + ...List.generate( + 2, + (index) => Expanded( + child: AnimatedOptionButton( + textStyle: TextStyles.textSMedium, + isSelected: state.selectedStateIndex == index, + text: tabTitles[index], + onTap: () { + widget.additionalOnTap?.call(); + context.read().changeSelectedStateIndex(index); + }, + ), + ), + ), + ], + ), + ), + ), + ], + ), + ); + }, + ); + } +} diff --git a/lib/features/my_plans/repositories/my_diets_repository.dart b/lib/features/my_plans/repositories/my_diets_repository.dart new file mode 100644 index 00000000..a28c88cd --- /dev/null +++ b/lib/features/my_plans/repositories/my_diets_repository.dart @@ -0,0 +1,39 @@ +import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart'; +import 'package:eatvio_app/features/my_plans/services/my_plans_service.dart'; + +class MyPlansRepository { + final MyPlansService myPlansService; + + MyPlansRepository({required this.myPlansService}); + + Future deletePlan(String slug) async { + try { + await myPlansService.deletePlan(slug: slug); + } catch (e) { + rethrow; + } + } + + Future getPlans({ + int page = 1, + String? query, + String? types, + int? duration, + int? minPrice, + int? maxPrice, + }) async { + try { + return (await myPlansService.getPlans( + page: page, + query: query, + types: types, + duration: duration, + minPrice: minPrice, + maxPrice: maxPrice, + )); + } catch (e) { + rethrow; + } + } + +} diff --git a/lib/features/my_plans/services/my_plans_service.dart b/lib/features/my_plans/services/my_plans_service.dart new file mode 100644 index 00000000..ee81e55e --- /dev/null +++ b/lib/features/my_plans/services/my_plans_service.dart @@ -0,0 +1,28 @@ +import 'package:dio/dio.dart'; +import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart'; +import 'package:retrofit/retrofit.dart'; + +part 'my_plans_service.g.dart'; + +@RestApi() +abstract class MyPlansService { + + factory MyPlansService(Dio dio, {String baseUrl}) = _MyPlansService; + + @GET('/api/plans/index') + Future getPlans({ + @Query('page') required int page, + @Query('filters[name][\$startsWith]') String? query, + @Query('filters[type][\$contains]') String? types, + @Query('filters[duration][\$eq]') int? duration, + @Query('filters[min_price_of_options][\$between][0]') int? minPrice, + @Query('filters[min_price_of_options][\$between][1]') int? maxPrice, + }); + + @POST('/api/plans/delete/{slug}') + Future deletePlan({ + @Path() required String slug, + }); + + +} \ No newline at end of file diff --git a/lib/features/my_plans/services/my_plans_service.g.dart b/lib/features/my_plans/services/my_plans_service.g.dart new file mode 100644 index 00000000..dd70281f --- /dev/null +++ b/lib/features/my_plans/services/my_plans_service.g.dart @@ -0,0 +1,126 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'my_plans_service.dart'; + +// ************************************************************************** +// RetrofitGenerator +// ************************************************************************** + +// ignore_for_file: unnecessary_brace_in_string_interps,no_leading_underscores_for_local_identifiers,unused_element,unnecessary_string_interpolations + +class _MyPlansService implements MyPlansService { + _MyPlansService( + this._dio, { + this.baseUrl, + this.errorLogger, + }); + + final Dio _dio; + + String? baseUrl; + + final ParseErrorLogger? errorLogger; + + @override + Future getPlans({ + required int page, + String? query, + String? types, + int? duration, + int? minPrice, + int? maxPrice, + }) async { + final _extra = {}; + final queryParameters = { + r'page': page, + r'filters[name][$startsWith]': query, + r'filters[type][$contains]': types, + r'filters[duration][$eq]': duration, + r'filters[min_price_of_options][$between][0]': minPrice, + r'filters[min_price_of_options][$between][1]': maxPrice, + }; + queryParameters.removeWhere((k, v) => v == null); + final _headers = {}; + const Map? _data = null; + final _options = _setStreamType(Options( + method: 'GET', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/api/plans/index', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + ))); + final _result = await _dio.fetch>(_options); + late PlanPaginationModel _value; + try { + _value = PlanPaginationModel.fromJson(_result.data!); + } on Object catch (e, s) { + errorLogger?.logError(e, s, _options); + rethrow; + } + return _value; + } + + @override + Future deletePlan({required String slug}) async { + final _extra = {}; + final queryParameters = {}; + final _headers = {}; + const Map? _data = null; + final _options = _setStreamType(Options( + method: 'POST', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/api/plans/delete/${slug}', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + ))); + await _dio.fetch(_options); + } + + RequestOptions _setStreamType(RequestOptions requestOptions) { + if (T != dynamic && + !(requestOptions.responseType == ResponseType.bytes || + requestOptions.responseType == ResponseType.stream)) { + if (T == String) { + requestOptions.responseType = ResponseType.plain; + } else { + requestOptions.responseType = ResponseType.json; + } + } + return requestOptions; + } + + String _combineBaseUrls( + String dioBaseUrl, + String? baseUrl, + ) { + if (baseUrl == null || baseUrl.trim().isEmpty) { + return dioBaseUrl; + } + + final url = Uri.parse(baseUrl); + + if (url.isAbsolute) { + return url.toString(); + } + + return Uri.parse(dioBaseUrl).resolveUri(url).toString(); + } +} diff --git a/lib/features/plans/presentation/cubit/about_plan_cubit.dart b/lib/features/plans/presentation/cubit/about_plan_cubit.dart index ee244be5..ad60723a 100644 --- a/lib/features/plans/presentation/cubit/about_plan_cubit.dart +++ b/lib/features/plans/presentation/cubit/about_plan_cubit.dart @@ -1,5 +1,8 @@ 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/plans/presentation/cubit/about_plan_state.dart'; import 'package:eatvio_app/features/plans/repositories/plan_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -22,11 +25,13 @@ class AboutPlanCubit extends Cubit { try { emit(state.copyWith(isLoading: true)); final plan = await planRepository.getPlan(slug); - emit(state.copyWith( - planModel: plan, - error: null, - isLoading: false, - ),); + emit( + state.copyWith( + planModel: plan, + error: null, + isLoading: false, + ), + ); } catch (e, stackTrace) { log(e.toString(), stackTrace: stackTrace); emit(state.copyWith(error: e.toString(), isLoading: false)); @@ -37,36 +42,87 @@ class AboutPlanCubit extends Cubit { emit(state.copyWith(chosenOptionIndex: index)); } - Future likePlan(String slug) async { - try { - await planRepository.likePlan(slug); - final plan = await planRepository.getPlan(slug); - emit(state.copyWith(planModel: plan, error: null)); - } catch (e, stackTrace) { - log(e.toString(), stackTrace: stackTrace); - emit(state.copyWith(error: e.toString())); + Future likePlan({required int? index, required String slug}) async { + final currentPlan = state.planModel; + if (currentPlan == null) return; + if (currentPlan.wasLikedByMe) { + emit(state.copyWith(planModel: currentPlan.copyWith(wasLikedByMe: false, totalLikes: currentPlan.totalLikes - 1), error: null)); + if (index != null) { + planRepository.emitLikesStateToFeed(LikesState(wasLikedByMe: false, totalLikes: currentPlan.totalLikes - 1, index: index)); + } + } else { + emit(state.copyWith(planModel: currentPlan.copyWith(wasLikedByMe: true, totalLikes: currentPlan.totalLikes + 1), error: null)); + if (index != null) { + planRepository.emitLikesStateToFeed(LikesState(wasLikedByMe: true, totalLikes: currentPlan.totalLikes + 1, index: index)); + } } + + EasyDebounce.debounce( + 'like_plan_$slug', + Duration(seconds: 1), + () async { + try { + await planRepository.likePlan(slug); + } catch (e) { + log(e.toString()); + if (index != null) { + planRepository.emitLikesStateToFeed(LikesState(wasLikedByMe: currentPlan.wasLikedByMe, totalLikes: currentPlan.totalLikes, index: index)); + } + emit(state.copyWith(planModel: currentPlan, error: null)); + } + }, + ); } - Future addToFavorite(String slug) async { - try { - await planRepository.addToFavorite(slug); - final plan = await planRepository.getPlan(slug); - emit(state.copyWith(planModel: plan, error: null)); - } catch (e, stackTrace) { - log(e.toString(), stackTrace: stackTrace); - emit(state.copyWith(error: e.toString())); + Future addToFavorite({required String slug, required int? index}) async { + final currentPlan = state.planModel; + if (currentPlan == null) return; + + emit(state.copyWith(planModel: currentPlan.copyWith(isFavorite: true))); + if (index != null) { + planRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: true, index: index)); } + EasyDebounce.debounce( + 'add_or_remove_plan_to_favorites', + Duration(seconds: 1), + () async { + try { + await planRepository.addToFavorite(slug); + } catch (e) { + log(e.toString()); + showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время'); + if (index != null) { + planRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: currentPlan.isFavorite, index: index)); + } + emit(state.copyWith(planModel: currentPlan, error: null)); + } + }, + ); } - Future removeFromFavorite(String slug) async { - try { - await planRepository.removeFromFavorite(slug); - final plan = await planRepository.getPlan(slug); - emit(state.copyWith(planModel: plan, error: null)); - } catch (e, stackTrace) { - log(e.toString(), stackTrace: stackTrace); - emit(state.copyWith(error: e.toString())); + Future removeFromFavorite({required String slug, required int? index}) async { + final currentPlan = state.planModel; + if (currentPlan == null) return; + + emit(state.copyWith(planModel: currentPlan.copyWith(isFavorite: false))); + if (index != null) { + planRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: false, index: index)); } + EasyDebounce.debounce( + 'add_or_remove_plan_to_favorites', + Duration(seconds: 1), + () async { + try { + await planRepository.removeFromFavorite(slug); + } catch (e) { + log(e.toString()); + showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время'); + if (index != null) { + planRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: currentPlan.isFavorite, index: index)); + } + emit(state.copyWith(planModel: currentPlan, error: null)); + } + }, + ); } } diff --git a/lib/features/plans/presentation/pages/about_plan_screen.dart b/lib/features/plans/presentation/pages/about_plan_screen.dart index d49db1b0..3392e523 100644 --- a/lib/features/plans/presentation/pages/about_plan_screen.dart +++ b/lib/features/plans/presentation/pages/about_plan_screen.dart @@ -5,7 +5,7 @@ import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/utils/localization_utils.dart'; import 'package:eatvio_app/core/utils/string_extentions.dart'; import 'package:eatvio_app/core/widgets/bookmark_button.dart'; -import 'package:eatvio_app/core/widgets/diet_info_row.dart'; +import 'package:eatvio_app/core/widgets/info_row.dart'; import 'package:eatvio_app/core/widgets/loader.dart'; import 'package:eatvio_app/features/diets/presentation/widgets/author_card.dart'; import 'package:eatvio_app/features/diets/presentation/widgets/expandable_text.dart'; @@ -31,9 +31,10 @@ import 'package:flutter_svg/svg.dart'; import '../widgets/option_widget.dart'; class AboutPlanScreen extends StatefulWidget { - AboutPlanScreen({super.key, required this.plan, required this.navigateToComments}); + AboutPlanScreen({super.key, required this.plan, required this.navigateToComments, required this.index}); final PlanModel plan; final bool navigateToComments; + final int? index; @override State createState() => _AboutPlanScreenState(); } @@ -90,14 +91,14 @@ class _AboutPlanScreenState extends State { } }, builder: (BuildContext context, CommentsState commentsState) { - Future.delayed(Duration(milliseconds: 500), () { - if (widget.navigateToComments && !_jumped) { - _scrollToBottom(); - } - }); return BlocBuilder( builder: (context, state) { if (state.isLoading) return Scaffold(body: Loader()); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (widget.navigateToComments && !_jumped) { + _scrollToBottom(); + } + }); return Scaffold( backgroundColor: appTheme.gray100, resizeToAvoidBottomInset: true, @@ -152,9 +153,9 @@ class _AboutPlanScreenState extends State { final planModel = state.planModel; if (planModel == null) return; if (planModel.isFavorite) { - context.read().removeFromFavorite(planModel.slug); + context.read().removeFromFavorite(slug: planModel.slug, index: widget.index); } else { - context.read().addToFavorite(planModel.slug); + context.read().addToFavorite(slug: planModel.slug, index: widget.index); } }, width: 52, @@ -291,7 +292,9 @@ class _AboutPlanScreenState extends State { actions: [ PortalTarget( visible: isMoreOptionsVisible, - portalFollower: MoreOptions(id: widget.plan.id), + portalFollower: MoreOptions( + path: 'plan/${widget.plan.id}', + ), anchor: Aligned( shiftToWithinBound: AxisFlag(x: true, y: true), follower: Alignment.topRight, @@ -352,7 +355,7 @@ class _AboutPlanScreenState extends State { SizedBox(width: 12), GestureDetector( onTap: () { - context.read().likePlan(widget.plan.slug); + context.read().likePlan(slug: widget.plan.slug, index: widget.index); }, child: InfoBouble( children: [ diff --git a/lib/features/plans/repositories/plan_repository.dart b/lib/features/plans/repositories/plan_repository.dart index 5c1df29a..313ad8b1 100644 --- a/lib/features/plans/repositories/plan_repository.dart +++ b/lib/features/plans/repositories/plan_repository.dart @@ -1,13 +1,21 @@ -import 'dart:developer'; - +import 'dart:async'; +import 'package:eatvio_app/features/diets/repositories/diet_repository.dart'; import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart'; import 'package:eatvio_app/features/plans/services/plan_service.dart'; + class PlanRepository { final PlanService planService; PlanRepository({required this.planService}); + final _planLikeStateStreamController = StreamController.broadcast(); + final _planFavoriteStateStreamController = StreamController.broadcast(); + final _planCommentsStateStreamController = StreamController.broadcast(); + Stream get planLikeState => _planLikeStateStreamController.stream; + Stream get planFavoriteState => _planFavoriteStateStreamController.stream; + Stream get planCommentsState => _planCommentsStateStreamController.stream; + Future getPlan(String slug) async { try { return planService.getPlan(slug); @@ -35,9 +43,14 @@ class PlanRepository { Future removeFromFavorite(String slug) async { try { await planService.removeFromFavorite(slug: slug); - } catch (e, stackTrace) { - log(e.toString(), stackTrace: stackTrace); + } catch (e) { rethrow; } } + + void emitLikesStateToFeed(LikesState state) => _planLikeStateStreamController.add(state); + void emitFavoriteStateToFeed(FavoriteState state) => _planFavoriteStateStreamController.add(state); + void emitCommentsStateToFeed(CommentsState state) => _planCommentsStateStreamController.add(state); } + + 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 7f2cf65a..ce0065c8 100644 --- a/lib/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart +++ b/lib/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart @@ -1,3 +1,4 @@ +import 'dart:developer'; import 'dart:io'; import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart'; import 'package:eatvio_app/features/chat/repositories/chat_repository.dart'; @@ -45,7 +46,8 @@ class ProfileCubit extends Cubit { circumferencePoints: await profileRepository.getCircumferences(period: period), ), ); - } catch (e) { + } catch (e, stackTrace) { + log(e.toString(), stackTrace: stackTrace); emit(ProfileState.error(errorMessage: e.toString())); } } diff --git a/lib/features/profile/presentation/pages/profile_screen.dart b/lib/features/profile/presentation/pages/profile_screen.dart index 6ee797a3..b838f707 100644 --- a/lib/features/profile/presentation/pages/profile_screen.dart +++ b/lib/features/profile/presentation/pages/profile_screen.dart @@ -276,7 +276,10 @@ class _ProfileScreenState extends State { height: 378, child: PageView.builder( itemBuilder: (BuildContext context, int index) { - return PlanWidget(planModel: state.plansList[index]); + return PlanWidget( + plan: state.plansList[index], + index: index, + ); }, onPageChanged: (bullet) { setState(() { @@ -1099,7 +1102,7 @@ class _ProfileScreenState extends State { ); } - _charts() { + _charts() { return Material( color: Colors.white, borderRadius: BorderRadius.circular(16), diff --git a/lib/features/profile/presentation/widgets/circumference_chart_widget.dart b/lib/features/profile/presentation/widgets/circumference_chart_widget.dart index 82130c54..d7673989 100644 --- a/lib/features/profile/presentation/widgets/circumference_chart_widget.dart +++ b/lib/features/profile/presentation/widgets/circumference_chart_widget.dart @@ -8,12 +8,12 @@ import 'package:syncfusion_flutter_charts/charts.dart'; class CircumferenceChartWidget extends StatefulWidget { final List circumferencePoints; - final ProfileCubit profileCubit; + final ProfileCubit? profileCubit; const CircumferenceChartWidget({ super.key, required this.circumferencePoints, - required this.profileCubit, + this.profileCubit, }); @override @@ -171,117 +171,118 @@ class _CircumferenceChartWidgetState extends State { series: _getSplineSeries(), tooltipBehavior: TooltipBehavior(enable: true), ), - FloatingActionButton.small( - heroTag: 'addCircumference', - onPressed: () { - if (widget.circumferencePoints.isNotEmpty) { - chest = widget.circumferencePoints.last.chestCircumference; - waist = widget.circumferencePoints.last.waistCircumference; - hip = widget.circumferencePoints.last.hipCircumference; - chestController = FixedExtentScrollController(initialItem: chest - 20); - waistController = FixedExtentScrollController(initialItem: waist - 20); - hipController = FixedExtentScrollController(initialItem: hip - 20); - } else { - chestController = FixedExtentScrollController(); - waistController = FixedExtentScrollController(); - hipController = FixedExtentScrollController(); - } - showModalBottomSheet( - useRootNavigator: true, - context: context, - builder: (context) { - return Column( - children: [ - Expanded( - child: Row( - children: [ - Expanded( - child: CupertinoPicker( - magnification: 1.22, - squeeze: 1.2, - useMagnifier: true, - scrollController: chestController, - itemExtent: 24, - onSelectedItemChanged: (int value) { - chest = value; - }, - children: [ - for (int i = 20; i < 200; i++) Text('$i'), - ], + if (widget.profileCubit != null) + FloatingActionButton.small( + heroTag: 'addCircumference', + onPressed: () { + if (widget.circumferencePoints.isNotEmpty) { + chest = widget.circumferencePoints.last.chestCircumference; + waist = widget.circumferencePoints.last.waistCircumference; + hip = widget.circumferencePoints.last.hipCircumference; + chestController = FixedExtentScrollController(initialItem: chest - 20); + waistController = FixedExtentScrollController(initialItem: waist - 20); + hipController = FixedExtentScrollController(initialItem: hip - 20); + } else { + chestController = FixedExtentScrollController(); + waistController = FixedExtentScrollController(); + hipController = FixedExtentScrollController(); + } + showModalBottomSheet( + useRootNavigator: true, + context: context, + builder: (context) { + return Column( + children: [ + Expanded( + child: Row( + children: [ + Expanded( + child: CupertinoPicker( + magnification: 1.22, + squeeze: 1.2, + useMagnifier: true, + scrollController: chestController, + itemExtent: 24, + onSelectedItemChanged: (int value) { + chest = value; + }, + children: [ + for (int i = 20; i < 200; i++) Text('$i'), + ], + ), ), - ), - Expanded( - child: CupertinoPicker( - magnification: 1.22, - squeeze: 1.2, - useMagnifier: true, - scrollController: waistController, - itemExtent: 24, - onSelectedItemChanged: (int value) { - waist = value; - }, - children: [ - for (int i = 20; i < 200; i++) Text('$i'), - ], + Expanded( + child: CupertinoPicker( + magnification: 1.22, + squeeze: 1.2, + useMagnifier: true, + scrollController: waistController, + itemExtent: 24, + onSelectedItemChanged: (int value) { + waist = value; + }, + children: [ + for (int i = 20; i < 200; i++) Text('$i'), + ], + ), ), - ), - Expanded( - child: CupertinoPicker( - magnification: 1.22, - squeeze: 1.2, - useMagnifier: true, - scrollController: hipController, - itemExtent: 24, - onSelectedItemChanged: (int value) { - hip = value; - }, - children: [ - for (int i = 20; i < 200; i++) Text('$i'), - ], + Expanded( + child: CupertinoPicker( + magnification: 1.22, + squeeze: 1.2, + useMagnifier: true, + scrollController: hipController, + itemExtent: 24, + onSelectedItemChanged: (int value) { + hip = value; + }, + children: [ + for (int i = 20; i < 200; i++) Text('$i'), + ], + ), ), - ), - ], - ), - ), - InkWell( - onTap: () { - widget.profileCubit.addCircumference( - chestCircumference: chest + 20, - waistCircumference: waist + 20, - hipCircumference: hip + 20, - ); - Navigator.of(context).pop(); - }, - borderRadius: BorderRadius.circular(16), - child: Ink( - width: MediaQuery.of(context).size.width - 32, - padding: EdgeInsets.all(16), - decoration: BoxDecoration( - color: appTheme.blueA400, - borderRadius: BorderRadius.circular(16), + ], ), - child: Center( - child: Text( - 'Сохранить', - style: TextStyle( - color: Colors.white, + ), + InkWell( + onTap: () { + widget.profileCubit?.addCircumference( + chestCircumference: chest + 20, + waistCircumference: waist + 20, + hipCircumference: hip + 20, + ); + Navigator.of(context).pop(); + }, + borderRadius: BorderRadius.circular(16), + child: Ink( + width: MediaQuery.of(context).size.width - 32, + padding: EdgeInsets.all(16), + decoration: BoxDecoration( + color: appTheme.blueA400, + borderRadius: BorderRadius.circular(16), + ), + child: Center( + child: Text( + 'Сохранить', + style: TextStyle( + color: Colors.white, + ), ), ), ), ), - ), - ], - ); - }, - ); - }, - backgroundColor: appTheme.blueA400, - shape: CircleBorder(), - child: Icon( - Icons.add, - color: Colors.white, + ], + ); + }, + ); + }, + backgroundColor: appTheme.blueA400, + shape: CircleBorder(), + child: Icon( + Icons.add, + color: Colors.white, + ), ), - ), ], ), ], diff --git a/lib/features/profile/presentation/widgets/weight_chart_widget.dart b/lib/features/profile/presentation/widgets/weight_chart_widget.dart index 082befe0..ecce8728 100644 --- a/lib/features/profile/presentation/widgets/weight_chart_widget.dart +++ b/lib/features/profile/presentation/widgets/weight_chart_widget.dart @@ -8,12 +8,12 @@ import 'package:syncfusion_flutter_charts/charts.dart'; class WeightChartWidget extends StatefulWidget { final List weightPoints; - final ProfileCubit profileCubit; + final ProfileCubit? profileCubit; const WeightChartWidget({ super.key, required this.weightPoints, - required this.profileCubit, + this.profileCubit, }); @override @@ -111,99 +111,100 @@ class _WeightChartWidgetState extends State { series: _getSplineSeries(), tooltipBehavior: TooltipBehavior(enable: true), ), - FloatingActionButton.small( - heroTag: 'addWeight', - onPressed: () { - if (widget.weightPoints.isNotEmpty) { - try { - weightKilo = int.parse(widget.weightPoints.last.weight.toString().split('.')[0]); - weightGrams = int.parse(widget.weightPoints.last.weight.toString().split('.')[1][0]); - } catch (e) { - weightGrams = 0; + if (widget.profileCubit != null) + FloatingActionButton.small( + heroTag: 'addWeight', + onPressed: () { + if (widget.weightPoints.isNotEmpty) { + try { + weightKilo = int.parse(widget.weightPoints.last.weight.toString().split('.')[0]); + weightGrams = int.parse(widget.weightPoints.last.weight.toString().split('.')[1][0]); + } catch (e) { + weightGrams = 0; + } + weightKiloController = FixedExtentScrollController(initialItem: weightKilo - 20); + weightGramsController = FixedExtentScrollController(initialItem: weightGrams); + } else { + weightKiloController = FixedExtentScrollController(); + weightGramsController = FixedExtentScrollController(); } - weightKiloController = FixedExtentScrollController(initialItem: weightKilo - 20); - weightGramsController = FixedExtentScrollController(initialItem: weightGrams); - } else { - weightKiloController = FixedExtentScrollController(); - weightGramsController = FixedExtentScrollController(); - } - showModalBottomSheet( - useRootNavigator: true, - context: context, - builder: (context) { - return Column( - children: [ - Expanded( - child: Row( - children: [ - Expanded( - child: CupertinoPicker( - magnification: 1.22, - squeeze: 1.2, - useMagnifier: true, - scrollController: weightKiloController, - itemExtent: 24, - onSelectedItemChanged: (int value) { - weightKilo = value + 20; - }, - children: [ - for (int i = 20; i < 300; i++) Text('$i'), - ], + showModalBottomSheet( + useRootNavigator: true, + context: context, + builder: (context) { + return Column( + children: [ + Expanded( + child: Row( + children: [ + Expanded( + child: CupertinoPicker( + magnification: 1.22, + squeeze: 1.2, + useMagnifier: true, + scrollController: weightKiloController, + itemExtent: 24, + onSelectedItemChanged: (int value) { + weightKilo = value + 20; + }, + children: [ + for (int i = 20; i < 300; i++) Text('$i'), + ], + ), ), - ), - Expanded( - child: CupertinoPicker( - magnification: 1.22, - squeeze: 1.2, - useMagnifier: true, - scrollController: weightGramsController, - itemExtent: 24, - onSelectedItemChanged: (int value) { - weightGrams = value; - }, - children: [ - for (int i = 0; i < 10; i++) Text('$i'), - ], + Expanded( + child: CupertinoPicker( + magnification: 1.22, + squeeze: 1.2, + useMagnifier: true, + scrollController: weightGramsController, + itemExtent: 24, + onSelectedItemChanged: (int value) { + weightGrams = value; + }, + children: [ + for (int i = 0; i < 10; i++) Text('$i'), + ], + ), ), - ), - ], - ), - ), - InkWell( - onTap: () { - widget.profileCubit.addWeight(weight: double.parse('$weightKilo.$weightGrams')); - Navigator.of(context).pop(); - }, - borderRadius: BorderRadius.circular(16), - child: Ink( - width: MediaQuery.of(context).size.width - 32, - padding: EdgeInsets.all(16), - decoration: BoxDecoration( - color: appTheme.blueA400, - borderRadius: BorderRadius.circular(16), + ], ), - child: Center( - child: Text( - 'Сохранить', - style: TextStyle( - color: Colors.white, + ), + InkWell( + onTap: () { + widget.profileCubit?.addWeight(weight: double.parse('$weightKilo.$weightGrams')); + Navigator.of(context).pop(); + }, + borderRadius: BorderRadius.circular(16), + child: Ink( + width: MediaQuery.of(context).size.width - 32, + padding: EdgeInsets.all(16), + decoration: BoxDecoration( + color: appTheme.blueA400, + borderRadius: BorderRadius.circular(16), + ), + child: Center( + child: Text( + 'Сохранить', + style: TextStyle( + color: Colors.white, + ), ), ), ), ), - ), - ], - ); - }, - ); - }, - backgroundColor: appTheme.blueA400, - shape: CircleBorder(), - child: Icon( - Icons.add, - color: Colors.white, + ], + ); + }, + ); + }, + backgroundColor: appTheme.blueA400, + shape: CircleBorder(), + child: Icon( + Icons.add, + color: Colors.white, + ), ), - ), ], ), ], diff --git a/lib/features/user_details/presentation/cubit/user_details_cubit.dart b/lib/features/user_details/presentation/cubit/user_details_cubit.dart new file mode 100644 index 00000000..c6eb36b7 --- /dev/null +++ b/lib/features/user_details/presentation/cubit/user_details_cubit.dart @@ -0,0 +1,41 @@ +import 'dart:developer'; + +import 'package:eatvio_app/features/profile/repositories/profile_repository.dart'; +import 'package:eatvio_app/features/user_details/presentation/cubit/user_details_state.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class UserDetailsCubit extends Cubit { + final ProfileRepository profileRepository; + + UserDetailsCubit({required this.profileRepository}) : super(UserDetailsState()); + + void getUserDetails(String slug) async { + try { + final profileDetails = await profileRepository.getProfileDetailsBySlug(slug); + emit(state.copyWith(profileDetails: profileDetails, isLoading: false)); + getChartData(slug: slug); + } catch (e) { + log(e.toString()); + emit(state.copyWith(error: e.toString(), isLoading: false)); + } + } + + void getChartData({ + String period = '1', + required String slug, + }) async { + try { + final weightPoints = await profileRepository.getWeight(period: period); + final circumferencePoints = await profileRepository.getCircumferences(period: period); + emit( + state.copyWith( + weightPoints: weightPoints, + circumferencePoints: circumferencePoints, + ), + ); + } catch (e, stackTrace) { + log(e.toString(), stackTrace: stackTrace); + emit(state.copyWith(error: e.toString(), isLoading: false)); + } + } +} diff --git a/lib/features/user_details/presentation/cubit/user_details_state.dart b/lib/features/user_details/presentation/cubit/user_details_state.dart new file mode 100644 index 00000000..354d6006 --- /dev/null +++ b/lib/features/user_details/presentation/cubit/user_details_state.dart @@ -0,0 +1,17 @@ +import 'package:eatvio_app/features/profile/models/circumference_model.dart'; +import 'package:eatvio_app/features/profile/models/profile_model.dart'; +import 'package:eatvio_app/features/profile/models/weight_model.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'user_details_state.freezed.dart'; + +@freezed +class UserDetailsState with _$UserDetailsState { + factory UserDetailsState({ + @Default(false) bool isLoading, + String? error, + ProfileDetailsModel? profileDetails, + List? weightPoints, + List? circumferencePoints, + }) = _UserDetailsState; +} \ No newline at end of file diff --git a/lib/features/user_details/presentation/cubit/user_details_state.freezed.dart b/lib/features/user_details/presentation/cubit/user_details_state.freezed.dart new file mode 100644 index 00000000..5cad9acb --- /dev/null +++ b/lib/features/user_details/presentation/cubit/user_details_state.freezed.dart @@ -0,0 +1,278 @@ +// 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 'user_details_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 _$UserDetailsState { + bool get isLoading => throw _privateConstructorUsedError; + String? get error => throw _privateConstructorUsedError; + ProfileDetailsModel? get profileDetails => throw _privateConstructorUsedError; + List? get weightPoints => throw _privateConstructorUsedError; + List? get circumferencePoints => + throw _privateConstructorUsedError; + + /// Create a copy of UserDetailsState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $UserDetailsStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $UserDetailsStateCopyWith<$Res> { + factory $UserDetailsStateCopyWith( + UserDetailsState value, $Res Function(UserDetailsState) then) = + _$UserDetailsStateCopyWithImpl<$Res, UserDetailsState>; + @useResult + $Res call( + {bool isLoading, + String? error, + ProfileDetailsModel? profileDetails, + List? weightPoints, + List? circumferencePoints}); + + $ProfileDetailsModelCopyWith<$Res>? get profileDetails; +} + +/// @nodoc +class _$UserDetailsStateCopyWithImpl<$Res, $Val extends UserDetailsState> + implements $UserDetailsStateCopyWith<$Res> { + _$UserDetailsStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of UserDetailsState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? isLoading = null, + Object? error = freezed, + Object? profileDetails = freezed, + Object? weightPoints = freezed, + Object? circumferencePoints = freezed, + }) { + return _then(_value.copyWith( + isLoading: null == isLoading + ? _value.isLoading + : isLoading // ignore: cast_nullable_to_non_nullable + as bool, + error: freezed == error + ? _value.error + : error // ignore: cast_nullable_to_non_nullable + as String?, + profileDetails: freezed == profileDetails + ? _value.profileDetails + : profileDetails // ignore: cast_nullable_to_non_nullable + as ProfileDetailsModel?, + weightPoints: freezed == weightPoints + ? _value.weightPoints + : weightPoints // ignore: cast_nullable_to_non_nullable + as List?, + circumferencePoints: freezed == circumferencePoints + ? _value.circumferencePoints + : circumferencePoints // ignore: cast_nullable_to_non_nullable + as List?, + ) as $Val); + } + + /// Create a copy of UserDetailsState + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $ProfileDetailsModelCopyWith<$Res>? get profileDetails { + if (_value.profileDetails == null) { + return null; + } + + return $ProfileDetailsModelCopyWith<$Res>(_value.profileDetails!, (value) { + return _then(_value.copyWith(profileDetails: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$UserDetailsStateImplCopyWith<$Res> + implements $UserDetailsStateCopyWith<$Res> { + factory _$$UserDetailsStateImplCopyWith(_$UserDetailsStateImpl value, + $Res Function(_$UserDetailsStateImpl) then) = + __$$UserDetailsStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {bool isLoading, + String? error, + ProfileDetailsModel? profileDetails, + List? weightPoints, + List? circumferencePoints}); + + @override + $ProfileDetailsModelCopyWith<$Res>? get profileDetails; +} + +/// @nodoc +class __$$UserDetailsStateImplCopyWithImpl<$Res> + extends _$UserDetailsStateCopyWithImpl<$Res, _$UserDetailsStateImpl> + implements _$$UserDetailsStateImplCopyWith<$Res> { + __$$UserDetailsStateImplCopyWithImpl(_$UserDetailsStateImpl _value, + $Res Function(_$UserDetailsStateImpl) _then) + : super(_value, _then); + + /// Create a copy of UserDetailsState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? isLoading = null, + Object? error = freezed, + Object? profileDetails = freezed, + Object? weightPoints = freezed, + Object? circumferencePoints = freezed, + }) { + return _then(_$UserDetailsStateImpl( + isLoading: null == isLoading + ? _value.isLoading + : isLoading // ignore: cast_nullable_to_non_nullable + as bool, + error: freezed == error + ? _value.error + : error // ignore: cast_nullable_to_non_nullable + as String?, + profileDetails: freezed == profileDetails + ? _value.profileDetails + : profileDetails // ignore: cast_nullable_to_non_nullable + as ProfileDetailsModel?, + weightPoints: freezed == weightPoints + ? _value._weightPoints + : weightPoints // ignore: cast_nullable_to_non_nullable + as List?, + circumferencePoints: freezed == circumferencePoints + ? _value._circumferencePoints + : circumferencePoints // ignore: cast_nullable_to_non_nullable + as List?, + )); + } +} + +/// @nodoc + +class _$UserDetailsStateImpl implements _UserDetailsState { + _$UserDetailsStateImpl( + {this.isLoading = false, + this.error, + this.profileDetails, + final List? weightPoints, + final List? circumferencePoints}) + : _weightPoints = weightPoints, + _circumferencePoints = circumferencePoints; + + @override + @JsonKey() + final bool isLoading; + @override + final String? error; + @override + final ProfileDetailsModel? profileDetails; + final List? _weightPoints; + @override + List? get weightPoints { + final value = _weightPoints; + if (value == null) return null; + if (_weightPoints is EqualUnmodifiableListView) return _weightPoints; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(value); + } + + final List? _circumferencePoints; + @override + List? get circumferencePoints { + final value = _circumferencePoints; + if (value == null) return null; + if (_circumferencePoints is EqualUnmodifiableListView) + return _circumferencePoints; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(value); + } + + @override + String toString() { + return 'UserDetailsState(isLoading: $isLoading, error: $error, profileDetails: $profileDetails, weightPoints: $weightPoints, circumferencePoints: $circumferencePoints)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$UserDetailsStateImpl && + (identical(other.isLoading, isLoading) || + other.isLoading == isLoading) && + (identical(other.error, error) || other.error == error) && + (identical(other.profileDetails, profileDetails) || + other.profileDetails == profileDetails) && + const DeepCollectionEquality() + .equals(other._weightPoints, _weightPoints) && + const DeepCollectionEquality() + .equals(other._circumferencePoints, _circumferencePoints)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + isLoading, + error, + profileDetails, + const DeepCollectionEquality().hash(_weightPoints), + const DeepCollectionEquality().hash(_circumferencePoints)); + + /// Create a copy of UserDetailsState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$UserDetailsStateImplCopyWith<_$UserDetailsStateImpl> get copyWith => + __$$UserDetailsStateImplCopyWithImpl<_$UserDetailsStateImpl>( + this, _$identity); +} + +abstract class _UserDetailsState implements UserDetailsState { + factory _UserDetailsState( + {final bool isLoading, + final String? error, + final ProfileDetailsModel? profileDetails, + final List? weightPoints, + final List? circumferencePoints}) = + _$UserDetailsStateImpl; + + @override + bool get isLoading; + @override + String? get error; + @override + ProfileDetailsModel? get profileDetails; + @override + List? get weightPoints; + @override + List? get circumferencePoints; + + /// Create a copy of UserDetailsState + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$UserDetailsStateImplCopyWith<_$UserDetailsStateImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/features/user_details/presentation/user_details_screen.dart b/lib/features/user_details/presentation/user_details_screen.dart new file mode 100644 index 00000000..2aa03504 --- /dev/null +++ b/lib/features/user_details/presentation/user_details_screen.dart @@ -0,0 +1,520 @@ +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/loader.dart'; +import 'package:eatvio_app/features/diets/presentation/widgets/more_options.dart'; +import 'package:eatvio_app/features/profile/models/circumference_model.dart'; +import 'package:eatvio_app/features/profile/models/weight_model.dart'; +import 'package:eatvio_app/features/profile/presentation/widgets/circumference_chart_widget.dart'; +import 'package:eatvio_app/features/profile/presentation/widgets/imt_chart_widget.dart'; +import 'package:eatvio_app/features/profile/presentation/widgets/weight_chart_widget.dart'; +import 'package:eatvio_app/features/user_details/presentation/cubit/user_details_cubit.dart'; +import 'package:eatvio_app/features/user_details/presentation/cubit/user_details_state.dart'; +import 'package:eatvio_app/features/user_details/presentation/widgets/expandable_bouble.dart'; +import 'package:eatvio_app/generated/l10n.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'; + +class UserDetailsScreen extends StatelessWidget { + final String slug; + const UserDetailsScreen({super.key, required this.slug}); + static const name = 'userDetailsScreen'; + static const path = '/user-details-screen'; + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => sl()..getUserDetails(slug), + child: UserDetailsContent( + slug: slug, + ), + ); + } +} + +class UserDetailsContent extends StatefulWidget { + const UserDetailsContent({super.key, required this.slug}); + final String slug; + @override + State createState() => _UserDetailsContentState(); +} + +class _UserDetailsContentState extends State { + final List chipsLabelList = ['Вес', 'Замеры', 'ИМТ']; + final List chipsIntervalLabelList = ['1М', '3М', '6М', '1Г', 'ВСЁ ВРЕМЯ']; + final List chipsIntervalList = ['1', '3', '6', '12', 'all']; + + List weightPoints = []; + List circumferencePoints = []; + int _selectedChipIndex = 0; + int _selectedIntervalChipIndex = 0; + bool isMoreOptionsVisible = false; + @override + Widget build(BuildContext context) { + final s = S.of(context); + return BlocBuilder( + builder: (context, state) { + final profileDetails = state.profileDetails; + if (profileDetails != null && state.isLoading == false) { + weightPoints = state.weightPoints ?? []; + circumferencePoints = state.circumferencePoints ?? []; + return Scaffold( + backgroundColor: appTheme.gray100, + appBar: AppBar( + centerTitle: true, + title: Text( + '${s.user_data} ${profileDetails.firstName} ${profileDetails.lastName}', + maxLines: 2, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + softWrap: true, + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), + ), + actions: [ + PortalTarget( + visible: isMoreOptionsVisible, + portalFollower: MoreOptions( + path: 'profile/${widget.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() + ..addListener(() { + setState(() { + isMoreOptionsVisible = false; + }); + }), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + spacing: 12, + children: [ + SizedBox.shrink(), + _charts(), + ExpandableBouble( + initiallyExpanded: true, + title: s.general_info, + children: [ + Text( + s.date_of_birth, + style: TextStyles.textSMedium.copyWith(color: appTheme.black), + ), + SizedBox(height: 4), + CustomTextFormField( + readOnly: true, + controller: TextEditingController(text: DateTime.parse(profileDetails.birthdate).format('dd.MM.yyyy')), + ), + SizedBox(height: 16), + Text( + s.sex, + style: TextStyles.textSMedium.copyWith(color: appTheme.black), + ), + SizedBox(height: 4), + CustomTextFormField( + readOnly: true, + controller: TextEditingController(text: profileDetails.gender.parseGender()), + ), + SizedBox(height: 16), + ], + ), + ExpandableBouble( + title: s.physical_fitness, + children: [ + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + s.height, + style: TextStyles.textSMedium.copyWith(color: appTheme.black), + ), + SizedBox(height: 4), + CustomTextFormField( + readOnly: true, + controller: TextEditingController(text: profileDetails.height.toString()), + ), + ], + ), + ), + SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + s.weight, + style: TextStyles.textSMedium.copyWith(color: appTheme.black), + ), + SizedBox(height: 4), + CustomTextFormField( + readOnly: true, + controller: TextEditingController(text: profileDetails.weight.toStringAsFixed(0)), + ), + ], + ), + ), + ], + ), + SizedBox(height: 16), + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + s.chest_circumference, + style: TextStyles.textSMedium.copyWith(color: appTheme.black), + ), + SizedBox(height: 4), + CustomTextFormField( + readOnly: true, + controller: TextEditingController(text: profileDetails.chestCircumference.toString()), + ), + ], + ), + ), + SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + s.waist_circumference, + style: TextStyles.textSMedium.copyWith(color: appTheme.black), + ), + SizedBox(height: 4), + CustomTextFormField( + readOnly: true, + controller: TextEditingController(text: profileDetails.waistCircumference.toString()), + ), + ], + ), + ), + SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + s.hip_circumference, + style: TextStyles.textSMedium.copyWith(color: appTheme.black), + ), + SizedBox(height: 4), + CustomTextFormField( + readOnly: true, + controller: TextEditingController(text: profileDetails.hipCircumference.toString()), + ), + ], + ), + ), + ], + ), + SizedBox(height: 16), + ], + ), + ExpandableBouble( + title: s.pressure, + children: [ + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + s.upper, + style: TextStyles.textSMedium.copyWith(color: appTheme.black), + ), + SizedBox(height: 4), + CustomTextFormField( + readOnly: true, + controller: TextEditingController(text: profileDetails.bloodPressureUp.toString()), + ), + ], + ), + ), + SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + s.lower, + style: TextStyles.textSMedium.copyWith(color: appTheme.black), + ), + SizedBox(height: 4), + CustomTextFormField( + readOnly: true, + controller: TextEditingController(text: profileDetails.bloodPressureDown.toStringAsFixed(0)), + ), + ], + ), + ), + ], + ), + SizedBox(height: 16), + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + s.has_hypo, + style: TextStyles.textSMedium.copyWith(color: appTheme.black), + ), + SizedBox(height: 4), + CustomTextFormField( + readOnly: true, + controller: TextEditingController(text: profileDetails.hasHypo.toString()), + ), + ], + ), + ), + SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + s.has_hyper, + style: TextStyles.textSMedium.copyWith(color: appTheme.black), + ), + SizedBox(height: 4), + CustomTextFormField( + readOnly: true, + controller: TextEditingController(text: profileDetails.hasHyper.toString()), + ), + ], + ), + ), + ], + ), + SizedBox(height: 16), + ], + ), + ExpandableBouble( + title: s.other_info, + children: [ + Text( + s.has_alergy, + style: TextStyles.textSMedium.copyWith(color: appTheme.black), + ), + SizedBox(height: 4), + CustomTextFormField( + readOnly: true, + controller: TextEditingController(text: profileDetails.aboutAlergy), + ), + SizedBox(height: 16), + Text( + s.takes_pills, + style: TextStyles.textSMedium.copyWith(color: appTheme.black), + ), + SizedBox(height: 4), + CustomTextFormField( + readOnly: true, + controller: TextEditingController(text: profileDetails.aboutTakingPils), + ), + SizedBox(height: 16), + Text( + s.sport_hobbies, + style: TextStyles.textSMedium.copyWith(color: appTheme.black), + ), + SizedBox(height: 4), + CustomTextFormField( + readOnly: true, + controller: TextEditingController(text: profileDetails.aboutSportHobbies), + ), + SizedBox(height: 16), + Text( + s.liked_groceries, + style: TextStyles.textSMedium.copyWith(color: appTheme.black), + ), + SizedBox(height: 4), + CustomTextFormField( + readOnly: true, + controller: TextEditingController(text: profileDetails.favoriteFoods), + ), + SizedBox(height: 16), + Text( + s.not_liked_groceries, + style: TextStyles.textSMedium.copyWith(color: appTheme.black), + ), + SizedBox(height: 4), + CustomTextFormField( + readOnly: true, + controller: TextEditingController(text: profileDetails.unfavoriteFoods), + ), + SizedBox(height: 16), + Text( + s.additional_information, + style: TextStyles.textSMedium.copyWith(color: appTheme.black), + ), + SizedBox(height: 4), + SizedBox( + height: 112, + child: CustomTextFormField( + readOnly: true, + maxLength: 1000, + maxLines: null, + minLines: null, + expands: true, + textAlignVertical: TextAlignVertical.top, + controller: TextEditingController(text: profileDetails.additionalInfo), + ), + ), + SizedBox(height: 16), + ], + ), + ], + ), + ), + ); + } else { + return Scaffold( + body: Loader(), + ); + } + }, + ); + } + + _charts() { + return Material( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 8), + SingleChildScrollView( + padding: EdgeInsets.symmetric(horizontal: 16), + scrollDirection: Axis.horizontal, + child: Row( + children: [ + _choiceChipWidget(0), + SizedBox(width: 8), + _choiceChipWidget(1), + SizedBox(width: 8), + _choiceChipWidget(2), + ], + ), + ), + Builder( + builder: (context) { + switch (_selectedChipIndex) { + case 0: + return WeightChartWidget( + weightPoints: weightPoints, + ); + case 1: + return CircumferenceChartWidget( + circumferencePoints: circumferencePoints, + ); + case 2: + return ImtChartWidget( + weightPoints: weightPoints, + ); + default: + return SizedBox.shrink(); + } + }, + ), + SizedBox(height: 8), + Row( + children: [ + SizedBox(width: 16), + _choiceIntervalChipWidget(0), + SizedBox(width: 8), + _choiceIntervalChipWidget(1), + SizedBox(width: 8), + _choiceIntervalChipWidget(2), + SizedBox(width: 8), + _choiceIntervalChipWidget(3), + SizedBox(width: 8), + _choiceIntervalChipWidget(4), + ], + ), + SizedBox(height: 8), + ], + ), + ); + } + + _choiceIntervalChipWidget(int index) { + return ChoiceChip( + selectedColor: Colors.grey.withValues(alpha: 0.1), + pressElevation: 0, + showCheckmark: false, + side: BorderSide.none, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + label: Text(chipsIntervalLabelList[index]), + labelStyle: TextStyle( + color: appTheme.gray600, + fontWeight: FontWeight.w500, + fontSize: 12, + ), + selected: _selectedIntervalChipIndex == index, + onSelected: (bool selected) { + context.read().getChartData(period: chipsIntervalList[index], slug: widget.slug); + setState(() { + _selectedIntervalChipIndex = index; + }); + }, + ); + } + + _choiceChipWidget(int index) { + return ChoiceChip( + selectedColor: appTheme.blueA400, + pressElevation: 0, + showCheckmark: false, + side: BorderSide.none, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + label: Text(chipsLabelList[index]), + labelStyle: TextStyle( + color: index == _selectedChipIndex ? Colors.white : appTheme.gray600, + fontWeight: FontWeight.w500, + fontSize: 12, + ), + selected: _selectedChipIndex == index, + backgroundColor: appTheme.gray100, + onSelected: (bool selected) { + setState(() { + _selectedChipIndex = index; + }); + }, + ); + } +} diff --git a/lib/features/user_details/presentation/widgets/expandable_bouble.dart b/lib/features/user_details/presentation/widgets/expandable_bouble.dart new file mode 100644 index 00000000..e5303a06 --- /dev/null +++ b/lib/features/user_details/presentation/widgets/expandable_bouble.dart @@ -0,0 +1,34 @@ +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:flutter/material.dart'; + +class ExpandableBouble extends StatelessWidget { + const ExpandableBouble({super.key, required this.title, required this.children, this.initiallyExpanded = false}); + final String title; + final List children; + final bool initiallyExpanded; + @override + Widget build(BuildContext context) { + return AnimatedSize( + duration: Duration(seconds: 1), + child: MainBackgroundBouble( + child: Theme( + data: Theme.of(context).copyWith(dividerColor: Colors.transparent), + child: ExpansionTile( + initiallyExpanded: initiallyExpanded, + expandedCrossAxisAlignment: CrossAxisAlignment.start, + expandedAlignment: Alignment.bottomLeft, + tilePadding: EdgeInsets.symmetric(horizontal: 16), + childrenPadding: EdgeInsets.symmetric(horizontal: 16), + title: Text( + title, + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), + ), + children: children, + ), + ), + ), + ); + } +} diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart index 0dc19f85..a894ea49 100644 --- a/lib/generated/intl/messages_en.dart +++ b/lib/generated/intl/messages_en.dart @@ -88,11 +88,12 @@ class MessageLookup extends MessageLookupByLibrary { "date_of_birth": MessageLookupByLibrary.simpleMessage("Дата рождения"), "day": MessageLookupByLibrary.simpleMessage("День"), "days": MessageLookupByLibrary.simpleMessage("дней"), + "deleted": MessageLookupByLibrary.simpleMessage("Удалена"), "diary": MessageLookupByLibrary.simpleMessage("Дневник"), + "diet": MessageLookupByLibrary.simpleMessage("Диета"), "diet_author": MessageLookupByLibrary.simpleMessage("Автор диеты"), "diet_plan": MessageLookupByLibrary.simpleMessage("План диеты"), "diet_state": MessageLookupByLibrary.simpleMessage("Статус диеты"), - "diet_time": MessageLookupByLibrary.simpleMessage("Время прохождения"), "diet_type": MessageLookupByLibrary.simpleMessage("Тип диеты"), "do_you_have_hypertension_tendency": MessageLookupByLibrary.simpleMessage("Есть ли у вас гипертония?"), @@ -103,6 +104,7 @@ class MessageLookup extends MessageLookupByLibrary { "down_pressure": MessageLookupByLibrary.simpleMessage("Нижние показатели давления:"), "draft": MessageLookupByLibrary.simpleMessage("Черновик"), + "drafted": MessageLookupByLibrary.simpleMessage("Черновики"), "eating_behavior": MessageLookupByLibrary.simpleMessage("Пищевое поведение"), "eating_behavior_type": MessageLookupByLibrary.simpleMessage( @@ -110,6 +112,7 @@ class MessageLookup extends MessageLookupByLibrary { "eating_behavior_type_desc": MessageLookupByLibrary.simpleMessage( "Формирование здорового пищевого поведения — это процесс, который включает в себя изменение привычек и отношения к еде. Он направлен на то, чтобы помочь людям научиться правильно питаться и поддерживать здоровый образ жизни.Основные принципы формирования здорового пищевого поведения: \n● Разнообразие продуктов; \n● Сбалансированный рацион; \n● Питьевой режим; \n● Ограничение вредных продуктов; \n● Регулярные приёмы пищи; \n● Умеренность;Осознанное питание; \n● Физическая активность;"), "economy": MessageLookupByLibrary.simpleMessage("Эконом"), + "edit": MessageLookupByLibrary.simpleMessage("Редактировать"), "email": MessageLookupByLibrary.simpleMessage("Элетронная почта"), "email_empty": MessageLookupByLibrary.simpleMessage( "Необходимо ввести адрес электронной почты"), @@ -143,11 +146,20 @@ class MessageLookup extends MessageLookupByLibrary { "fit_type_desc": MessageLookupByLibrary.simpleMessage( "План питания для поддержания формы сбалансированный и разнообразный, чтобы обеспечить организм всеми необходимыми питательными веществами. Общие принципы: \n● Дробное питание; \n● Разнообразие продуктов; \n● Контроль порций; \n● Питьевой режим; \n● Ограничение вредных продуктов;"), "fitness": MessageLookupByLibrary.simpleMessage("Поддержка формы"), + "from": MessageLookupByLibrary.simpleMessage("от"), "from_a_to_z": MessageLookupByLibrary.simpleMessage("Название: А-Я"), "from_z_to_a": MessageLookupByLibrary.simpleMessage("Название: Я-А"), "gallery": MessageLookupByLibrary.simpleMessage("Галерея"), + "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("Свернуть"), @@ -169,9 +181,12 @@ class MessageLookup extends MessageLookupByLibrary { "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("Мясо"), @@ -188,6 +203,8 @@ class MessageLookup extends MessageLookupByLibrary { "msg_something_went_wrong": MessageLookupByLibrary.simpleMessage("Что то пошло не так!"), "my_blog": MessageLookupByLibrary.simpleMessage("Мой блог"), + "my_diets": MessageLookupByLibrary.simpleMessage("Мои диеты"), + "my_plans": MessageLookupByLibrary.simpleMessage("Мои планы"), "my_settings": MessageLookupByLibrary.simpleMessage("Мои настройки"), "name_empty": MessageLookupByLibrary.simpleMessage( "Имя пользователя обязательно для заполнения"), @@ -196,6 +213,8 @@ class MessageLookup extends MessageLookupByLibrary { "no": MessageLookupByLibrary.simpleMessage("Нет"), "no_account": MessageLookupByLibrary.simpleMessage("У меня нет аккаунта"), + "not_liked_groceries": + MessageLookupByLibrary.simpleMessage("Не любимые продукты"), "one_option": MessageLookupByLibrary.simpleMessage("1 опция"), "option": MessageLookupByLibrary.simpleMessage("Опция"), "option_content": @@ -211,6 +230,7 @@ class MessageLookup extends MessageLookupByLibrary { "Опция подразумевает создание индивидуальной диеты"), "option_title": MessageLookupByLibrary.simpleMessage("Заголовок опции"), "order": MessageLookupByLibrary.simpleMessage("Порядок"), + "other_info": MessageLookupByLibrary.simpleMessage("Прочая информация"), "password": MessageLookupByLibrary.simpleMessage("Пароль"), "password_confirmation": MessageLookupByLibrary.simpleMessage("Подтвердите пароль"), @@ -221,9 +241,12 @@ class MessageLookup extends MessageLookupByLibrary { "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("Автор плана"), @@ -233,42 +256,58 @@ class MessageLookup extends MessageLookupByLibrary { "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("Имя пользователя"), diff --git a/lib/generated/intl/messages_ru.dart b/lib/generated/intl/messages_ru.dart index e1e70e28..328f6779 100644 --- a/lib/generated/intl/messages_ru.dart +++ b/lib/generated/intl/messages_ru.dart @@ -45,13 +45,13 @@ class MessageLookup extends MessageLookupByLibrary { "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("Грудь"), @@ -62,11 +62,11 @@ class MessageLookup extends MessageLookupByLibrary { "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("Город"), @@ -88,35 +88,38 @@ class MessageLookup extends MessageLookupByLibrary { "date_of_birth": MessageLookupByLibrary.simpleMessage("Дата рождения"), "day": MessageLookupByLibrary.simpleMessage("День"), "days": MessageLookupByLibrary.simpleMessage("дней"), + "deleted": MessageLookupByLibrary.simpleMessage("Удалена"), "diary": MessageLookupByLibrary.simpleMessage("Дневник"), + "diet": MessageLookupByLibrary.simpleMessage("Диета"), "diet_author": MessageLookupByLibrary.simpleMessage("Автор диеты"), "diet_plan": MessageLookupByLibrary.simpleMessage("План диеты"), "diet_state": MessageLookupByLibrary.simpleMessage("Статус диеты"), - "diet_time": 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● Физическая активность;",), + "Формирование здорового пищевого поведения — это процесс, который включает в себя изменение привычек и отношения к еде. Он направлен на то, чтобы помочь людям научиться правильно питаться и поддерживать здоровый образ жизни.Основные принципы формирования здорового пищевого поведения: \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": @@ -129,20 +132,21 @@ class MessageLookup extends MessageLookupByLibrary { "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 символ",), + "В имени должен быть минимум 1 символ"), "fit_type": MessageLookupByLibrary.simpleMessage("Поддержание формы"), "fit_type_desc": MessageLookupByLibrary.simpleMessage( - "План питания для поддержания формы сбалансированный и разнообразный, чтобы обеспечить организм всеми необходимыми питательными веществами. Общие принципы: \n● Дробное питание; \n● Разнообразие продуктов; \n● Контроль порций; \n● Питьевой режим; \n● Ограничение вредных продуктов;",), + "План питания для поддержания формы сбалансированный и разнообразный, чтобы обеспечить организм всеми необходимыми питательными веществами. Общие принципы: \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("Галерея"), @@ -162,13 +166,13 @@ class MessageLookup extends MessageLookupByLibrary { "language": MessageLookupByLibrary.simpleMessage("Язык"), "last_name": MessageLookupByLibrary.simpleMessage("Фамилия"), "last_name_empty": MessageLookupByLibrary.simpleMessage( - "Фамилия обязательна для заполнения",), + "Фамилия обязательна для заполнения"), "last_name_short": MessageLookupByLibrary.simpleMessage( - "В фамилии должен быть минимум 6 символов",), + "В фамилии должен быть минимум 6 символов"), "lbl_english": MessageLookupByLibrary.simpleMessage("English"), "light_type": MessageLookupByLibrary.simpleMessage("Легкая"), "light_type_desc": MessageLookupByLibrary.simpleMessage( - "С некоторыми видами диет можно употреблять алкоголь в умеренных количествах. Рекомендуется совмещать вместе с низкокалорийными закусками.",), + "С некоторыми видами диет можно употреблять алкоголь в умеренных количествах. Рекомендуется совмещать вместе с низкокалорийными закусками."), "link_was_copied": MessageLookupByLibrary.simpleMessage("Ссылка успешно скопирована"), "loading": MessageLookupByLibrary.simpleMessage("Загрузка..."), @@ -177,10 +181,10 @@ class MessageLookup extends MessageLookupByLibrary { "meat": MessageLookupByLibrary.simpleMessage("Мясо"), "meat_type": MessageLookupByLibrary.simpleMessage("С мясом"), "meat_type_desc": MessageLookupByLibrary.simpleMessage( - "Диета с мясом полезная и сбалансированная. Включает в себя нежирное мясо: курицу, индейку, кролика или говядину. В сочетании с овощами получаете все необходимые витамины и минералы. Не забывайте о порциях и старайтесь есть медленно, прислушиваясь к своему организму.",), + "Диета с мясом полезная и сбалансированная. Включает в себя нежирное мясо: курицу, индейку, кролика или говядину. В сочетании с овощами получаете все необходимые витамины и минералы. Не забывайте о порциях и старайтесь есть медленно, прислушиваясь к своему организму."), "milliliter": MessageLookupByLibrary.simpleMessage("мл"), "minimal_customer_price": MessageLookupByLibrary.simpleMessage( - "Минимальная цена для покупателя: 1200₽",), + "Минимальная цена для покупателя: 1200₽"), "most_new": MessageLookupByLibrary.simpleMessage("Самые новые"), "most_old": MessageLookupByLibrary.simpleMessage("Самые старые"), "msg_network_err": @@ -188,11 +192,13 @@ class MessageLookup extends MessageLookupByLibrary { "msg_something_went_wrong": MessageLookupByLibrary.simpleMessage("Что то пошло не так!"), "my_blog": MessageLookupByLibrary.simpleMessage("Мой блог"), + "my_diets": MessageLookupByLibrary.simpleMessage("Мои диеты"), + "my_plans": MessageLookupByLibrary.simpleMessage("Мои планы"), "my_settings": MessageLookupByLibrary.simpleMessage("Мои настройки"), "name_empty": MessageLookupByLibrary.simpleMessage( - "Имя пользователя обязательно для заполнения",), + "Имя пользователя обязательно для заполнения"), "name_short": MessageLookupByLibrary.simpleMessage( - "В имени пользователя должно быть минимум 6 символов",), + "В имени пользователя должно быть минимум 6 символов"), "no": MessageLookupByLibrary.simpleMessage("Нет"), "no_account": MessageLookupByLibrary.simpleMessage("У меня нет аккаунта"), @@ -204,11 +210,11 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("На связи 7 дней в неделю"), "option_desc": MessageLookupByLibrary.simpleMessage("Описание опции"), "option_desc_hint_text": MessageLookupByLibrary.simpleMessage( - "Этот план питания для похудения на 7 дней сбалансированный и разнообразный",), + "Этот план питания для похудения на 7 дней сбалансированный и разнообразный"), "option_duration": MessageLookupByLibrary.simpleMessage("Продолжительность опции"), "option_personal_diet_desc": MessageLookupByLibrary.simpleMessage( - "Опция подразумевает создание индивидуальной диеты",), + "Опция подразумевает создание индивидуальной диеты"), "option_title": MessageLookupByLibrary.simpleMessage("Заголовок опции"), "order": MessageLookupByLibrary.simpleMessage("Порядок"), "password": MessageLookupByLibrary.simpleMessage("Пароль"), @@ -217,10 +223,11 @@ class MessageLookup extends MessageLookupByLibrary { "password_confirmation_mismatch": MessageLookupByLibrary.simpleMessage("Пароли не совпадают"), "password_empty": MessageLookupByLibrary.simpleMessage( - "Пароль обязателен для заполнения",), + "Пароль обязателен для заполнения"), "password_short": MessageLookupByLibrary.simpleMessage( - "В пароле должно быть минимум 6 символов",), + "В пароле должно быть минимум 6 символов"), "pcs": MessageLookupByLibrary.simpleMessage("шт"), + "personal": MessageLookupByLibrary.simpleMessage("Персональные"), "personal_diet_state": MessageLookupByLibrary.simpleMessage("Персональная"), "photo": MessageLookupByLibrary.simpleMessage("Фото"), @@ -229,7 +236,7 @@ class MessageLookup extends MessageLookupByLibrary { "plan_author": MessageLookupByLibrary.simpleMessage("Автор плана"), "plan_desc": MessageLookupByLibrary.simpleMessage("Описание плана"), "plan_desc_example": MessageLookupByLibrary.simpleMessage( - "Большое подробное, привлекательное описание плана",), + "Большое подробное, привлекательное описание плана"), "plan_duration": MessageLookupByLibrary.simpleMessage("Срок плана"), "plan_label_desc": MessageLookupByLibrary.simpleMessage("План для поддержания формы"), @@ -239,15 +246,19 @@ class MessageLookup extends MessageLookupByLibrary { "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("Настройки"), @@ -256,26 +267,29 @@ class MessageLookup extends MessageLookupByLibrary { "slim": MessageLookupByLibrary.simpleMessage("Похудение"), "slim_type": MessageLookupByLibrary.simpleMessage("Похудение"), "slim_type_desc": MessageLookupByLibrary.simpleMessage( - "План питания для похудения должен быть разработан с учётом индивидуальных особенностей организма, возраста, пола, уровня физической активности и состояния здоровья. Общие принципы: \n● Снижение калорийности; \n● Сбалансированный рацион; \n● Разнообразие продуктов; \n● Питьевой режим; \n● Ограничение вредных продуктов;",), + "План питания для похудения должен быть разработан с учётом индивидуальных особенностей организма, возраста, пола, уровня физической активности и состояния здоровья. Общие принципы: \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("Обхват талии"), @@ -288,11 +302,11 @@ class MessageLookup extends MessageLookupByLibrary { "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("Ваше артериальное давление?"), @@ -301,6 +315,6 @@ class MessageLookup extends MessageLookupByLibrary { "your_gender": MessageLookupByLibrary.simpleMessage("Ваш пол?"), "your_height": MessageLookupByLibrary.simpleMessage("Ваш рост:"), "your_schedule": MessageLookupByLibrary.simpleMessage("Ваш режим дня?"), - "your_weight": MessageLookupByLibrary.simpleMessage("Ваш вес:"), + "your_weight": MessageLookupByLibrary.simpleMessage("Ваш вес:") }; } diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index f4c76c59..bd4faecc 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -1381,10 +1381,10 @@ class S { } /// `Время прохождения` - String get diet_time { + String get time { return Intl.message( 'Время прохождения', - name: 'diet_time', + name: 'time', desc: '', args: [], ); @@ -1410,6 +1410,16 @@ class S { ); } + /// `Тип плана` + String get plan_type { + return Intl.message( + 'Тип плана', + name: 'plan_type', + desc: '', + args: [], + ); + } + /// `Популярные` String get popular { return Intl.message( @@ -2039,6 +2049,286 @@ class S { args: [], ); } + + /// `Цена` + String get price { + return Intl.message( + 'Цена', + name: 'price', + desc: '', + args: [], + ); + } + + /// `Мои диеты` + String get my_diets { + return Intl.message( + 'Мои диеты', + name: 'my_diets', + desc: '', + args: [], + ); + } + + /// `Опубликованные` + String get published { + return Intl.message( + 'Опубликованные', + name: 'published', + desc: '', + args: [], + ); + } + + /// `Черновики` + String get drafted { + return Intl.message( + 'Черновики', + name: 'drafted', + desc: '', + args: [], + ); + } + + /// `Персональные` + String get personal { + return Intl.message( + 'Персональные', + name: 'personal', + desc: '', + args: [], + ); + } + + /// `Редактировать` + String get edit { + return Intl.message( + 'Редактировать', + name: 'edit', + desc: '', + args: [], + ); + } + + /// `Предложить` + String get toOffer { + return Intl.message( + 'Предложить', + name: 'toOffer', + desc: '', + args: [], + ); + } + + /// `Снять с публикации` + String get removeFromActive { + return Intl.message( + 'Снять с публикации', + name: 'removeFromActive', + desc: '', + args: [], + ); + } + + /// `Диета` + String get diet { + return Intl.message( + 'Диета', + name: 'diet', + desc: '', + args: [], + ); + } + + /// `Удалена` + String get deleted { + return Intl.message( + 'Удалена', + name: 'deleted', + desc: '', + args: [], + ); + } + + /// `Мои планы` + String get my_plans { + return Intl.message( + 'Мои планы', + name: 'my_plans', + desc: '', + args: [], + ); + } + + /// `от` + String get from { + return Intl.message( + 'от', + name: 'from', + desc: '', + args: [], + ); + } + + /// `Данные пользователя` + String get user_data { + return Intl.message( + 'Данные пользователя', + name: 'user_data', + desc: '', + args: [], + ); + } + + /// `Общая информация` + String get general_info { + return Intl.message( + 'Общая информация', + name: 'general_info', + desc: '', + args: [], + ); + } + + /// `Пол` + String get sex { + return Intl.message( + 'Пол', + name: 'sex', + desc: '', + args: [], + ); + } + + /// `Физическая форма` + String get physical_fitness { + return Intl.message( + 'Физическая форма', + name: 'physical_fitness', + desc: '', + args: [], + ); + } + + /// `Рост` + String get height { + return Intl.message( + 'Рост', + name: 'height', + desc: '', + args: [], + ); + } + + /// `Давление` + String get pressure { + return Intl.message( + 'Давление', + name: 'pressure', + desc: '', + args: [], + ); + } + + /// `Верхнее` + String get upper { + return Intl.message( + 'Верхнее', + name: 'upper', + desc: '', + args: [], + ); + } + + /// `Нижнее` + String get lower { + return Intl.message( + 'Нижнее', + name: 'lower', + desc: '', + args: [], + ); + } + + /// `Склонность к гипотонии` + String get has_hypo { + return Intl.message( + 'Склонность к гипотонии', + name: 'has_hypo', + desc: '', + args: [], + ); + } + + /// `Склонность к гипертонии` + String get has_hyper { + return Intl.message( + 'Склонность к гипертонии', + name: 'has_hyper', + desc: '', + args: [], + ); + } + + /// `Прочая информация` + String get other_info { + return Intl.message( + 'Прочая информация', + name: 'other_info', + desc: '', + args: [], + ); + } + + /// `Наличие аллергии` + String get has_alergy { + return Intl.message( + 'Наличие аллергии', + name: 'has_alergy', + desc: '', + args: [], + ); + } + + /// `Принимает лекарства` + String get takes_pills { + return Intl.message( + 'Принимает лекарства', + name: 'takes_pills', + desc: '', + args: [], + ); + } + + /// `Спортивные увлечения` + String get sport_hobbies { + return Intl.message( + 'Спортивные увлечения', + name: 'sport_hobbies', + desc: '', + args: [], + ); + } + + /// `Любимые продукты` + String get liked_groceries { + return Intl.message( + 'Любимые продукты', + name: 'liked_groceries', + desc: '', + args: [], + ); + } + + /// `Не любимые продукты` + String get not_liked_groceries { + return Intl.message( + 'Не любимые продукты', + name: 'not_liked_groceries', + desc: '', + args: [], + ); + } } class AppLocalizationDelegate extends LocalizationsDelegate { diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index bef56ef4..355d15b2 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -132,9 +132,10 @@ "choose_start_diet_date": "Выберите дату старты диеты. Она отобразиться у Вас в дневнике", "continue_": "Продолжить", "order": "Порядок", - "diet_time": "Время прохождения", + "time": "Время прохождения", "all": "Все", "diet_type": "Тип диеты", + "plan_type": "Тип плана", "popular": "Популярные", "most_new": "Самые новые", "most_old": "Самые старые", @@ -197,5 +198,33 @@ "wellness_type": "Улучшение самочувствия", "wellness_type_desc": "Если вы любите мучные продукты, но хотите придерживаться диеты, вам подходит диета с добавленим мучных блюд на основе цельнозерновых вариантов. В них больше сложных углеводов и клетчатки, что способствует длительному чувству сытости и поддерживает здоровье пищеварительной системы.", "eating_behavior_type": "Формирование пищевого поведения", - "eating_behavior_type_desc": "Формирование здорового пищевого поведения — это процесс, который включает в себя изменение привычек и отношения к еде. Он направлен на то, чтобы помочь людям научиться правильно питаться и поддерживать здоровый образ жизни.Основные принципы формирования здорового пищевого поведения: \n● Разнообразие продуктов; \n● Сбалансированный рацион; \n● Питьевой режим; \n● Ограничение вредных продуктов; \n● Регулярные приёмы пищи; \n● Умеренность;Осознанное питание; \n● Физическая активность;" + "eating_behavior_type_desc": "Формирование здорового пищевого поведения — это процесс, который включает в себя изменение привычек и отношения к еде. Он направлен на то, чтобы помочь людям научиться правильно питаться и поддерживать здоровый образ жизни.Основные принципы формирования здорового пищевого поведения: \n● Разнообразие продуктов; \n● Сбалансированный рацион; \n● Питьевой режим; \n● Ограничение вредных продуктов; \n● Регулярные приёмы пищи; \n● Умеренность;Осознанное питание; \n● Физическая активность;", + "price": "Цена", + "my_diets": "Мои диеты", + "published": "Опубликованные", + "drafted": "Черновики", + "personal": "Персональные", + "edit": "Редактировать", + "toOffer": "Предложить", + "removeFromActive": "Снять с публикации", + "diet": "Диета", + "deleted": "Удалена", + "my_plans": "Мои планы", + "from": "от", + "user_data": "Данные пользователя", + "general_info": "Общая информация", + "sex": "Пол", + "physical_fitness": "Физическая форма", + "height": "Рост", + "pressure": "Давление", + "upper": "Верхнее", + "lower": "Нижнее", + "has_hypo": "Склонность к гипотонии", + "has_hyper": "Склонность к гипертонии", + "other_info": "Прочая информация", + "has_alergy": "Наличие аллергии", + "takes_pills": "Принимает лекарства", + "sport_hobbies": "Спортивные увлечения", + "liked_groceries": "Любимые продукты", + "not_liked_groceries": "Не любимые продукты" } diff --git a/lib/l10n/intl_ru.arb b/lib/l10n/intl_ru.arb index b5f2d8a3..a1d3cd53 100644 --- a/lib/l10n/intl_ru.arb +++ b/lib/l10n/intl_ru.arb @@ -197,5 +197,18 @@ "wellness_type": "Улучшение самочувствия", "wellness_type_desc": "Если вы любите мучные продукты, но хотите придерживаться диеты, вам подходит диета с добавленим мучных блюд на основе цельнозерновых вариантов. В них больше сложных углеводов и клетчатки, что способствует длительному чувству сытости и поддерживает здоровье пищеварительной системы.", "eating_behavior_type": "Формирование пищевого поведения", - "eating_behavior_type_desc": "Формирование здорового пищевого поведения — это процесс, который включает в себя изменение привычек и отношения к еде. Он направлен на то, чтобы помочь людям научиться правильно питаться и поддерживать здоровый образ жизни.Основные принципы формирования здорового пищевого поведения: \n● Разнообразие продуктов; \n● Сбалансированный рацион; \n● Питьевой режим; \n● Ограничение вредных продуктов; \n● Регулярные приёмы пищи; \n● Умеренность;Осознанное питание; \n● Физическая активность;" + "eating_behavior_type_desc": "Формирование здорового пищевого поведения — это процесс, который включает в себя изменение привычек и отношения к еде. Он направлен на то, чтобы помочь людям научиться правильно питаться и поддерживать здоровый образ жизни.Основные принципы формирования здорового пищевого поведения: \n● Разнообразие продуктов; \n● Сбалансированный рацион; \n● Питьевой режим; \n● Ограничение вредных продуктов; \n● Регулярные приёмы пищи; \n● Умеренность;Осознанное питание; \n● Физическая активность;", + "price": "Цена", + "my_diets": "Мои диеты", + "published": "Опубликованные", + "drafted": "Черновики", + "personal": "Персональные", + "edit": "Редактировать", + "toOffer": "Предложить", + "removeFromActive": "Снять с публикации", + "diet": "Диета", + "deleted": "Удалена", + "my_plans": "Мои планы", + "from": "от", + "user_data": "Данные пользователя" }