From 27241aebacb7b17a813bf53b8d8ac8e1db7cd93e Mon Sep 17 00:00:00 2001 From: Username1548 Date: Mon, 16 Dec 2024 20:31:23 +0700 Subject: [PATCH] plans filters ready --- lib/core/app_router.dart | 2 +- lib/core/const.dart | 3 +- lib/core/locator_service.dart | 2 +- lib/core/models/pagination_model.dart | 2 + lib/core/models/pagination_model.freezed.dart | 64 ++- lib/core/models/pagination_model.g.dart | 4 + lib/core/utils/date_time_utils.dart | 16 +- lib/core/widgets/custom_text_form_field.dart | 7 +- .../presentation/cubit/create_diet_state.dart | 2 +- .../widgets/diet_type_choice_widget.dart | 9 +- .../presentation/cubit/about_diet_cubit.dart | 25 +- .../presentation/pages/about_diet_screen.dart | 5 +- .../presentation/widgets/author_card.dart | 3 +- .../presentation/widgets/expandable_text.dart | 61 +-- .../diets/repositories/diet_repository.dart | 26 +- .../feed/models/feed_blog_post_model.dart | 6 +- .../models/feed_blog_post_model.freezed.dart | 30 +- .../feed/models/feed_blog_post_model.g.dart | 4 +- .../feed/models/plan_pagination_model.dart | 1 + .../models/plan_pagination_model.freezed.dart | 70 ++-- .../feed/models/plan_pagination_model.g.dart | 2 + .../bloc/diets_cubit/diets_cubit.dart | 39 +- .../bloc/plans_cubit/plans_cubit.dart | 221 ++++++++++- .../bloc/plans_cubit/plans_state.dart | 21 +- .../bloc/plans_cubit/plans_state.freezed.dart | 279 ++++++++++++- .../feed/presentation/pages/feed_screen.dart | 367 ++++++++++-------- .../widgets/blog_post_widget.dart | 3 +- .../widgets/diet_filter_bottom_sheet.dart | 38 +- .../duration_choice_filter_widget.dart | 161 +++++--- .../presentation/widgets/filter_badge.dart | 12 +- .../presentation/widgets/plan_widget.dart | 284 +++++++------- .../widgets/plans_filter_bottom_sheet.dart | 74 ++++ .../widgets/price_choice_filter_widget.dart | 183 +++++++++ .../widgets/type_choice_filter_widget.dart | 219 +++++++---- .../feed/repositories/feed_repository.dart | 8 +- lib/features/feed/services/feed_service.dart | 5 + .../feed/services/feed_service.g.dart | 19 +- .../presentation/cubit/about_plan_cubit.dart | 104 +++-- .../presentation/pages/about_plan_screen.dart | 19 +- .../plans/repositories/plan_repository.dart | 21 +- .../bloc/profile_cubit/profile_cubit.dart | 4 +- .../presentation/pages/profile_screen.dart | 2 +- lib/generated/intl/messages_en.dart | 4 +- lib/generated/l10n.dart | 24 +- lib/l10n/intl_en.arb | 8 +- 45 files changed, 1806 insertions(+), 657 deletions(-) create mode 100644 lib/features/feed/presentation/widgets/plans_filter_bottom_sheet.dart create mode 100644 lib/features/feed/presentation/widgets/price_choice_filter_widget.dart diff --git a/lib/core/app_router.dart b/lib/core/app_router.dart index 899804f1..f939208a 100644 --- a/lib/core/app_router.dart +++ b/lib/core/app_router.dart @@ -116,7 +116,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, ); }, diff --git a/lib/core/const.dart b/lib/core/const.dart index f7dba8dd..273ac73d 100644 --- a/lib/core/const.dart +++ b/lib/core/const.dart @@ -2,4 +2,5 @@ const 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 7bc58da5..4b2106f8 100644 --- a/lib/core/locator_service.dart +++ b/lib/core/locator_service.dart @@ -79,7 +79,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())); 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/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/widgets/custom_text_form_field.dart b/lib/core/widgets/custom_text_form_field.dart index b9588f68..f343e996 100644 --- a/lib/core/widgets/custom_text_form_field.dart +++ b/lib/core/widgets/custom_text_form_field.dart @@ -40,6 +40,7 @@ class CustomTextFormField extends StatelessWidget { this.counter = const SizedBox.shrink(), super.key, this.hasError = false, + this.readOnly = false, }); final Widget? counter; final Alignment? alignment; @@ -76,16 +77,18 @@ class CustomTextFormField extends StatelessWidget { final bool expands; final TextAlignVertical? textAlignVertical; final bool hasError; + final bool readOnly; @override Widget build(BuildContext context) { return Align( alignment: alignment ?? Alignment.center, - child: TextFormField( + child: TextFormField( + readOnly: readOnly, textAlignVertical: textAlignVertical, expands: expands, minLines: minLines, maxLength: maxLength, - onChanged: onChanged, + onChanged: onChanged, controller: controller, focusNode: focusNode ?? null, autofocus: autofocus, 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/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..2e31c681 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) { @@ -42,7 +42,7 @@ class AboutDietCubit extends Cubit { totalLikes: currentDiet.totalLikes - 1, wasLikedByMe: false, ); - dietsRepository.emitLikesStateToFeed(DietLikesState(wasLikedByMe: updatedDiet.wasLikedByMe, totalLikes: updatedDiet.totalLikes, index: dietIndexInFeed)); + dietsRepository.emitLikesStateToFeed(LikesState(wasLikedByMe: updatedDiet.wasLikedByMe, totalLikes: updatedDiet.totalLikes, index: dietIndexInFeed)); emit(AboutDietState.loaded(updatedDiet)); } else { final updatedDiet = currentDiet.copyWith( @@ -50,7 +50,7 @@ class AboutDietCubit extends Cubit { wasLikedByMe: true, ); - dietsRepository.emitLikesStateToFeed(DietLikesState(wasLikedByMe: updatedDiet.wasLikedByMe, totalLikes: updatedDiet.totalLikes, index: dietIndexInFeed)); + dietsRepository.emitLikesStateToFeed(LikesState(wasLikedByMe: updatedDiet.wasLikedByMe, totalLikes: updatedDiet.totalLikes, index: dietIndexInFeed)); emit(AboutDietState.loaded(updatedDiet)); } @@ -62,15 +62,14 @@ class AboutDietCubit extends Cubit { await dietsRepository.likeDiet(id); } catch (e) { log(e.toString()); - dietsRepository - .emitLikesStateToFeed(DietLikesState(wasLikedByMe: currentDiet.wasLikedByMe, totalLikes: currentDiet.totalLikes, index: dietIndexInFeed)); + 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 +77,11 @@ class AboutDietCubit extends Cubit { final updatedDiet = currentDiet.copyWith( isFavorite: true, ); - dietsRepository.emitFavoriteStateToFeed(DietFavoriteState(isFavorite: updatedDiet.isFavorite, index: dietIndexInFeed)); + 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 +89,14 @@ class AboutDietCubit extends Cubit { } catch (e) { log(e.toString()); showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время'); - dietsRepository.emitFavoriteStateToFeed(DietFavoriteState(isFavorite: currentDiet.isFavorite, index: dietIndexInFeed)); + 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 +104,11 @@ class AboutDietCubit extends Cubit { final updatedDiet = currentDiet.copyWith( isFavorite: false, ); - dietsRepository.emitFavoriteStateToFeed(DietFavoriteState(isFavorite: updatedDiet.isFavorite, index: dietIndexInFeed)); + 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 +116,7 @@ class AboutDietCubit extends Cubit { } catch (e) { log(e.toString()); showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время'); - dietsRepository.emitFavoriteStateToFeed(DietFavoriteState(isFavorite: currentDiet.isFavorite, index: dietIndexInFeed)); + 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 405e39b0..0ff130b0 100644 --- a/lib/features/diets/presentation/pages/about_diet_screen.dart +++ b/lib/features/diets/presentation/pages/about_diet_screen.dart @@ -128,7 +128,7 @@ class _AboutDietScreenState extends State { elevation: 0, focusElevation: 0, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), + borderRadius: BorderRadius.circular(20), ), backgroundColor: appTheme.greenA700, onPressed: () { @@ -159,7 +159,8 @@ class _AboutDietScreenState extends State { width: 52, height: 52, size: 24, - shape: BoxShape.circle, + shape: BoxShape.rectangle, + borderRadius: BorderRadius.circular(20), color: appTheme.gray50, ), ], 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 c2decf4c..44a16b0c 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.withOpacity(0.0), - Colors.white.withOpacity(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.withOpacity(0.0), + Colors.white.withOpacity(0.6), + Colors.white, + ], + ), ), ), ), ), - ), - ], + ], + ), ), Align( alignment: Alignment.bottomCenter, 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 f56c2998..69a1908d 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 7814df66..4b2ad476 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 50511fed..024d2b47 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..6d8aa4ad 100644 --- a/lib/features/feed/models/plan_pagination_model.dart +++ b/lib/features/feed/models/plan_pagination_model.dart @@ -37,6 +37,7 @@ 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, + @Default(false) bool wasLikedByMe, }) = _PlanModel; factory PlanModel.fromJson(Map json) => _$PlanModelFromJson(json); diff --git a/lib/features/feed/models/plan_pagination_model.freezed.dart b/lib/features/feed/models/plan_pagination_model.freezed.dart index a3348859..16fa0ec3 100644 --- a/lib/features/feed/models/plan_pagination_model.freezed.dart +++ b/lib/features/feed/models/plan_pagination_model.freezed.dart @@ -294,6 +294,7 @@ mixin _$PlanModel { int get totalComments => throw _privateConstructorUsedError; @JsonKey(name: "is_favorite") bool get isFavorite => throw _privateConstructorUsedError; + bool get wasLikedByMe => throw _privateConstructorUsedError; /// Serializes this PlanModel to a JSON map. Map toJson() => throw _privateConstructorUsedError; @@ -327,7 +328,8 @@ 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, + bool wasLikedByMe}); $PlanTypeCopyWith<$Res> get type; $PacketOptionsCopyWith<$Res>? get packet1Options; @@ -368,6 +370,7 @@ class _$PlanModelCopyWithImpl<$Res, $Val extends PlanModel> Object? totalViews = null, Object? totalComments = null, Object? isFavorite = null, + Object? wasLikedByMe = null, }) { return _then(_value.copyWith( id: null == id @@ -438,6 +441,10 @@ class _$PlanModelCopyWithImpl<$Res, $Val extends PlanModel> ? _value.isFavorite : isFavorite // ignore: cast_nullable_to_non_nullable as bool, + wasLikedByMe: null == wasLikedByMe + ? _value.wasLikedByMe + : wasLikedByMe // ignore: cast_nullable_to_non_nullable + as bool, ) as $Val); } @@ -529,7 +536,8 @@ 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, + bool wasLikedByMe}); @override $PlanTypeCopyWith<$Res> get type; @@ -573,6 +581,7 @@ class __$$PlanModelImplCopyWithImpl<$Res> Object? totalViews = null, Object? totalComments = null, Object? isFavorite = null, + Object? wasLikedByMe = null, }) { return _then(_$PlanModelImpl( id: null == id @@ -643,6 +652,10 @@ class __$$PlanModelImplCopyWithImpl<$Res> ? _value.isFavorite : isFavorite // ignore: cast_nullable_to_non_nullable as bool, + wasLikedByMe: null == wasLikedByMe + ? _value.wasLikedByMe + : wasLikedByMe // ignore: cast_nullable_to_non_nullable + as bool, )); } } @@ -667,7 +680,8 @@ 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, + this.wasLikedByMe = false}); factory _$PlanModelImpl.fromJson(Map json) => _$$PlanModelImplFromJson(json); @@ -723,10 +737,13 @@ class _$PlanModelImpl implements _PlanModel { @override @JsonKey(name: "is_favorite") final bool isFavorite; + @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, wasLikedByMe: $wasLikedByMe)'; } @override @@ -759,7 +776,9 @@ class _$PlanModelImpl implements _PlanModel { (identical(other.totalComments, totalComments) || other.totalComments == totalComments) && (identical(other.isFavorite, isFavorite) || - other.isFavorite == isFavorite)); + other.isFavorite == isFavorite) && + (identical(other.wasLikedByMe, wasLikedByMe) || + other.wasLikedByMe == wasLikedByMe)); } @JsonKey(includeFromJson: false, includeToJson: false) @@ -782,7 +801,8 @@ class _$PlanModelImpl implements _PlanModel { totalLikes, totalViews, totalComments, - isFavorite); + isFavorite, + wasLikedByMe); /// Create a copy of PlanModel /// with the given fields replaced by the non-null parameter values. @@ -802,24 +822,24 @@ 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") 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, + final bool wasLikedByMe}) = _$PlanModelImpl; factory _PlanModel.fromJson(Map json) = _$PlanModelImpl.fromJson; @@ -875,6 +895,8 @@ abstract class _PlanModel implements PlanModel { @override @JsonKey(name: "is_favorite") bool get isFavorite; + @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..9f321958 100644 --- a/lib/features/feed/models/plan_pagination_model.g.dart +++ b/lib/features/feed/models/plan_pagination_model.g.dart @@ -52,6 +52,7 @@ _$PlanModelImpl _$$PlanModelImplFromJson(Map json) => totalViews: (json['total_views'] as num).toInt(), totalComments: (json['total_comments'] as num).toInt(), isFavorite: json['is_favorite'] as bool, + wasLikedByMe: json['wasLikedByMe'] as bool? ?? false, ); Map _$$PlanModelImplToJson(_$PlanModelImpl instance) => @@ -73,6 +74,7 @@ Map _$$PlanModelImplToJson(_$PlanModelImpl instance) => 'total_views': instance.totalViews, 'total_comments': instance.totalComments, 'is_favorite': instance.isFavorite, + '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..04c01bbd 100644 --- a/lib/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart +++ b/lib/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart @@ -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; @@ -54,17 +54,35 @@ class DietsCubit extends Cubit { 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 +100,8 @@ 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 +198,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 +218,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 +237,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 +278,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 +318,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 ba2c8993..989e36a2 100644 --- a/lib/features/feed/presentation/pages/feed_screen.dart +++ b/lib/features/feed/presentation/pages/feed_screen.dart @@ -16,6 +16,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 +95,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( @@ -188,185 +202,193 @@ class _FeedScreenState extends State { } _buildAllBlogPosts() { - return BlocProvider( - create: (context) => feedCubit, - child: RefreshIndicator( - onRefresh: () => feedCubit.loadFeedBlogPostList(), - child: BlocBuilder( - builder: (BuildContext context, FeedState state) { - if (state is FeedStateLoaded) { - return ListView.separated( - padding: EdgeInsets.symmetric(vertical: 8), - shrinkWrap: true, - physics: BouncingScrollPhysics(), - itemBuilder: (context, index) { - if (index == state.feedBlogPostList.length) { - return const Loader(); - } - return BlogPostWidget(index: index); - }, - separatorBuilder: (context, index) { - return SizedBox(height: 8); - }, - itemCount: state.isLoading ? state.feedBlogPostList.length + 1 : state.feedBlogPostList.length, - ); - } else if (state is FeedStateError) { - return Scaffold( - backgroundColor: Colors.grey.shade200, - body: Padding( - padding: const EdgeInsets.all(16), - child: Center( - child: Text(state.errorMessage), - ), - ), - ); - } else { - return Scaffold( - backgroundColor: Colors.grey.shade200, - body: const Loader(), - ); - } - }, - ), - ), - ); - } - - _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: () => feedCubit.loadFeedBlogPostList(), + child: BlocBuilder( + builder: (BuildContext context, FeedState state) { + if (state is FeedStateLoaded) { return ListView.separated( padding: EdgeInsets.symmetric(vertical: 8), shrinkWrap: true, physics: BouncingScrollPhysics(), itemBuilder: (context, index) { - return PlanWidget(planModel: state.plans[index]); + if (index == state.feedBlogPostList.length) { + return const Loader(); + } + return BlogPostWidget(index: index); }, separatorBuilder: (context, index) { return SizedBox(height: 8); }, - itemCount: state.plans.length, + itemCount: state.isLoading ? state.feedBlogPostList.length + 1 : state.feedBlogPostList.length, ); - }, - ), + } else if (state is FeedStateError) { + return Scaffold( + backgroundColor: Colors.grey.shade200, + body: Padding( + padding: const EdgeInsets.all(16), + child: Center( + child: Text(state.errorMessage), + ), + ), + ); + } else { + return Scaffold( + backgroundColor: Colors.grey.shade200, + body: const Loader(), + ); + } + }, + ), + ); + } + + _buildAllRecipes() { + 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 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 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, + ), + ), + ), + ], + ); + }, ), ); } @@ -500,6 +522,9 @@ class _FeedScreenState extends State { case 2: dietsCubit.loadDiets(query: query); break; + case 3: + plansCubit.loadPlans(query: query); + break; } }, ); diff --git a/lib/features/feed/presentation/widgets/blog_post_widget.dart b/lib/features/feed/presentation/widgets/blog_post_widget.dart index be4cdcf7..9805750d 100644 --- a/lib/features/feed/presentation/widgets/blog_post_widget.dart +++ b/lib/features/feed/presentation/widgets/blog_post_widget.dart @@ -90,7 +90,8 @@ class _BlogPostWidgetState extends State { ], ), const Spacer(), - if (!(state.feedBlogPostList[widget.index].author.isEntitySubscribedByMe ?? false) && !state.feedBlogPostList[widget.index].author.authorIsMe) + if (!(state.feedBlogPostList[widget.index].author.isEntitySubscribedByMe ?? false) && + !state.feedBlogPostList[widget.index].author.authorIsMe) SubscribeButtonWidget( slug: state.feedBlogPostList[widget.index].author.authorSlug, ), 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..ab15c100 100644 --- a/lib/features/feed/presentation/widgets/diet_filter_bottom_sheet.dart +++ b/lib/features/feed/presentation/widgets/diet_filter_bottom_sheet.dart @@ -31,22 +31,9 @@ class DietFilterBottomSheet extends StatelessWidget { style: TextStyles.textMSemibold.copyWith(color: appTheme.black), ), SizedBox(height: 12), - SortingDropDownFilter(cubit: dietsCubit,), - SizedBox( - height: 16, + SortingDropDownFilter( + cubit: dietsCubit, ), - 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 +48,26 @@ class DietFilterBottomSheet extends StatelessWidget { SizedBox( height: 12, ), - DurationChoiceFilterWidget(cubit: dietsCubit,), + TypeChoiceFilterWidget( + dietsCubit: dietsCubit, + ), + SizedBox( + height: 16, + ), + CustomDivider(), + SizedBox( + height: 16, + ), + Text( + s.time, + style: TextStyles.textMSemibold.copyWith(color: appTheme.black), + ), + SizedBox( + height: 12, + ), + DurationChoiceFilterWidget( + dietCubit: dietsCubit, + ), SizedBox( height: 24, ), diff --git a/lib/features/feed/presentation/widgets/duration_choice_filter_widget.dart b/lib/features/feed/presentation/widgets/duration_choice_filter_widget.dart index d3a5ec9b..8f6e68a1 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,23 @@ 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/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}); + final DietsCubit? dietCubit; + final PlansCubit? plansCubit; @override State createState() => _DurationChoiceFilterWidgetState(); } class _DurationChoiceFilterWidgetState extends State { final List chipsLabelList = []; - @override void didChangeDependencies() { final s = S.of(context); @@ -31,55 +33,108 @@ class _DurationChoiceFilterWidgetState extends State @override Widget build(BuildContext context) { - return BlocBuilder( - bloc: widget.cubit, - builder: (context, state) { - return Wrap( - children: [ - ChoiceChipWidget( - chipsLabelList: chipsLabelList, - index: 0, - selectedChipIndex: state.filterState.durationIndex, - onSelected: (selectedValue) { - widget.cubit.changeDurationFilter(0); - }, - ), - SizedBox( - width: 8, - ), - ChoiceChipWidget( - chipsLabelList: chipsLabelList, - index: 1, - selectedChipIndex: state.filterState.durationIndex, - onSelected: (selectedValue) { - widget.cubit.changeDurationFilter(1); - }, - ), - SizedBox( - width: 8, - ), - ChoiceChipWidget( - chipsLabelList: chipsLabelList, - index: 2, - selectedChipIndex: state.filterState.durationIndex, - onSelected: (selectedValue) { - widget.cubit.changeDurationFilter(2); - }, - ), - SizedBox( - width: 8, - ), - ChoiceChipWidget( - chipsLabelList: chipsLabelList, - index: 3, - selectedChipIndex: state.filterState.durationIndex, - onSelected: (selectedValue) { - widget.cubit.changeDurationFilter(3); - }, - ), - ], - ); - }, - ); + if (widget.dietCubit != null) { + return BlocBuilder( + bloc: widget.dietCubit!, + builder: (context, state) { + return Wrap( + children: [ + ChoiceChipWidget( + chipsLabelList: chipsLabelList, + index: 0, + selectedChipIndex: state.filterState.durationIndex, + onSelected: (selectedValue) { + widget.dietCubit?.changeDurationFilter(0); + }, + ), + SizedBox( + width: 8, + ), + ChoiceChipWidget( + chipsLabelList: chipsLabelList, + index: 1, + selectedChipIndex: state.filterState.durationIndex, + onSelected: (selectedValue) { + widget.dietCubit?.changeDurationFilter(1); + }, + ), + SizedBox( + width: 8, + ), + ChoiceChipWidget( + chipsLabelList: chipsLabelList, + index: 2, + selectedChipIndex: state.filterState.durationIndex, + onSelected: (selectedValue) { + widget.dietCubit?.changeDurationFilter(2); + }, + ), + SizedBox( + width: 8, + ), + ChoiceChipWidget( + chipsLabelList: chipsLabelList, + index: 3, + selectedChipIndex: state.filterState.durationIndex, + onSelected: (selectedValue) { + widget.dietCubit?.changeDurationFilter(3); + }, + ), + ], + ); + }, + ); + } else { + return BlocBuilder( + bloc: widget.plansCubit!, + builder: (context, state) { + return Wrap( + children: [ + ChoiceChipWidget( + chipsLabelList: chipsLabelList, + index: 0, + selectedChipIndex: state.filterState.durationIndex, + onSelected: (selectedValue) { + widget.plansCubit?.changeDurationFilter(0); + }, + ), + SizedBox( + width: 8, + ), + ChoiceChipWidget( + chipsLabelList: chipsLabelList, + index: 1, + selectedChipIndex: state.filterState.durationIndex, + onSelected: (selectedValue) { + widget.plansCubit?.changeDurationFilter(1); + }, + ), + SizedBox( + width: 8, + ), + ChoiceChipWidget( + chipsLabelList: chipsLabelList, + index: 2, + selectedChipIndex: state.filterState.durationIndex, + onSelected: (selectedValue) { + widget.plansCubit?.changeDurationFilter(2); + }, + ), + SizedBox( + width: 8, + ), + ChoiceChipWidget( + chipsLabelList: chipsLabelList, + index: 3, + selectedChipIndex: state.filterState.durationIndex, + onSelected: (selectedValue) { + widget.plansCubit?.changeDurationFilter(3); + }, + ), + ], + ); + }, + ); + } } } 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 5d844029..e62eea64 100644 --- a/lib/features/feed/presentation/widgets/plan_widget.dart +++ b/lib/features/feed/presentation/widgets/plan_widget.dart @@ -1,174 +1,172 @@ 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/feed_icon_button.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: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, - }, + 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.withOpacity(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.withOpacity(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, - ), - ), - ), - 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.withOpacity(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, - ), + Padding( + padding: const EdgeInsets.only(left: 16, top: 12), + 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..7c77dc86 --- /dev/null +++ b/lib/features/feed/presentation/widgets/plans_filter_bottom_sheet.dart @@ -0,0 +1,74 @@ +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/generated/l10n.dart'; +import 'package:eatvio_app/core/theme/custom_text_style.dart'; +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:flutter/material.dart'; + +class PlansFilterBottomSheet extends StatelessWidget { + const PlansFilterBottomSheet({super.key, required this.plansCubit}); + final PlansCubit plansCubit; + @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), + SizedBox(height: 16), + CustomDivider(), + SizedBox( + height: 16, + ), + Text( + s.time, + style: TextStyles.textMSemibold.copyWith(color: appTheme.black), + ), + SizedBox( + height: 12, + ), + DurationChoiceFilterWidget( + plansCubit: plansCubit, + ), + SizedBox( + height: 16, + ), + CustomDivider(), + SizedBox( + height: 16, + ), + Text( + s.plan_type, + style: TextStyles.textMSemibold.copyWith(color: appTheme.black), + ), + SizedBox( + height: 12, + ), + TypeChoiceFilterWidget( + plansCubit: plansCubit, + ), + 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..015dc4cf --- /dev/null +++ b/lib/features/feed/presentation/widgets/price_choice_filter_widget.dart @@ -0,0 +1,183 @@ +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:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class PriceChoiceFilterWidget extends StatefulWidget { + PriceChoiceFilterWidget({super.key, required this.plansCubit}); + final PlansCubit plansCubit; + + @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) { + 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/type_choice_filter_widget.dart b/lib/features/feed/presentation/widgets/type_choice_filter_widget.dart index b4cd6bca..47766db0 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,20 @@ 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/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, + }); + final DietsCubit? dietsCubit; + final PlansCubit? plansCubit; @override State createState() => _TypeChoiceFilterWidgetState(); } @@ -18,77 +25,151 @@ 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) { + 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, - 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); - }, - ), - ], - ); - }, - ); + if (widget.plansCubit != null) { + return BlocBuilder( + bloc: widget.plansCubit, + builder: (context, state) { + return Wrap( + children: [ + ChoiceChipWidget( + chipsLabelList: chipsLabelList, + index: 0, + selectedChipIndex: state.filterState.typeIndex, + onSelected: (selectedValue) { + widget.plansCubit?.changeTypeFilter(0); + }, + ), + SizedBox( + width: 8, + ), + ChoiceChipWidget( + chipsLabelList: chipsLabelList, + index: 1, + selectedChipIndex: state.filterState.typeIndex, + onSelected: (selectedValue) { + widget.plansCubit?.changeTypeFilter(1); + }, + ), + SizedBox( + width: 8, + ), + ChoiceChipWidget( + chipsLabelList: chipsLabelList, + index: 2, + selectedChipIndex: state.filterState.typeIndex, + onSelected: (selectedValue) { + widget.plansCubit?.changeTypeFilter(2); + }, + ), + SizedBox( + width: 8, + ), + ChoiceChipWidget( + chipsLabelList: chipsLabelList, + index: 3, + selectedChipIndex: state.filterState.typeIndex, + onSelected: (selectedValue) { + widget.plansCubit?.changeTypeFilter(3); + }, + ), + SizedBox( + width: 8, + ), + ChoiceChipWidget( + chipsLabelList: chipsLabelList, + index: 4, + selectedChipIndex: state.filterState.typeIndex, + onSelected: (selectedValue) { + widget.plansCubit?.changeTypeFilter(4); + }, + ), + ], + ); + }, + ); + } else { + return BlocBuilder( + bloc: widget.dietsCubit, + builder: (context, state) { + return Wrap( + children: [ + ChoiceChipWidget( + chipsLabelList: chipsLabelList, + index: 0, + selectedChipIndex: state.filterState.typeIndex, + onSelected: (selectedValue) { + widget.dietsCubit?.changeTypeFilter(0); + }, + ), + SizedBox( + width: 8, + ), + ChoiceChipWidget( + chipsLabelList: chipsLabelList, + index: 1, + selectedChipIndex: state.filterState.typeIndex, + onSelected: (selectedValue) { + widget.dietsCubit?.changeTypeFilter(1); + }, + ), + SizedBox( + width: 8, + ), + ChoiceChipWidget( + chipsLabelList: chipsLabelList, + index: 2, + selectedChipIndex: state.filterState.typeIndex, + onSelected: (selectedValue) { + widget.dietsCubit?.changeTypeFilter(2); + }, + ), + SizedBox( + width: 8, + ), + ChoiceChipWidget( + chipsLabelList: chipsLabelList, + index: 3, + selectedChipIndex: state.filterState.typeIndex, + onSelected: (selectedValue) { + widget.dietsCubit?.changeTypeFilter(3); + }, + ), + SizedBox( + width: 8, + ), + ChoiceChipWidget( + chipsLabelList: chipsLabelList, + index: 4, + selectedChipIndex: state.filterState.typeIndex, + onSelected: (selectedValue) { + widget.dietsCubit?.changeTypeFilter(4); + }, + ), + ], + ); + }, + ); + } } } 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/plans/presentation/cubit/about_plan_cubit.dart b/lib/features/plans/presentation/cubit/about_plan_cubit.dart index ee244be5..b22bcfaf 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,73 @@ 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)); + planRepository.emitLikesStateToFeed(LikesState(wasLikedByMe: false, totalLikes: currentPlan.totalLikes - 1, index: index)); + } else { + emit(state.copyWith(planModel: currentPlan.copyWith(wasLikedByMe: true, totalLikes: currentPlan.totalLikes + 1), error: null)); + planRepository.emitLikesStateToFeed(LikesState(wasLikedByMe: true, totalLikes: currentPlan.totalLikes + 1, index: index)); } + + EasyDebounce.debounce( + 'like_plan_$slug', + Duration(seconds: 1), + () async { + try { + await planRepository.likePlan(slug); + } catch (e) { + log(e.toString()); + 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))); + 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('Произошла ошибка. Попробуйте ещё раз через некоторое время'); + 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))); + 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('Произошла ошибка. Попробуйте ещё раз через некоторое время'); + planRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: currentPlan.isFavorite, index: currentPlan.id)); + 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 1f2aa1cf..3b6489ca 100644 --- a/lib/features/plans/presentation/pages/about_plan_screen.dart +++ b/lib/features/plans/presentation/pages/about_plan_screen.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, @@ -352,7 +353,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 9c0f18ad..b6dff9b3 100644 --- a/lib/features/profile/presentation/pages/profile_screen.dart +++ b/lib/features/profile/presentation/pages/profile_screen.dart @@ -273,7 +273,7 @@ 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(() { diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart index 0dc19f85..64dc4d01 100644 --- a/lib/generated/intl/messages_en.dart +++ b/lib/generated/intl/messages_en.dart @@ -92,7 +92,6 @@ class MessageLookup extends MessageLookupByLibrary { "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("Есть ли у вас гипертония?"), @@ -233,6 +232,7 @@ class MessageLookup extends MessageLookupByLibrary { "plan_duration": MessageLookupByLibrary.simpleMessage("Срок плана"), "plan_label_desc": MessageLookupByLibrary.simpleMessage("План для поддержания формы"), + "plan_type": MessageLookupByLibrary.simpleMessage("Тип плана"), "plans_complete": MessageLookupByLibrary.simpleMessage("Пройденные планы"), "popular": MessageLookupByLibrary.simpleMessage("Популярные"), @@ -243,6 +243,7 @@ class MessageLookup extends MessageLookupByLibrary { "pressure_is_not_a_number": MessageLookupByLibrary.simpleMessage( "Все показатели давления должны быть числами"), "preview": MessageLookupByLibrary.simpleMessage("Превью"), + "price": MessageLookupByLibrary.simpleMessage("Цена"), "price_explanation": MessageLookupByLibrary.simpleMessage( "Разница в ценах, это комиссия Итвио"), "profile": MessageLookupByLibrary.simpleMessage("Профиль"), @@ -262,6 +263,7 @@ class MessageLookup extends MessageLookupByLibrary { "tell_your_schedule": MessageLookupByLibrary.simpleMessage( "Расскажите в свободной форме о том как просыпаетесь, когда едите, когда ложитесь спать:"), "three_options": MessageLookupByLibrary.simpleMessage("3 опции"), + "time": MessageLookupByLibrary.simpleMessage("Время прохождения"), "to_draft": MessageLookupByLibrary.simpleMessage("В черновик"), "unfavorite_foods": MessageLookupByLibrary.simpleMessage("Нелюбимые продукты"), diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index 6eb67d1f..363081a6 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,16 @@ class S { args: [], ); } + + /// `Цена` + String get price { + return Intl.message( + 'Цена', + name: 'price', + desc: '', + args: [], + ); + } } class AppLocalizationDelegate extends LocalizationsDelegate { diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index b5f2d8a3..4d8be716 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,8 @@ "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": "Цена" + + }