From bf6e23e96a6a4002bb9b4b5a285ace32d11a05a0 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Wed, 3 Sep 2025 11:34:41 +0300 Subject: [PATCH 01/28] feat(metrica): Issue-685 --- .../profile_avatar_cubit.dart | 47 +++++++++++++++---- 1 file changed, 39 insertions(+), 8 deletions(-) diff --git a/lib/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart b/lib/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart index cc7939dd..935eecf7 100644 --- a/lib/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart +++ b/lib/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart @@ -1,24 +1,55 @@ +import 'package:appmetrica_plugin/appmetrica_plugin.dart'; import 'package:eatvio_app/core/app_router.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_state.dart'; import 'package:eatvio_app/features/profile/repositories/profile_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; +import 'package:permission_handler/permission_handler.dart'; class ProfileAvatarCubit extends Cubit { final ProfileRepository profileRepository; - ProfileAvatarCubit({ - required this.profileRepository, - }) : super(const ProfileAvatarState.loading()); + ProfileAvatarCubit({required this.profileRepository}) : super(const ProfileAvatarState.loading()); + + int _calculateAge(String birthdate) { + try { + final dateParts = birthdate.split('-'); + if (dateParts.length == 3) { + final birthYear = int.parse(dateParts[0]); + final birthMonth = int.parse(dateParts[1]); + final birthDay = int.parse(dateParts[2]); + + final now = DateTime.now(); + int age = now.year - birthYear; + + if (now.month < birthMonth || (now.month == birthMonth && now.day < birthDay)) { + age--; + } + + return age; + } + } catch (e) { + return 0; + } + return 0; + } Future getMyProfile() async { emit(const ProfileAvatarState.loading()); + final profile = await profileRepository.getMyProfile(); + final profileDetails = await profileRepository.getMyProfileDetails(); try { - emit( - ProfileAvatarState.loaded( - profile: await profileRepository.getMyProfile(), - ), - ); + emit(ProfileAvatarState.loaded(profile: profile)); + final notificationStatus = await Permission.notification.status; + final isNotificationEnabled = notificationStatus.isGranted; + AppMetricaUserProfile userProfile = AppMetricaUserProfile([ + AppMetricaNameAttribute.withValue(profile.name), + AppMetricaGenderAttribute.withValue(profileDetails.gender == 'male' ? AppMetricaGender.male : AppMetricaGender.female), + AppMetricaBirthDateAttribute.withAge(_calculateAge(profileDetails.birthdate)), + AppMetricaNotificationEnabledAttribute.withValue(isNotificationEnabled), + ]); + AppMetrica.setUserProfileID(profile.slug); + AppMetrica.reportUserProfile(userProfile); } catch (e) { AppRouter.navigatorKey.currentContext?.go('/auth-screen'); emit(ProfileAvatarState.error(errorMessage: e.toString())); From b6b1c8b3c13356da606f26c218e3475e3207a441 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Mon, 8 Sep 2025 14:14:41 +0300 Subject: [PATCH 02/28] =?UTF-8?q?feat(diary):=20=D0=BA=D0=B1=D0=B6=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../diary/models/diary_day_model.dart | 38 +-- .../widgets/meal_button_widget.dart | 116 +++++---- .../widgets/meal_summary_widget.dart | 112 +++++++++ .../presentation/widgets/meal_widget.dart | 25 +- .../presentation/widgets/product_widget.dart | 157 +++++++----- .../widgets/products_list_widget.dart | 25 +- .../diary/repositories/diary_repository.dart | 225 +++++++----------- .../diary/services/diary_service.dart | 94 ++------ .../bloc/feed_cubit/feed_cubit.dart | 61 ++--- .../pages/registration_screen.dart | 6 + 10 files changed, 461 insertions(+), 398 deletions(-) create mode 100644 lib/features/diary/presentation/widgets/meal_summary_widget.dart diff --git a/lib/features/diary/models/diary_day_model.dart b/lib/features/diary/models/diary_day_model.dart index 6e08f6d0..e1ad4572 100644 --- a/lib/features/diary/models/diary_day_model.dart +++ b/lib/features/diary/models/diary_day_model.dart @@ -22,12 +22,7 @@ enum PortionState { @freezed abstract class DiaryDayWrapperModel with _$DiaryDayWrapperModel { - const factory DiaryDayWrapperModel({ - @JsonKey( - name: "diary", - ) - required DiaryDayModel diary, - }) = _DiaryDayWrapperModel; + const factory DiaryDayWrapperModel({@JsonKey(name: "diary") required DiaryDayModel diary}) = _DiaryDayWrapperModel; factory DiaryDayWrapperModel.fromJson(Map json) => _$DiaryDayWrapperModelFromJson(json); } @@ -71,21 +66,13 @@ DiaryDietModel? _parseDiet(dynamic json) { @freezed abstract class DiaryDietModel with _$DiaryDietModel { - const factory DiaryDietModel({ - required int id, - required int duration, - required String name, - }) = _DiaryDietModel; + const factory DiaryDietModel({required int id, required int duration, required String name}) = _DiaryDietModel; factory DiaryDietModel.fromJson(Map json) => _$DiaryDietModelFromJson(json); } TypeOfDiaryModel _parseDayTypeForFutureDay(dynamic json) { - return TypeOfDiaryModel( - hasChosenDiet: json['has_chosen_diet'] as bool, - hasChosenPlan: json['has_chosen_plan'] as bool, - diaryState: DiaryStateEnum.init, - ); + return TypeOfDiaryModel(hasChosenDiet: json['has_chosen_diet'] as bool, hasChosenPlan: json['has_chosen_plan'] as bool, diaryState: DiaryStateEnum.init); } @freezed @@ -115,11 +102,30 @@ abstract class DiaryPhotoModel with _$DiaryPhotoModel { factory DiaryPhotoModel.fromJson(Map json) => _$DiaryPhotoModelFromJson(json); } +@freezed +abstract class ProductCaloriesInfoModel with _$ProductCaloriesInfoModel { + const factory ProductCaloriesInfoModel({ + @JsonKey(name: "id") required int id, + @JsonKey(name: "product_name") required String productName, + @JsonKey(name: "energy_on_100gr") required double calories, + @JsonKey(name: "proteins_on_100gr") required double proteins, + @JsonKey(name: "fats_on_100gr") required double fats, + @JsonKey(name: "carbohydrates_on_100gr") required double carbohydrates, + @JsonKey(name: "energy_on_portion") required double energyOnPortion, + @JsonKey(name: "proteins_on_portion") required double proteinsOnPortion, + @JsonKey(name: "fats_on_portion") required double fatsOnPortion, + @JsonKey(name: "carbohydrates_on_portion") required double carbohydratesOnPortion, + }) = _ProductCaloriesInfoModel; + + factory ProductCaloriesInfoModel.fromJson(Map json) => _$ProductCaloriesInfoModelFromJson(json); +} + @freezed abstract class PortionModel with _$PortionModel { const factory PortionModel({ @JsonKey(name: "id") required int id, @JsonKey(name: "product_nutritional_value_id") int? productNutritionalValueId, + ProductCaloriesInfoModel? productCaloriesInfo, @JsonKey(name: "product_name") required String productName, @JsonKey(name: "quantity") required int quantity, @JsonKey(name: "portion") required String portion, diff --git a/lib/features/diary/presentation/widgets/meal_button_widget.dart b/lib/features/diary/presentation/widgets/meal_button_widget.dart index d8937b14..9e17c217 100644 --- a/lib/features/diary/presentation/widgets/meal_button_widget.dart +++ b/lib/features/diary/presentation/widgets/meal_button_widget.dart @@ -1,16 +1,15 @@ import 'package:collection/collection.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/features/diary/models/diary_day_model.dart'; import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart'; import 'package:eatvio_app/features/diary/presentation/bloc/diary_state.dart'; +import 'package:eatvio_app/features/diary/presentation/widgets/meal_summary_widget.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class MealButtonWidget extends StatefulWidget { final int mealNumber; - const MealButtonWidget({ - super.key, - required this.mealNumber, - }); + const MealButtonWidget({super.key, required this.mealNumber}); @override State createState() => _MealButtonWidgetState(); @@ -22,9 +21,7 @@ class _MealButtonWidgetState extends State { return BlocBuilder( builder: (context, state) { if (state is DiaryStateLoaded) { - final consultantComment = state.consultantReview?.consultantComments.mealsComments.firstWhereOrNull( - (element) => element.typeNo == widget.mealNumber, - ); + final consultantComment = state.consultantReview?.consultantComments.mealsComments.firstWhereOrNull((element) => element.typeNo == widget.mealNumber); final Color bgColor; if (state.selectedMeal == widget.mealNumber) { @@ -34,55 +31,84 @@ class _MealButtonWidgetState extends State { } else { bgColor = const Color(0xFFF1F2F6); } - return InkWell( - onTap: () { - context.read().setSelectedMeal(widget.mealNumber); - }, - borderRadius: BorderRadius.circular(100), - child: Ink( - width: MediaQuery.of(context).size.width / 5 - (96 / 5), - height: 32, - decoration: BoxDecoration( + + final mealIndex = widget.mealNumber - 1; + final portions = mealIndex < state.diary.meals.length ? state.diary.meals[mealIndex].portions : []; + final totals = MealSummaryWidget.calculateMealTotals(portions); + final calories = totals['calories']!.round(); + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + InkWell( + onTap: () { + context.read().setSelectedMeal(widget.mealNumber); + }, borderRadius: BorderRadius.circular(100), - color: bgColor, - ), - child: Center( - child: Text( - '${widget.mealNumber}', - style: TextStyle( - color: state.selectedMeal == widget.mealNumber ? Colors.white : const Color(0xFF6D6F71), - fontSize: 16, - fontWeight: FontWeight.w600, + child: Ink( + width: MediaQuery.of(context).size.width / 5 - (96 / 5), + height: 32, + decoration: BoxDecoration(borderRadius: BorderRadius.circular(100), color: bgColor), + child: Center( + child: Text( + '${widget.mealNumber}', + style: TextStyle( + color: state.selectedMeal == widget.mealNumber ? Colors.white : const Color(0xFF6D6F71), + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), ), ), ), - ), + const SizedBox(height: 4), + Text( + calories > 0 ? '$calories ккал' : '— ккал', + style: const TextStyle(color: Color(0xFF6D6F71), fontSize: 12, fontWeight: FontWeight.w500), + ), + ], ); } if (state is DiaryStateLoadedTomorrowWithDiet) { - return InkWell( - onTap: () { - context.read().setSelectedMeal(widget.mealNumber); - }, - borderRadius: BorderRadius.circular(100), - child: Ink( - width: MediaQuery.of(context).size.width / 5 - (96 / 5), - height: 32, - decoration: BoxDecoration( + + final mealIndex = widget.mealNumber - 1; + final portions = mealIndex < state.diary.meals.length ? state.diary.meals[mealIndex].portions : []; + final totals = MealSummaryWidget.calculateMealTotals(portions); + final calories = totals['calories']!.round(); + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + InkWell( + onTap: () { + context.read().setSelectedMeal(widget.mealNumber); + }, borderRadius: BorderRadius.circular(100), - color: state.selectedMeal == widget.mealNumber ? appTheme.blueA400 : const Color(0xFFF1F2F6), - ), - child: Center( - child: Text( - '${widget.mealNumber}', - style: TextStyle( - color: state.selectedMeal == widget.mealNumber ? Colors.white : const Color(0xFF6D6F71), - fontSize: 16, - fontWeight: FontWeight.w600, + child: Ink( + width: MediaQuery.of(context).size.width / 5 - (96 / 5), + height: 32, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(100), + color: state.selectedMeal == widget.mealNumber ? appTheme.blueA400 : const Color(0xFFF1F2F6), + ), + child: Center( + child: Text( + '${widget.mealNumber}', + style: TextStyle( + color: state.selectedMeal == widget.mealNumber ? Colors.white : const Color(0xFF6D6F71), + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), ), ), ), - ), + const SizedBox(height: 4), + Text( + calories > 0 ? '$calories ккал' : '— ккал', + style: const TextStyle(color: Color(0xFF6D6F71), fontSize: 12, fontWeight: FontWeight.w500), + ), + ], ); } return const SizedBox.shrink(); diff --git a/lib/features/diary/presentation/widgets/meal_summary_widget.dart b/lib/features/diary/presentation/widgets/meal_summary_widget.dart new file mode 100644 index 00000000..dd7108e6 --- /dev/null +++ b/lib/features/diary/presentation/widgets/meal_summary_widget.dart @@ -0,0 +1,112 @@ +import 'package:eatvio_app/features/diary/models/diary_day_model.dart'; +import 'package:flutter/material.dart'; + +class MealSummaryWidget extends StatelessWidget { + final List portions; + final int mealNumber; + + const MealSummaryWidget({super.key, required this.portions, required this.mealNumber}); + + @override + Widget build(BuildContext context) { + final totals = _calculateTotals(); + + if (totals['calories'] == 0 && totals['proteins'] == 0 && totals['fats'] == 0 && totals['carbohydrates'] == 0) { + return const SizedBox.shrink(); + } + + return Container( + margin: const EdgeInsets.only(top: 8), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: const Color(0xFF2F80ED), width: 2), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Прием $mealNumber', + style: const TextStyle(color: Color(0xFF323F4B), fontSize: 16, fontWeight: FontWeight.w600), + ), + Row( + children: [ + _buildNutritionChip('К', totals['calories']!.round()), + const SizedBox(width: 4), + _buildNutritionChip('Б', totals['proteins']!.round()), + const SizedBox(width: 4), + _buildNutritionChip('Ж', totals['fats']!.round()), + const SizedBox(width: 4), + _buildNutritionChip('У', totals['carbohydrates']!.round()), + ], + ), + ], + ), + ); + } + + Widget _buildNutritionChip(String label, int value) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), + decoration: BoxDecoration(color: const Color(0xFFF6FAFF), borderRadius: BorderRadius.circular(12)), + child: RichText( + text: TextSpan( + children: [ + TextSpan( + text: '$label ', + style: const TextStyle(color: Color(0xFF2F80ED), fontSize: 12, fontWeight: FontWeight.w600), + ), + TextSpan( + text: '$value', + style: const TextStyle(color: Color(0xFF6D6F71), fontSize: 12, fontWeight: FontWeight.w600), + ), + ], + ), + ), + ); + } + + Map _calculateTotals() { + return calculateMealTotals(portions); + } + + static Map calculateMealTotals(List portions) { + double totalCalories = 0; + double totalProteins = 0; + double totalFats = 0; + double totalCarbohydrates = 0; + + for (final portion in portions) { + if (portion.portionState == PortionState.eatenDeleted) { + continue; + } + + final caloriesInfo = portion.productCaloriesInfo; + if (caloriesInfo == null) { + continue; + } + + double calories, proteins, fats, carbohydrates; + + if (portion.portion == 'gr') { + calories = (caloriesInfo.calories * portion.quantity) / 100; + proteins = (caloriesInfo.proteins * portion.quantity) / 100; + fats = (caloriesInfo.fats * portion.quantity) / 100; + carbohydrates = (caloriesInfo.carbohydrates * portion.quantity) / 100; + } else { + calories = caloriesInfo.energyOnPortion; + proteins = caloriesInfo.proteinsOnPortion; + fats = caloriesInfo.fatsOnPortion; + carbohydrates = caloriesInfo.carbohydratesOnPortion; + } + + totalCalories += calories; + totalProteins += proteins; + totalFats += fats; + totalCarbohydrates += carbohydrates; + } + + return {'calories': totalCalories, 'proteins': totalProteins, 'fats': totalFats, 'carbohydrates': totalCarbohydrates}; + } +} diff --git a/lib/features/diary/presentation/widgets/meal_widget.dart b/lib/features/diary/presentation/widgets/meal_widget.dart index ce71ef66..21b7fe20 100644 --- a/lib/features/diary/presentation/widgets/meal_widget.dart +++ b/lib/features/diary/presentation/widgets/meal_widget.dart @@ -40,11 +40,7 @@ class _MealWidgetState extends State { children: [ const Text( 'Прием пищи', - style: TextStyle( - color: Color(0xFF323F4B), - fontSize: 18, - fontWeight: FontWeight.w500, - ), + style: TextStyle(color: Color(0xFF323F4B), fontSize: 18, fontWeight: FontWeight.w500), ), const SizedBox(height: 16), const Row( @@ -57,23 +53,17 @@ class _MealWidgetState extends State { MealButtonWidget(mealNumber: 5), ], ), - const SizedBox(height: 12), + const SizedBox(height: 16), const TimeOfMealPickerWidget(), const ChosenDietWidget(), const ChosenPlanWidget(), const ProductsListWidget(), - if (widget.showAddButton) ...[ - const SizedBox(height: 12), - const AddProductButtonWidget(), - ], + if (widget.showAddButton) ...[const SizedBox(height: 12), const AddProductButtonWidget()], const SizedBox(height: 12), const PhotoGalleryWidget(), const SizedBox(height: 12), const SatietyRatingWidget(), - if (widget.showMood) ...[ - const SizedBox(height: 12), - const MoodRatingWidget(), - ], + if (widget.showMood) ...[const SizedBox(height: 12), const MoodRatingWidget()], BlocBuilder( builder: (context, state) { if (state is DiaryStateLoaded && state.isConsultantView && !state.isDiaryCommented) { @@ -91,10 +81,7 @@ class _MealWidgetState extends State { if (consultantCommentOfMeal != null) { return Column( children: [ - Text( - '${s.mealComment} ${state.selectedMeal}', - style: TextStyles.textLSemibold.copyWith(color: appTheme.black), - ), + Text('${s.mealComment} ${state.selectedMeal}', style: TextStyles.textLSemibold.copyWith(color: appTheme.black)), const SizedBox(height: 12), CommentContainer(comment: consultantCommentOfMeal.comment), ], @@ -103,7 +90,7 @@ class _MealWidgetState extends State { } return const SizedBox.shrink(); }, - ) + ), ], ), ), diff --git a/lib/features/diary/presentation/widgets/product_widget.dart b/lib/features/diary/presentation/widgets/product_widget.dart index a221ffc6..cdf34f28 100644 --- a/lib/features/diary/presentation/widgets/product_widget.dart +++ b/lib/features/diary/presentation/widgets/product_widget.dart @@ -1,3 +1,4 @@ + import 'package:collection/collection.dart'; import 'package:dotted_border/dotted_border.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; @@ -15,10 +16,7 @@ import 'package:flutter_svg/flutter_svg.dart'; class ProductWidget extends StatefulWidget { final PortionModel portion; - const ProductWidget({ - super.key, - required this.portion, - }); + const ProductWidget({super.key, required this.portion}); @override State createState() => _ProductWidgetState(); @@ -37,6 +35,60 @@ class _ProductWidgetState extends State { isEaten = widget.portion.portionState == PortionState.eatenFromDiet ? true : false; } + Widget _buildNutritionalInfo() { + if (widget.portion.productNutritionalValueId == null) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(12)), + child: const Text( + 'Продукт отправлен на модерацию', + style: TextStyle(color: Color(0xFF2F80ED), fontSize: 12, fontWeight: FontWeight.w500), + ), + ); + } + + final caloriesInfo = widget.portion.productCaloriesInfo; + if (caloriesInfo == null) { + return const SizedBox.shrink(); + } + double calories, proteins, fats, carbohydrates; + + if (widget.portion.portion == 'gr') { + calories = (caloriesInfo.calories * widget.portion.quantity) / 100; + proteins = (caloriesInfo.proteins * widget.portion.quantity) / 100; + fats = (caloriesInfo.fats * widget.portion.quantity) / 100; + carbohydrates = (caloriesInfo.carbohydrates * widget.portion.quantity) / 100; + } else { + calories = caloriesInfo.energyOnPortion; + proteins = caloriesInfo.proteinsOnPortion; + fats = caloriesInfo.fatsOnPortion; + carbohydrates = caloriesInfo.carbohydratesOnPortion; + } + + return Row( + children: [ + _buildNutritionChip('К', calories.round()), + const SizedBox(width: 4), + _buildNutritionChip('Б', proteins.round()), + const SizedBox(width: 4), + _buildNutritionChip('Ж', fats.round()), + const SizedBox(width: 4), + _buildNutritionChip('У', carbohydrates.round()), + ], + ); + } + + Widget _buildNutritionChip(String label, int value) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(8)), + child: Text( + '$label $value', + style: const TextStyle(color: Color(0xFF6D6F71), fontSize: 12, fontWeight: FontWeight.w500), + ), + ); + } + @override void didUpdateWidget(ProductWidget oldWidget) { super.didUpdateWidget(oldWidget); @@ -48,15 +100,9 @@ class _ProductWidgetState extends State { return showModalBottomSheet( useRootNavigator: true, context: context, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(12)), - ), + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(12))), isScrollControlled: true, - builder: (context) => AddProductModal( - state: state, - portion: portion, - diaryCubit: diaryCubit, - ), + builder: (context) => AddProductModal(state: state, portion: portion, diaryCubit: diaryCubit), ); } @@ -69,15 +115,21 @@ class _ProductWidgetState extends State { horizontalTitleGap: 0, contentPadding: widget.portion.fromDiet ? const EdgeInsets.only(right: 24, left: 12) : null, title: Text(widget.portion.productName), - subtitle: Text('${widget.portion.quantity} ${widget.portion.portion == 'gr' ? 'гр' : 'шт'}'), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('${widget.portion.quantity} ${widget.portion.portion == 'gr' ? 'гр' : 'шт'}'), + const SizedBox(height: 4), + _buildNutritionalInfo(), + ], ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), tileColor: const Color(0xFFF6FAFF), ); } if (state is DiaryStateLoaded) { - final isDiaryCommented = state.diary.typeOfDiary.diaryState == DiaryStateEnum.commented || + final isDiaryCommented = + state.diary.typeOfDiary.diaryState == DiaryStateEnum.commented || state.diary.typeOfDiary.diaryState == DiaryStateEnum.lastDayCommented || state.diary.typeOfDiary.diaryState == DiaryStateEnum.reviewedByUser; final isDiaryFinished = @@ -99,21 +151,20 @@ class _ProductWidgetState extends State { color: Color(0xFF6D6F71), ), child: ListTile( - onTap: () => diaryCubit.restore( - diaryId: state.diary.id, - mealId: state.selectedMeal, - portionId: widget.portion.id, - ), - trailing: SvgPicture.asset( - 'assets/icons/revert_product.svg', - ), + onTap: () => diaryCubit.restore(diaryId: state.diary.id, mealId: state.selectedMeal, portionId: widget.portion.id), + trailing: SvgPicture.asset('assets/icons/revert_product.svg'), horizontalTitleGap: 0, contentPadding: widget.portion.fromDiet ? const EdgeInsets.only(right: 24, left: 4) : null, title: Text(widget.portion.productName), - subtitle: Text('${widget.portion.quantity} ${widget.portion.portion == 'gr' ? 'гр' : 'шт'}'), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('${widget.portion.quantity} ${widget.portion.portion == 'gr' ? 'гр' : 'шт'}'), + const SizedBox(height: 4), + _buildNutritionalInfo(), + ], ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), // ignore: use_full_hex_values_for_flutter_colors tileColor: const Color(0xfff6faff03).withValues(alpha: 0.01), ), @@ -122,12 +173,13 @@ class _ProductWidgetState extends State { Widget? trailing; if (state.isConsultantView && !isDiaryCommented) { trailing = GestureDetector( - onTap: () { - setState(() { - showCommentWidget = !showCommentWidget; - }); - }, - child: SvgPicture.asset('assets/icons/edit.svg')); + onTap: () { + setState(() { + showCommentWidget = !showCommentWidget; + }); + }, + child: SvgPicture.asset('assets/icons/edit.svg'), + ); } else if (!isDiaryCommented && !widget.portion.fromDiet) { trailing = const Icon(Icons.more_vert_rounded); } @@ -147,47 +199,36 @@ class _ProductWidgetState extends State { isEaten = value!; }); if (isEaten) { - diaryCubit.markEaten( - diaryId: state.diary.id, - mealId: state.selectedMeal, - portionId: widget.portion.id, - ); + diaryCubit.markEaten(diaryId: state.diary.id, mealId: state.selectedMeal, portionId: widget.portion.id); } else { - diaryCubit.unmarkEaten( - diaryId: state.diary.id, - mealId: state.selectedMeal, - portionId: widget.portion.id, - ); + diaryCubit.unmarkEaten(diaryId: state.diary.id, mealId: state.selectedMeal, portionId: widget.portion.id); } } : null, activeColor: appTheme.blueA400, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(4), - ), - side: const BorderSide( - color: Color(0xFF809CB9), - ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)), + side: const BorderSide(color: Color(0xFF809CB9)), ), ) : null, horizontalTitleGap: 0, contentPadding: widget.portion.fromDiet ? const EdgeInsets.only(right: 24, left: 4) : null, title: Text(widget.portion.productName), - subtitle: Text('${widget.portion.quantity} ${widget.portion.portion == 'gr' ? 'гр' : 'шт'}'), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('${widget.portion.quantity} ${widget.portion.portion == 'gr' ? 'гр' : 'шт'}'), + const SizedBox(height: 4), + _buildNutritionalInfo(), + ], ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), tileColor: const Color(0xFFF6FAFF), ), Padding( padding: const EdgeInsets.only(top: 8.0), - child: PortionCommentWidget( - enabled: showCommentWidget, - portionId: widget.portion.id, - comment: portionComment?.comment, - ), - ) + child: PortionCommentWidget(enabled: showCommentWidget, portionId: widget.portion.id, comment: portionComment?.comment), + ), ], ); } diff --git a/lib/features/diary/presentation/widgets/products_list_widget.dart b/lib/features/diary/presentation/widgets/products_list_widget.dart index 6882af08..d3d7ac10 100644 --- a/lib/features/diary/presentation/widgets/products_list_widget.dart +++ b/lib/features/diary/presentation/widgets/products_list_widget.dart @@ -1,5 +1,6 @@ import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart'; import 'package:eatvio_app/features/diary/presentation/bloc/diary_state.dart'; +import 'package:eatvio_app/features/diary/presentation/widgets/meal_summary_widget.dart'; import 'package:eatvio_app/features/diary/presentation/widgets/product_loader_widget.dart'; import 'package:eatvio_app/features/diary/presentation/widgets/product_widget.dart'; import 'package:flutter/material.dart'; @@ -16,33 +17,33 @@ class _ProductsListWidgetState extends State { @override Widget build(BuildContext context) { return BlocBuilder( - builder: (BuildContext context, DiaryState state) { + builder: (BuildContext context, DiaryState state) { if (state is DiaryStateLoaded) { int mealIndex = state.selectedMeal - 1; + final portions = state.diary.meals[mealIndex].portions; return Column( mainAxisSize: MainAxisSize.min, children: [ - for (int i = 0; i < state.diary.meals[mealIndex].portions.length; i++) ...[ - ProductWidget(portion: state.diary.meals[mealIndex].portions[i]), - i < state.diary.meals[mealIndex].portions.length - 1 ? const SizedBox(height: 8) : const SizedBox.shrink(), + for (int i = 0; i < portions.length; i++) ...[ + ProductWidget(portion: portions[i]), + i < portions.length - 1 ? const SizedBox(height: 8) : const SizedBox.shrink(), ], - if (state.isLoadingAddingProductInMeal) - const Padding( - padding: EdgeInsets.only(top: 8), - child: ProductLoaderWidget(), - ), + if (portions.isNotEmpty) MealSummaryWidget(portions: portions, mealNumber: state.selectedMeal), + if (state.isLoadingAddingProductInMeal) const Padding(padding: EdgeInsets.only(top: 8), child: ProductLoaderWidget()), ], ); } if (state is DiaryStateLoadedTomorrowWithDiet) { int mealIndex = state.selectedMeal - 1; + final portions = state.diary.meals[mealIndex].portions; return Column( mainAxisSize: MainAxisSize.min, children: [ - for (int i = 0; i < state.diary.meals[mealIndex].portions.length; i++) ...[ - ProductWidget(portion: state.diary.meals[mealIndex].portions[i]), - i < state.diary.meals[mealIndex].portions.length - 1 ? const SizedBox(height: 8) : const SizedBox.shrink(), + for (int i = 0; i < portions.length; i++) ...[ + ProductWidget(portion: portions[i]), + i < portions.length - 1 ? const SizedBox(height: 8) : const SizedBox.shrink(), ], + if (portions.isNotEmpty) MealSummaryWidget(portions: portions, mealNumber: state.selectedMeal), ], ); } diff --git a/lib/features/diary/repositories/diary_repository.dart b/lib/features/diary/repositories/diary_repository.dart index 2ac30803..0165ad7f 100644 --- a/lib/features/diary/repositories/diary_repository.dart +++ b/lib/features/diary/repositories/diary_repository.dart @@ -15,13 +15,55 @@ class DiaryRepository { Stream get forceFetchStream => _diaryForceFetchController.stream; - DiaryRepository({ - required this.diaryService, - }); + DiaryRepository({required this.diaryService}); Future getDiaryByDate(String date) async { try { - return (await getDiaryIndexByDate(date)).last; + Set ids = {}; + final DiaryDayModel diaryDay = (await getDiaryIndexByDate(date)).last; + for (var meal in diaryDay.meals) { + for (var portion in meal.portions) { + if (portion.productNutritionalValueId != null) { + ids.add(portion.productNutritionalValueId!); + } + } + } + if (ids.isNotEmpty) { + final glossary = await getGlossariesIndex(ids: ids.toList()); + final List updatedMeals = List.from(diaryDay.meals); + for (var mealIndex = 0; mealIndex < updatedMeals.length; mealIndex++) { + final meal = updatedMeals[mealIndex]; + final updatedPortions = meal.portions.map((portion) { + if (portion.productNutritionalValueId != null) { + try { + final glossaryItem = glossary.glossariesList.firstWhere((g) => g.id == portion.productNutritionalValueId); + return portion.copyWith( + productCaloriesInfo: ProductCaloriesInfoModel( + id: glossaryItem.id, + productName: glossaryItem.name, + calories: glossaryItem.energyOn100Gr, + proteins: glossaryItem.proteinsOn100Gr, + fats: glossaryItem.fatsOn100Gr, + carbohydrates: glossaryItem.carbohydratesOn100Gr, + energyOnPortion: glossaryItem.energyOnPortion, + proteinsOnPortion: glossaryItem.proteinsOnPortion, + fatsOnPortion: glossaryItem.fatsOnPortion, + carbohydratesOnPortion: glossaryItem.carbohydratesOnPortion, + ), + ); + } catch (e) { + return portion; + } + } + return portion; + }).toList(); + + updatedMeals[mealIndex] = meal.copyWith(portions: updatedPortions); + } + final updatedDiaryDay = diaryDay.copyWith(meals: updatedMeals); + return updatedDiaryDay; + } + return diaryDay; } catch (e) { rethrow; } @@ -75,109 +117,47 @@ class DiaryRepository { int? productNutritionalValueId, required PortionState portionState, }) async { - Map data = ProductInMeal( - productName: productName, - portion: portion, - quantity: quantity, - ).toJson(); + Map data = ProductInMeal(productName: productName, portion: portion, quantity: quantity).toJson(); data['product_nutritional_value_id'] = productNutritionalValueId; data['state'] = portionState.name; - return await diaryService.updateProductInMeal( - diary: diaryId, - typeNo: mealId, - portionModel: portionId, - productInMeal: data, - ); + return await diaryService.updateProductInMeal(diary: diaryId, typeNo: mealId, portionModel: portionId, productInMeal: data); } - Future markEaten({ - required int diaryId, - required int mealId, - required int portionId, - }) async { - return await diaryService.markEaten( - diary: diaryId, - typeNo: mealId, - portionModel: portionId, - ); + Future markEaten({required int diaryId, required int mealId, required int portionId}) async { + return await diaryService.markEaten(diary: diaryId, typeNo: mealId, portionModel: portionId); } - Future unmarkEaten({ - required int diaryId, - required int mealId, - required int portionId, - }) async { - return await diaryService.unmarkEaten( - diary: diaryId, - typeNo: mealId, - portionModel: portionId, - ); + Future unmarkEaten({required int diaryId, required int mealId, required int portionId}) async { + return await diaryService.unmarkEaten(diary: diaryId, typeNo: mealId, portionModel: portionId); } - Future restore({ - required int diaryId, - required int mealId, - required int portionId, - }) async { - return await diaryService.restore( - diary: diaryId, - typeNo: mealId, - portionModel: portionId, - ); + Future restore({required int diaryId, required int mealId, required int portionId}) async { + return await diaryService.restore(diary: diaryId, typeNo: mealId, portionModel: portionId); } - Future deleteProductFromMeal({ - required int diaryId, - required int mealId, - required int portionId, - }) async { - return await diaryService.deleteProductFromMeal( - diary: diaryId, - typeNo: mealId, - portionModel: portionId, - ); + Future deleteProductFromMeal({required int diaryId, required int mealId, required int portionId}) async { + return await diaryService.deleteProductFromMeal(diary: diaryId, typeNo: mealId, portionModel: portionId); } - Future uploadPhotos({ - required int diaryId, - required int mealId, - required List files, - }) async { + Future uploadPhotos({required int diaryId, required int mealId, required List files}) async { try { for (int i = 0; i < files.length; i++) { - await diaryService.uploadPhotoOfMeal( - diary: diaryId, - typeNo: mealId, - photo: files[i], - ); + await diaryService.uploadPhotoOfMeal(diary: diaryId, typeNo: mealId, photo: files[i]); } } catch (e) { rethrow; } } - Future deletePhoto({ - required int diaryId, - required int mealId, - required DiaryPhotoModel photo, - }) async { + Future deletePhoto({required int diaryId, required int mealId, required DiaryPhotoModel photo}) async { try { - await diaryService.deletePhotoOfMeal( - diaryId: diaryId, - typeNo: mealId, - userAttachment: photo.id, - ); + await diaryService.deletePhotoOfMeal(diaryId: diaryId, typeNo: mealId, userAttachment: photo.id); } catch (e) { rethrow; } } - Future finishDiaryDay({ - required int diaryId, - required String wakeUp, - required String retire, - String? comment, - }) async { + Future finishDiaryDay({required int diaryId, required String wakeUp, required String retire, String? comment}) async { return await diaryService.finishDiaryDay( diary: diaryId, finishDiaryDayModel: FinishDiaryDayModel(wakeUp: wakeUp, retire: retire, comment: comment), @@ -192,87 +172,45 @@ class DiaryRepository { required double carbohydrates, }) async { return await diaryService.sendProductToModeration( - data: ProductModerationModel( - name: name, - energy: energy, - proteins: proteins, - fats: fats, - carbohydrates: carbohydrates, - ).toJson(), + data: ProductModerationModel(name: name, energy: energy, proteins: proteins, fats: fats, carbohydrates: carbohydrates).toJson(), ); } - Future fillSatiety({ - required int diaryId, - required int mealId, - required int satiety, - }) async { + Future fillSatiety({required int diaryId, required int mealId, required int satiety}) async { try { - return await diaryService.fillSatiety( - diary: diaryId, - typeNo: mealId, - satiety: satiety, - ); + return await diaryService.fillSatiety(diary: diaryId, typeNo: mealId, satiety: satiety); } catch (e) { rethrow; } } - Future fillMood({ - required int diaryId, - required int mealId, - required int mood, - }) async { + Future fillMood({required int diaryId, required int mealId, required int mood}) async { try { - return await diaryService.fillMood( - diary: diaryId, - typeNo: mealId, - mood: mood, - ); + return await diaryService.fillMood(diary: diaryId, typeNo: mealId, mood: mood); } catch (e) { rethrow; } } - Future fillTime({ - required int diaryId, - required int mealId, - required String time, - }) async { + Future fillTime({required int diaryId, required int mealId, required String time}) async { try { - return await diaryService.fillTime( - diary: diaryId, - typeNo: mealId, - time: time, - ); + return await diaryService.fillTime(diary: diaryId, typeNo: mealId, time: time); } catch (e) { rethrow; } } - Future fillWakeup({ - required int diaryId, - required String time, - }) async { + Future fillWakeup({required int diaryId, required String time}) async { try { - return await diaryService.fillWakeup( - diary: diaryId, - wakeUp: time, - ); + return await diaryService.fillWakeup(diary: diaryId, wakeUp: time); } catch (e) { rethrow; } } - Future fillRetire({ - required int diaryId, - required String time, - }) async { + Future fillRetire({required int diaryId, required String time}) async { try { - return await diaryService.fillRetire( - diary: diaryId, - retire: time, - ); + return await diaryService.fillRetire(diary: diaryId, retire: time); } catch (e) { rethrow; } @@ -287,10 +225,7 @@ class DiaryRepository { } } - Future getDiaryComments({ - required int planId, - required int diaryId, - }) async { + Future getDiaryComments({required int planId, required int diaryId}) async { try { return await diaryService.getDiaryComments(planId: planId, diaryId: diaryId); } catch (e) { @@ -305,4 +240,18 @@ class DiaryRepository { rethrow; } } + + Future getGlossariesIndex({required List ids}) async { + Map filters = {}; + if (ids.isNotEmpty) { + for (int i = 0; i < ids.length; i++) { + filters['filters[id][\$in][$i]'] = ids[i]; + } + } + try { + return await diaryService.getGlossariesIndex(filters: filters); + } catch (e) { + rethrow; + } + } } diff --git a/lib/features/diary/services/diary_service.dart b/lib/features/diary/services/diary_service.dart index d9967200..8cba6f33 100644 --- a/lib/features/diary/services/diary_service.dart +++ b/lib/features/diary/services/diary_service.dart @@ -15,32 +15,20 @@ abstract class DiaryService { factory DiaryService(Dio dio, {String baseUrl}) = _DiaryService; @GET('/api/diary/index/{date}') - Future> getDiaryIndexByDate({ - @Path('date') required String date, - }); + Future> getDiaryIndexByDate({@Path('date') required String date}); @GET('/api/diary/index/{date}') - Future> getDiaryIndexByDateForDietOnly({ - @Path('date') required String date, - }); + Future> getDiaryIndexByDateForDietOnly({@Path('date') required String date}); @GET('/api/diary/{diary}/show') - Future getDiaryDay({ - @Path('diary') required int diary, - }); + Future getDiaryDay({@Path('diary') required int diary}); @GET('/api/diary/{diary}/dailyNutritionData') - Future getNutrition({ - @Path('diary') required int diary, - }); + Future getNutrition({@Path('diary') required int diary}); @POST('/api/diary/{diary}/meals/{type_no}/photo/upload') @MultiPart() - Future uploadPhotoOfMeal({ - @Path('diary') required int diary, - @Path('type_no') required int typeNo, - @Part(name: 'photo') required File photo, - }); + Future uploadPhotoOfMeal({@Path('diary') required int diary, @Path('type_no') required int typeNo, @Part(name: 'photo') required File photo}); @DELETE('/api/diary/{diary}/meals/{type_no}/photo/{user_attachment}/delete') Future deletePhotoOfMeal({ @@ -68,25 +56,13 @@ abstract class DiaryService { }); @POST('/api/diary/{diary}/meals/{type_no}/{portionModel}/mark_eaten') - Future markEaten({ - @Path('diary') required int diary, - @Path('type_no') required int typeNo, - @Path('portionModel') required int portionModel, - }); + Future markEaten({@Path('diary') required int diary, @Path('type_no') required int typeNo, @Path('portionModel') required int portionModel}); @POST('/api/diary/{diary}/meals/{type_no}/{portionModel}/unmark_eaten') - Future unmarkEaten({ - @Path('diary') required int diary, - @Path('type_no') required int typeNo, - @Path('portionModel') required int portionModel, - }); + Future unmarkEaten({@Path('diary') required int diary, @Path('type_no') required int typeNo, @Path('portionModel') required int portionModel}); @POST('/api/diary/{diary}/meals/{type_no}/portions/{portionModel}/restore') - Future restore({ - @Path('diary') required int diary, - @Path('type_no') required int typeNo, - @Path('portionModel') required int portionModel, - }); + Future restore({@Path('diary') required int diary, @Path('type_no') required int typeNo, @Path('portionModel') required int portionModel}); @DELETE('/api/diary/{diary}/meals/{type_no}/portions/{portionModel}/delete') Future deleteProductFromMeal({ @@ -96,63 +72,35 @@ abstract class DiaryService { }); @POST('/api/diary/{diary}/finishDay') - Future finishDiaryDay({ - @Path('diary') required int diary, - @Body(nullToAbsent: true) required FinishDiaryDayModel finishDiaryDayModel, - }); + Future finishDiaryDay({@Path('diary') required int diary, @Body(nullToAbsent: true) required FinishDiaryDayModel finishDiaryDayModel}); @POST('/api/diary/{diary}/meals/{type_no}/satiety/fill') - Future fillSatiety({ - @Path('diary') required int diary, - @Path('type_no') required int typeNo, - @Field() required int satiety, - }); + Future fillSatiety({@Path('diary') required int diary, @Path('type_no') required int typeNo, @Field() required int satiety}); @POST('/api/diary/{diary}/meals/{type_no}/mood/fill') - Future fillMood({ - @Path('diary') required int diary, - @Path('type_no') required int typeNo, - @Field() required int mood, - }); + Future fillMood({@Path('diary') required int diary, @Path('type_no') required int typeNo, @Field() required int mood}); @POST('/api/diary/{diary}/meals/{type_no}/time/fill') - Future fillTime({ - @Path('diary') required int diary, - @Path('type_no') required int typeNo, - @Field() required String time, - }); + Future fillTime({@Path('diary') required int diary, @Path('type_no') required int typeNo, @Field() required String time}); @POST('/api/diary/{diary}/wakeUp/fill') - Future fillWakeup({ - @Path('diary') required int diary, - @Field() required String wakeUp, - }); + Future fillWakeup({@Path('diary') required int diary, @Field() required String wakeUp}); @POST('/api/diary/{diary}/retire/fill') - Future fillRetire({ - @Path('diary') required int diary, - @Field() required String retire, - }); + Future fillRetire({@Path('diary') required int diary, @Field() required String retire}); @POST('/api/diary/product_nutritional_values/sendToModeration') - Future sendProductToModeration({ - @Body() required Map data, - }); + Future sendProductToModeration({@Body() required Map data}); @POST('/api/diary/{id}/consultant/comment') - Future commentUserDay({ - @Path('id') required int id, - @Body() required ConsultantCommentModel body, - }); + Future commentUserDay({@Path('id') required int id, @Body() required ConsultantCommentModel body}); @GET('/api/chosen_plans/{planId}/{diaryId}/comments/show') - Future getDiaryComments({ - @Path() required int planId, - @Path() required int diaryId, - }); + Future getDiaryComments({@Path() required int planId, @Path() required int diaryId}); @POST('/api/diary/{diaryId}/consultant/comment/view') - Future viewCommentOfDiaryDayByConsultant({ - @Path() required int diaryId, - }); + Future viewCommentOfDiaryDayByConsultant({@Path() required int diaryId}); + + @GET('/api/glossaries/index') + Future getGlossariesIndex({@Queries() Map? filters}); } diff --git a/lib/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart b/lib/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart index d124a9f9..d3ba615a 100644 --- a/lib/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart +++ b/lib/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart @@ -12,70 +12,57 @@ class FeedCubit extends Cubit { bool isLoading = false; String? _query; - FeedCubit({ - required this.feedRepository, - }) : super(const FeedState.loading()); + FeedCubit({required this.feedRepository}) : super(const FeedState.loading()); void updateFeedBlogPostList(FeedBlogPostModel? feedBlogPostModel, bool wasLikedByMe) { - if (feedBlogPostModel == null) return; + if (feedBlogPostModel == null || isClosed) return; List feedBlogPostList = [...(state as FeedStateLoaded).feedBlogPostList]; final int index = feedBlogPostList.indexOf(feedBlogPostModel); feedBlogPostList[index] = feedBlogPostModel.copyWith( wasLikedByMe: wasLikedByMe, totalLikes: wasLikedByMe == false ? feedBlogPostList[index].totalLikes - 1 : feedBlogPostList[index].totalLikes + 1, ); - emit( - FeedState.loaded( - feedBlogPostList: feedBlogPostList, - isLoading: isLoading, - ), - ); + if (!isClosed) { + emit(FeedState.loaded(feedBlogPostList: feedBlogPostList, isLoading: isLoading)); + } } - Future loadFeedBlogPostList({ - String? query, - }) async { + Future loadFeedBlogPostList({String? query}) async { try { _query = query; final feedBlogPosts = await feedRepository.getFeedBlogPosts(query: query); lastPage = feedBlogPosts.meta.lastPage; - emit( - FeedState.loaded( - feedBlogPostList: feedBlogPosts.feedBlogPostList, - isLoading: false, - ), - ); + if (!isClosed) { + emit(FeedState.loaded(feedBlogPostList: feedBlogPosts.feedBlogPostList, isLoading: false)); + } } catch (e) { - emit(FeedState.error(errorMessage: e.toString())); + if (!isClosed) { + emit(FeedState.error(errorMessage: e.toString())); + } } } Future loadNextPage() async { - if (currentPage < lastPage && !isLoading) { + if (currentPage < lastPage && !isLoading && !isClosed) { isLoading = true; - emit((state as FeedStateLoaded).copyWith(isLoading: isLoading)); + if (!isClosed) { + emit((state as FeedStateLoaded).copyWith(isLoading: isLoading)); + } try { - if (currentPage < lastPage) { + if (currentPage < lastPage && !isClosed) { currentPage++; - final feedBlogPosts = await feedRepository.getFeedBlogPosts( - page: currentPage, - query: _query, - ); + final feedBlogPosts = await feedRepository.getFeedBlogPosts(page: currentPage, query: _query); isLoading = false; - emit( - FeedState.loaded( - feedBlogPostList: [ - ...(state as FeedStateLoaded).feedBlogPostList, - ...feedBlogPosts.feedBlogPostList, - ], - isLoading: isLoading, - ), - ); + if (!isClosed) { + emit(FeedState.loaded(feedBlogPostList: [...(state as FeedStateLoaded).feedBlogPostList, ...feedBlogPosts.feedBlogPostList], isLoading: isLoading)); + } } } catch (e) { - emit(FeedState.error(errorMessage: e.toString())); + if (!isClosed) { + emit(FeedState.error(errorMessage: e.toString())); + } } } } diff --git a/lib/features/registration/presentation/pages/registration_screen.dart b/lib/features/registration/presentation/pages/registration_screen.dart index 9f30ca33..b00df3ea 100644 --- a/lib/features/registration/presentation/pages/registration_screen.dart +++ b/lib/features/registration/presentation/pages/registration_screen.dart @@ -40,6 +40,12 @@ class _RegistrationScreenState extends State { final pageController = PageController(); final RegistrationCubit registrationCubit = sl(); + @override + void initState() { + super.initState(); + RegistrationAnalyticsService.reportRegistrationStep(step); + } + @override Widget build(BuildContext context) { return BlocProvider( From 89f0a46192d0be084b473250722562a6d5109934 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Tue, 9 Sep 2025 18:58:39 +0300 Subject: [PATCH 03/28] fix(diet): Issue-180 --- .../presentation/diet_preview_screen.dart | 175 ++++-------------- .../presentation/widgets/add_meal_widget.dart | 136 ++++---------- 2 files changed, 69 insertions(+), 242 deletions(-) diff --git a/lib/features/create_diet/presentation/diet_preview_screen.dart b/lib/features/create_diet/presentation/diet_preview_screen.dart index 6c337142..94b21709 100644 --- a/lib/features/create_diet/presentation/diet_preview_screen.dart +++ b/lib/features/create_diet/presentation/diet_preview_screen.dart @@ -26,10 +26,7 @@ class DietPreviewScreen extends StatefulWidget { late final List selectedIndexes; static const name = 'createDietScreenPreview'; static const path = '/create-diet-screen/preview'; - DietPreviewScreen({ - super.key, - required this.diet, - }) { + DietPreviewScreen({super.key, required this.diet}) { selectedIndexes = List.generate(diet.dailySchedule.length, (_) => 0); } @@ -65,11 +62,7 @@ class _DietPreviewScreenState extends State { padding: const EdgeInsets.symmetric(vertical: 12) + (Platform.isIOS ? const EdgeInsets.only(bottom: 16) : EdgeInsets.zero), decoration: BoxDecoration( color: Colors.white, - border: Border( - top: BorderSide( - color: Colors.black.withValues(alpha: 0.1), - ), - ), + border: Border(top: BorderSide(color: Colors.black.withValues(alpha: 0.1))), ), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), @@ -79,34 +72,16 @@ class _DietPreviewScreenState extends State { Row( children: [ Expanded( - child: SizedBox( - child: FloatingActionButton.extended( - heroTag: 'nextButtonRegistration', - highlightElevation: 0, - elevation: 0, - focusElevation: 0, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - backgroundColor: appTheme.greenA700, - onPressed: () {}, - label: Text( - s.startDiet, - style: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), - ), - ), + child: BookmarkButton( + isFavorite: widget.diet.isFavorite, + onTap: () {}, + width: 52, + height: 52, + size: 24, + shape: BoxShape.circle, + color: appTheme.gray50, ), ), - const SizedBox(width: 8), - BookmarkButton( - isFavorite: widget.diet.isFavorite, - onTap: () {}, - width: 52, - height: 52, - size: 24, - shape: BoxShape.circle, - color: appTheme.gray50, - ), ], ), const SizedBox(height: 8), @@ -133,24 +108,15 @@ class _DietPreviewScreenState extends State { filled: true, border: OutlineInputBorder( borderRadius: BorderRadius.circular(16), - borderSide: const BorderSide( - color: Color(0xFFD1D1D6), - width: 1, - ), + borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(16), - borderSide: const BorderSide( - color: Color(0xFFD1D1D6), - width: 1, - ), + borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(16), - borderSide: const BorderSide( - color: Color(0xFFD1D1D6), - width: 1, - ), + borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1), ), ), ), @@ -160,17 +126,10 @@ class _DietPreviewScreenState extends State { GestureDetector( onTap: () {}, child: Container( - decoration: BoxDecoration( - color: textEditingController.text.length > 2 ? appTheme.blueA400 : appTheme.gray50, - shape: BoxShape.circle, - ), + decoration: BoxDecoration(color: textEditingController.text.length > 2 ? appTheme.blueA400 : appTheme.gray50, shape: BoxShape.circle), height: 32, width: 32, - child: Icon( - Icons.send_rounded, - size: 18, - color: textEditingController.text.length > 2 ? appTheme.whiteA700 : appTheme.black, - ), + child: Icon(Icons.send_rounded, size: 18, color: textEditingController.text.length > 2 ? appTheme.whiteA700 : appTheme.black), ), ), ], @@ -212,12 +171,7 @@ class _DietPreviewScreenState extends State { children: [ Stack( children: [ - Image.memory( - imageData, - height: 300, - width: MediaQuery.sizeOf(context).width, - fit: BoxFit.fill, - ), + Image.memory(imageData, height: 300, width: MediaQuery.sizeOf(context).width, fit: BoxFit.fill), // CachedNetworkImage( // imageUrl: baseUrl + '/storage/' + widget.diet.image, // ), @@ -233,10 +187,7 @@ class _DietPreviewScreenState extends State { children: [ SvgPicture.asset('assets/icons/eye.svg', width: 18, height: 12), const SizedBox(width: 8), - Text( - widget.diet.totalViews.toString(), - style: TextStyles.textXSSemibold.copyWith(color: appTheme.black), - ), + Text(widget.diet.totalViews.toString(), style: TextStyles.textXSSemibold.copyWith(color: appTheme.black)), ], ), const SizedBox(width: 12), @@ -252,10 +203,7 @@ class _DietPreviewScreenState extends State { colorFilter: widget.diet.wasLikedByMe ? ColorFilter.mode(appTheme.blueA400, BlendMode.srcIn) : null, ), const SizedBox(width: 8), - Text( - widget.diet.totalLikes.toString(), - style: TextStyles.textXSSemibold.copyWith(color: appTheme.black), - ), + Text(widget.diet.totalLikes.toString(), style: TextStyles.textXSSemibold.copyWith(color: appTheme.black)), ], ), ), @@ -265,10 +213,7 @@ class _DietPreviewScreenState extends State { children: [ SvgPicture.asset('assets/icons/comment2.svg', width: 13, height: 12), const SizedBox(width: 8), - Text( - widget.diet.totalComments.toString(), - style: TextStyles.textXSSemibold.copyWith(color: appTheme.black), - ), + Text(widget.diet.totalComments.toString(), style: TextStyles.textXSSemibold.copyWith(color: appTheme.black)), ], ), ], @@ -279,23 +224,15 @@ class _DietPreviewScreenState extends State { const SizedBox(height: 16), Padding( padding: const EdgeInsets.symmetric(horizontal: 16), - child: Text( - s.about_diet, - style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), - ), + child: Text(s.about_diet, style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)), ), const SizedBox(height: 12), - InfoRow(diet: widget.diet, type: ProductType.diet,), - ExpandableText( - text: widget.diet.details, - ), + InfoRow(diet: widget.diet, type: ProductType.diet), + ExpandableText(text: widget.diet.details), const SizedBox(height: 20), Padding( padding: const EdgeInsets.symmetric(horizontal: 16), - child: AuthorCard( - author: widget.diet.author, - title: s.diet_author, - ), + child: AuthorCard(author: widget.diet.author, title: s.diet_author), ), const SizedBox(height: 16), ], @@ -308,9 +245,7 @@ class _DietPreviewScreenState extends State { child: Center( child: Text( '${s.diet_plan}(${widget.diet.duration.toString()} ${getDayForm(widget.diet.duration, s)})', - style: TextStyles.headingXLSemibold.copyWith( - color: appTheme.black, - ), + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), ), ), ), @@ -320,7 +255,7 @@ class _DietPreviewScreenState extends State { physics: const NeverScrollableScrollPhysics(), shrinkWrap: true, itemBuilder: (context, index) { - return _dayOfPlan(widget.diet.dailySchedule[index] ?? {}, index, s); + return _dayOfPlan(widget.diet.dailySchedule[index + 1] ?? {}, index, s); }, itemCount: widget.diet.dailySchedule.length, ), @@ -331,12 +266,7 @@ class _DietPreviewScreenState extends State { Container( padding: const EdgeInsets.symmetric(horizontal: 16), alignment: Alignment.centerLeft, - child: Text( - '0 ${getCommentForm(widget.diet.totalComments, s)}', - style: TextStyles.textMMedium.copyWith( - color: appTheme.gray500, - ), - ), + child: Text('0 ${getCommentForm(widget.diet.totalComments, s)}', style: TextStyles.textMMedium.copyWith(color: appTheme.gray500)), ), const SizedBox(height: 16), ], @@ -355,14 +285,10 @@ class _DietPreviewScreenState extends State { pressElevation: 0, showCheckmark: false, side: BorderSide.none, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), label: Text((index + 1).toString()), - labelStyle: TextStyles.textXSSemibold.copyWith( - color: index == widget.selectedIndexes[dayNumber] ? Colors.white : appTheme.gray600, - ), + labelStyle: TextStyles.textXSSemibold.copyWith(color: index == widget.selectedIndexes[dayNumber] ? Colors.white : appTheme.gray600), selected: widget.selectedIndexes[dayNumber] == index, backgroundColor: appTheme.gray100, onSelected: (bool selected) { @@ -376,9 +302,7 @@ class _DietPreviewScreenState extends State { return Padding( padding: const EdgeInsets.only(bottom: 8), child: MainBackgroundBouble( - padding: const EdgeInsets.only( - top: 16, - ), + padding: const EdgeInsets.only(top: 16), child: Column( children: [ Padding( @@ -386,31 +310,12 @@ class _DietPreviewScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - '${s.day} ${dayNumber + 1}', - style: TextStyles.headingXLSemibold.copyWith( - color: appTheme.black, - ), - ), - const Padding( - padding: EdgeInsets.symmetric(vertical: 12), - child: CustomDivider(), - ), - Text( - s.meal, - style: TextStyles.textMMedium.copyWith( - color: appTheme.black, - ), - ), + Text('${s.day} ${dayNumber + 1}', style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)), + const Padding(padding: EdgeInsets.symmetric(vertical: 12), child: CustomDivider()), + Text(s.meal, style: TextStyles.textMMedium.copyWith(color: appTheme.black)), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - choiceChipWidget(0), - choiceChipWidget(1), - choiceChipWidget(2), - choiceChipWidget(3), - choiceChipWidget(4), - ], + children: [choiceChipWidget(0), choiceChipWidget(1), choiceChipWidget(2), choiceChipWidget(3), choiceChipWidget(4)], ), const SizedBox(height: 8), ListView.builder( @@ -443,20 +348,10 @@ class _DietPreviewScreenState extends State { children: [ SizedBox( width: MediaQuery.sizeOf(context).width * 0.6, - child: Text( - daily.name, - style: TextStyles.textMMedium.copyWith( - color: appTheme.black, - ), - ), + child: Text(daily.name, style: TextStyles.textMMedium.copyWith(color: appTheme.black)), ), const Spacer(), - Text( - '${daily.quantity} ${getLocalizedMeasurementUnits(s, daily.portion)}', - style: TextStyles.textMSemibold.copyWith( - color: appTheme.black, - ), - ), + Text('${daily.quantity} ${getLocalizedMeasurementUnits(s, daily.portion)}', style: TextStyles.textMSemibold.copyWith(color: appTheme.black)), ], ), ); diff --git a/lib/features/create_diet/presentation/widgets/add_meal_widget.dart b/lib/features/create_diet/presentation/widgets/add_meal_widget.dart index 8f125abd..ececed87 100644 --- a/lib/features/create_diet/presentation/widgets/add_meal_widget.dart +++ b/lib/features/create_diet/presentation/widgets/add_meal_widget.dart @@ -46,21 +46,14 @@ class _AddMealWidgetState extends State { @override Widget build(BuildContext context) { return Padding( - padding: const EdgeInsets.all(16) + - EdgeInsets.only( - bottom: MediaQuery.of(context).viewInsets.bottom, - ), + padding: const EdgeInsets.all(16) + EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ const Text( 'Добавление блюда', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w600, - color: Color(0xFF323F4B), - ), + style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600, color: Color(0xFF323F4B)), ), const SizedBox(height: 16), Row( @@ -73,21 +66,17 @@ class _AddMealWidgetState extends State { autofocus: true, hintText: 'Введите название продукта/блюда', onChanged: (query) { - EasyDebounce.debounce( - 'glossary-search', - const Duration(milliseconds: 250), - () async { + EasyDebounce.debounce('glossary-search', const Duration(milliseconds: 250), () async { + selectedGlossary = null; + if (query.isEmpty) { + glossariesList = []; selectedGlossary = null; - if (query.isEmpty) { - glossariesList = []; - selectedGlossary = null; - setState(() {}); - return; - } - glossariesList = await widget.createDietCubit.getGlossaryListByQuery(query); setState(() {}); - }, - ); + return; + } + glossariesList = await widget.createDietCubit.getGlossaryListByQuery(query); + setState(() {}); + }); }, ), ), @@ -100,22 +89,20 @@ class _AddMealWidgetState extends State { hintText: selectedGlossary == null ? '100' : selectedGlossary!.grInPortion == 0 - ? '100' - : '1', + ? '100' + : '1', suffixText: selectedGlossary == null ? 'гр' : selectedGlossary?.grInPortion == 0 - ? 'гр' - : 'шт', + ? 'гр' + : 'шт', ), ), ], ), glossariesList.isEmpty ? const SizedBox(height: 16) : const SizedBox.shrink(), Container( - constraints: const BoxConstraints( - maxHeight: 256, - ), + constraints: const BoxConstraints(maxHeight: 256), child: ListView.separated( physics: const BouncingScrollPhysics(), shrinkWrap: true, @@ -130,28 +117,12 @@ class _AddMealWidgetState extends State { setState(() {}); FocusManager.instance.primaryFocus?.nextFocus(); }, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), - ), - title: Text( - glossariesList[i].name, - style: const TextStyle( - height: 1, - ), - ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + title: Text(glossariesList[i].name, style: const TextStyle(height: 1)), subtitle: glossariesList[i].grInPortion == 0 ? null - : Text( - '${glossariesList[i].grInPortion} г', - style: TextStyle( - height: 1, - color: Colors.grey.shade600, - ), - ), - trailing: Icon( - Icons.add_rounded, - color: Colors.grey.shade400, - ), + : Text('${glossariesList[i].grInPortion} г', style: TextStyle(height: 1, color: Colors.grey.shade600)), + trailing: Icon(Icons.add_rounded, color: Colors.grey.shade400), contentPadding: const EdgeInsets.only(left: 12), ); }, @@ -166,25 +137,15 @@ class _AddMealWidgetState extends State { if (widget.portionIndex != null) InkWell( onTap: () { - widget.createDietCubit.removePortion( - day: widget.dayIndex, - meal: widget.mealIndex, - portionIndex: widget.portionIndex!, - ); + widget.createDietCubit.removePortion(day: widget.dayIndex, meal: widget.mealIndex, portionIndex: widget.portionIndex!); Navigator.of(context).pop(); }, borderRadius: BorderRadius.circular(20), child: Ink( width: 52, height: 52, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - color: const Color(0xFFF6F6F6), - ), - child: const Icon( - Icons.delete_outline_rounded, - color: Colors.red, - ), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: const Color(0xFFF6F6F6)), + child: const Icon(Icons.delete_outline_rounded, color: Colors.red), ), ), if (widget.portionIndex != null) const SizedBox(width: 8), @@ -194,18 +155,11 @@ class _AddMealWidgetState extends State { child: Ink( width: MediaQuery.of(context).size.width - (widget.portionIndex == null ? 32 : 92), height: 52, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - color: appTheme.blueA400, - ), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: appTheme.blueA400), child: Center( child: Text( widget.portionIndex == null ? 'Добавить' : 'Сохранить', - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - color: Colors.white, - ), + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Colors.white), ), ), ), @@ -222,8 +176,8 @@ class _AddMealWidgetState extends State { var measurementUnits = selectedGlossary?.grInPortion == null ? Portion.pcs : selectedGlossary?.grInPortion == 0 - ? Portion.grams - : Portion.pcs; + ? Portion.grams + : Portion.pcs; final glossary = selectedGlossary; if (glossary != null) { final [calories, carbohydrates, proteins, fats] = _getCPFC(glossary, measurementUnits); @@ -235,19 +189,11 @@ class _AddMealWidgetState extends State { portion: measurementUnits, ); if (portionIndex == null) { - widget.createDietCubit.addPortion( - dayIndex: widget.dayIndex, - mealIndex: widget.mealIndex, - portion: portion, - ); + widget.createDietCubit.addPortion(dayIndex: widget.dayIndex, mealIndex: widget.mealIndex, portion: portion); } else { - widget.createDietCubit.updatePortion( - dayIndex: widget.dayIndex, - mealIndex: widget.mealIndex, - portionIndex: portionIndex, - portion: portion, - ); + widget.createDietCubit.updatePortion(dayIndex: widget.dayIndex, mealIndex: widget.mealIndex, portionIndex: portionIndex, portion: portion); } + Navigator.of(context).pop(); } else { Navigator.of(context).pop(); final newGlossary = await showModalBottomSheet( @@ -255,13 +201,8 @@ class _AddMealWidgetState extends State { useRootNavigator: true, context: context, builder: (context) => Padding( - padding: EdgeInsets.only( - bottom: MediaQuery.of(context).viewInsets.bottom, - ), - child: GlossaryModerationRequestModalWidget( - productName: productNameController.text, - diaryCubit: sl(), - ), + padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), + child: GlossaryModerationRequestModalWidget(productName: productNameController.text, diaryCubit: sl()), ), ); if (newGlossary != null) { @@ -275,18 +216,9 @@ class _AddMealWidgetState extends State { portion: measurementUnits, ); if (portionIndex == null) { - widget.createDietCubit.addPortion( - dayIndex: widget.dayIndex, - mealIndex: widget.mealIndex, - portion: portion, - ); + widget.createDietCubit.addPortion(dayIndex: widget.dayIndex, mealIndex: widget.mealIndex, portion: portion); } else { - widget.createDietCubit.updatePortion( - dayIndex: widget.dayIndex, - mealIndex: widget.mealIndex, - portion: portion, - portionIndex: portionIndex, - ); + widget.createDietCubit.updatePortion(dayIndex: widget.dayIndex, mealIndex: widget.mealIndex, portion: portion, portionIndex: portionIndex); } } } From f1f5c796618f13433e6d13b7b4729c00e233667e Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Wed, 10 Sep 2025 15:58:26 +0300 Subject: [PATCH 04/28] fix(notifications): Issue-236 --- lib/core/theme/theme_helper.dart | 2 + .../pages/notifications_settings_screen.dart | 125 +++++++++++------- lib/generated/intl/messages_en.dart | 1 + lib/generated/intl/messages_ru.dart | 1 + lib/generated/l10n.dart | 17 ++- lib/l10n/intl_en.arb | 3 +- lib/l10n/intl_ru.arb | 3 +- 7 files changed, 95 insertions(+), 57 deletions(-) diff --git a/lib/core/theme/theme_helper.dart b/lib/core/theme/theme_helper.dart index 3b2a063d..c0c0af29 100644 --- a/lib/core/theme/theme_helper.dart +++ b/lib/core/theme/theme_helper.dart @@ -202,6 +202,8 @@ class PrimaryColors { Color get darkGray => const Color(0XFFBBBCC0); Color get commentBg => const Color(0XFFFFFDD8); + + Color get cardNotificationsBG => const Color(0xFFA4A4A4); } PrimaryColors get appTheme => ThemeHelper().themeColor(); diff --git a/lib/features/menu/presentation/pages/notifications_settings_screen.dart b/lib/features/menu/presentation/pages/notifications_settings_screen.dart index 1c1065a3..e2afa516 100644 --- a/lib/features/menu/presentation/pages/notifications_settings_screen.dart +++ b/lib/features/menu/presentation/pages/notifications_settings_screen.dart @@ -3,9 +3,11 @@ import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/features/menu/models/notification_settings_model.dart'; import 'package:eatvio_app/features/menu/presentation/bloc/notification_settings_cubit/notification_settings_cubit.dart'; import 'package:eatvio_app/features/menu/presentation/bloc/notification_settings_cubit/notification_settings_state.dart'; +import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/flutter_svg.dart'; class NotificationsSettingsScreen extends StatelessWidget { const NotificationsSettingsScreen({super.key}); @@ -21,8 +23,10 @@ class NotificationsSettingsContent extends StatelessWidget { @override Widget build(BuildContext context) { + final s = S.of(context); + return Scaffold( - appBar: AppBar(title: const Text('Уведомления')), + appBar: AppBar(title: Text(s.notifications)), body: BlocBuilder( builder: (context, state) { if (state is NotificationSettingsStateLoading) { @@ -58,7 +62,7 @@ class NotificationsSettingsContent extends StatelessWidget { _tile( padding: const EdgeInsets.symmetric(horizontal: 16), onTap: () {}, - title: 'Уведомление о новых обновлениях', + title: 'Новые обновления', subtitle: null, trailing: CupertinoSwitch( value: notificationSettings.newUpdatesNotification, @@ -72,7 +76,7 @@ class NotificationsSettingsContent extends StatelessWidget { _tile( padding: const EdgeInsets.symmetric(horizontal: 16), onTap: () {}, - title: 'Уведомления о взвешивании', + title: 'Напоминать о взвешивании', subtitle: null, trailing: CupertinoSwitch( value: notificationSettings.weightMeasurementsNotification, @@ -111,27 +115,29 @@ class NotificationsSettingsContent extends StatelessWidget { ), ), _divider(context), - const SizedBox(height: 16), _buildDiaryRemindersSection(context, notificationSettings.diaryReminderNotifications, cubit), ], ); } Widget _buildDiaryRemindersSection(BuildContext context, DiaryReminderNotifications diaryReminderNotifications, NotificationSettingsCubit cubit) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.all(16), - child: Text( - 'Напоминания о дневнике', - style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: appTheme.blueA400), + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Напоминание о заполнении дневника', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500, color: appTheme.cardNotificationsBG), ), - ), - _buildReminderTile(context, 'Первое напоминание', diaryReminderNotifications.firstReminder, 'first_reminder', cubit), - _buildReminderTile(context, 'Второе напоминание', diaryReminderNotifications.secondReminder, 'second_reminder', cubit), - _buildReminderTile(context, 'Третье напоминание', diaryReminderNotifications.thirdReminder, 'third_reminder', cubit), - ], + const SizedBox(height: 8), + _buildReminderTile(context, '№1 Напоминание о приеме пищи', diaryReminderNotifications.firstReminder, 'first_reminder', cubit), + const SizedBox(height: 8), + _buildReminderTile(context, '№2 Напоминание о приеме пищи', diaryReminderNotifications.secondReminder, 'second_reminder', cubit), + const SizedBox(height: 8), + _buildReminderTile(context, '№3 Напоминание о приеме пищи', diaryReminderNotifications.thirdReminder, 'third_reminder', cubit), + ], + ), ); } @@ -147,41 +153,58 @@ class NotificationsSettingsContent extends StatelessWidget { } Widget _buildReminderTile(BuildContext context, String title, ReminderSettings reminder, String reminderType, NotificationSettingsCubit cubit) { - return _tile( - padding: const EdgeInsets.symmetric(horizontal: 16), - onTap: reminder.enabled - ? () { - _showTimePicker(context, reminder, reminderType, cubit); - } - : null, - title: title, - subtitle: reminder.enabled - ? Row( - children: [ - Icon(Icons.access_time, size: 16, color: appTheme.blueA400), - const SizedBox(width: 8), - Text( - '${reminder.localTimeHours.toString().padLeft(2, '0')}:${reminder.localTimeMins.toString().padLeft(2, '0')}', - style: TextStyle(fontSize: 14, color: appTheme.blueA400, fontWeight: FontWeight.w500), - ), - const SizedBox(width: 4), - const Text('(нажмите для изменения)', style: TextStyle(fontSize: 12, color: Colors.grey)), - ], - ) - : null, - trailing: Row( - mainAxisSize: MainAxisSize.min, + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(12), color: appTheme.dividerColor), + child: Column( children: [ - CupertinoSwitch( - value: reminder.enabled, - activeTrackColor: appTheme.blueA400, - onChanged: (newValue) { - _updateReminderSetting(cubit, reminderType, { - 'enabled': newValue, - 'local_time_hours': reminder.localTimeHours, - 'local_time_mins': reminder.localTimeMins, - }); - }, + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + reminderType == 'third_reminder' ? SvgPicture.asset('assets/icons/retire.svg') : SvgPicture.asset('assets/icons/wakeup.svg'), + const SizedBox(width: 8), + Text( + title, + style: const TextStyle(color: Color(0xFF323F4B), fontSize: 16, fontWeight: FontWeight.w500), + ), + ], + ), + const SizedBox(height: 8), + Row( + mainAxisSize: MainAxisSize.max, + children: [ + Expanded( + child: GestureDetector( + onTap: reminder.enabled + ? () { + _showTimePicker(context, reminder, reminderType, cubit); + } + : null, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration(color: reminder.enabled ? Colors.white : Colors.white.withAlpha((255 * 0.7).toInt()), borderRadius: BorderRadius.circular(48)), + child: Center( + child: Text( + '${reminder.localTimeHours.toString().padLeft(2, '0')}:${reminder.localTimeMins.toString().padLeft(2, '0')}', + style: TextStyle(fontSize: 17, color: appTheme.gray, fontWeight: FontWeight.w500), + ), + ), + ), + ), + ), + const SizedBox(width: 8), + CupertinoSwitch( + value: reminder.enabled, + activeTrackColor: appTheme.blueA400, + onChanged: (newValue) { + _updateReminderSetting(cubit, reminderType, { + 'enabled': newValue, + 'local_time_hours': reminder.localTimeHours, + 'local_time_mins': reminder.localTimeMins, + }); + }, + ), + ], ), ], ), diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart index 373b48eb..6403bc49 100644 --- a/lib/generated/intl/messages_en.dart +++ b/lib/generated/intl/messages_en.dart @@ -511,6 +511,7 @@ class MessageLookup extends MessageLookupByLibrary { "not_liked_groceries": MessageLookupByLibrary.simpleMessage( "Не любимые продукты", ), + "notifications": MessageLookupByLibrary.simpleMessage("Уведомления"), "ok": MessageLookupByLibrary.simpleMessage("Хорошо"), "one_option": MessageLookupByLibrary.simpleMessage("1 опция"), "option": MessageLookupByLibrary.simpleMessage("Опция"), diff --git a/lib/generated/intl/messages_ru.dart b/lib/generated/intl/messages_ru.dart index 82821c9a..b7db0a45 100644 --- a/lib/generated/intl/messages_ru.dart +++ b/lib/generated/intl/messages_ru.dart @@ -241,6 +241,7 @@ class MessageLookup extends MessageLookupByLibrary { ), "no": MessageLookupByLibrary.simpleMessage("Нет"), "no_account": MessageLookupByLibrary.simpleMessage("У меня нет аккаунта"), + "notifications": MessageLookupByLibrary.simpleMessage("Уведомления"), "one_option": MessageLookupByLibrary.simpleMessage("1 опция"), "option": MessageLookupByLibrary.simpleMessage("Опция"), "option_content": MessageLookupByLibrary.simpleMessage("Содержание опции"), diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index 25d71d68..105b40c7 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -28,10 +28,9 @@ class S { static const AppLocalizationDelegate delegate = AppLocalizationDelegate(); static Future load(Locale locale) { - final name = - (locale.countryCode?.isEmpty ?? false) - ? locale.languageCode - : locale.toString(); + final name = (locale.countryCode?.isEmpty ?? false) + ? locale.languageCode + : locale.toString(); final localeName = Intl.canonicalizedLocale(name); return initializeMessages(localeName).then((_) { Intl.defaultLocale = localeName; @@ -3469,6 +3468,16 @@ class S { args: [], ); } + + /// `Уведомления` + String get notifications { + return Intl.message( + 'Уведомления', + name: 'notifications', + desc: '', + args: [], + ); + } } class AppLocalizationDelegate extends LocalizationsDelegate { diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 7424fe87..c6898871 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -411,6 +411,7 @@ "itCantBeRecovered": "Его нельзя будет восстановить", "delete": "Удалить", "cannotDeleteAccount": "Не получилось удалить ваш аккаунт", - "resend": "Отправить повторно" + "resend": "Отправить повторно", + "notifications": "Уведомления" } diff --git a/lib/l10n/intl_ru.arb b/lib/l10n/intl_ru.arb index 221d36ff..b52f418f 100644 --- a/lib/l10n/intl_ru.arb +++ b/lib/l10n/intl_ru.arb @@ -213,5 +213,6 @@ "my_plans": "Мои планы", "from": "от", "user_data": "Данные пользователя", - "infoAboutOrder": "Информация о заказе" + "infoAboutOrder": "Информация о заказе", + "notifications": "Уведомления" } From 0a5d78be1f1af1e646ed108c2791d582d9635564 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Wed, 10 Sep 2025 16:45:22 +0300 Subject: [PATCH 05/28] fix: ver upd --- ios/Runner.xcodeproj/project.pbxproj | 6 +++--- ios/Runner/Info.plist | 2 +- pubspec.yaml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 7d740632..198e6f7b 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -388,7 +388,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 71; + CURRENT_PROJECT_VERSION = 72; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -538,7 +538,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 71; + CURRENT_PROJECT_VERSION = 72; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -576,7 +576,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 71; + CURRENT_PROJECT_VERSION = 72; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 5cba2fb5..b05e5fac 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -21,7 +21,7 @@ CFBundleSignature ???? CFBundleVersion - 71 + 72 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/pubspec.yaml b/pubspec.yaml index ae850a35..acf68969 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: eatvio_app description: A new Flutter project. -version: 1.2.11+71 +version: 1.2.12+72 publish_to: 'none' environment: From 5c911707a3e59fa31da645e49bbe6cb882483bd7 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Wed, 10 Sep 2025 17:36:46 +0300 Subject: [PATCH 06/28] fix(comments): Issue-548 --- .../comments_actions_bottom_sheet.dart | 116 +++++++----------- .../presentation/widgets/comments_widget.dart | 52 ++------ lib/generated/intl/messages_en.dart | 3 + lib/generated/intl/messages_ru.dart | 3 + lib/generated/l10n.dart | 10 ++ lib/l10n/intl_en.arb | 1 + lib/l10n/intl_ru.arb | 1 + 7 files changed, 68 insertions(+), 118 deletions(-) diff --git a/lib/core/comments/presentation/comments_actions_bottom_sheet.dart b/lib/core/comments/presentation/comments_actions_bottom_sheet.dart index 3c1c3859..c4f3e540 100644 --- a/lib/core/comments/presentation/comments_actions_bottom_sheet.dart +++ b/lib/core/comments/presentation/comments_actions_bottom_sheet.dart @@ -18,25 +18,23 @@ void showCommentsActionsBottomSheet( required bool isMyComment, }) { showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (context) => Container( - margin: EdgeInsets.only(left: 8, right: 8, bottom: MediaQuery.of(context).padding.bottom + 20), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - color: appTheme.whiteA700, - ), - child: CommentsActionsBottomSheet( - isMyComment: isMyComment, - reply: reply, - copy: copy, - edit: edit, - delete: delete, - complain: complain, - commentText: commentText, - ), - )); + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) => Container( + margin: EdgeInsets.only(left: 8, right: 8, bottom: MediaQuery.of(context).viewInsets.bottom + MediaQuery.of(context).padding.bottom + 20), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(12), color: appTheme.whiteA700), + child: CommentsActionsBottomSheet( + isMyComment: isMyComment, + reply: reply, + copy: copy, + edit: edit, + delete: delete, + complain: complain, + commentText: commentText, + ), + ), + ); } class CommentsActionsBottomSheet extends StatefulWidget { @@ -82,44 +80,33 @@ class _CommentsActionsBottomSheetState extends State child: Column( mainAxisSize: MainAxisSize.min, children: [ - Row(children: [ - Expanded( - child: CustomTextFormField( - autofocus: true, - controller: controller, - onChanged: (value) => setState(() {}), + Row( + children: [ + Expanded( + child: CustomTextFormField(autofocus: true, controller: controller, onChanged: (value) => setState(() {})), ), - ), - const SizedBox(width: 8), - GestureDetector( - onTap: () { - widget.edit(controller.text); - GoRouter.of(context).pop(); - }, - child: Container( - decoration: BoxDecoration( - color: controller.text.length > 2 ? appTheme.blueA400 : appTheme.gray50, - shape: BoxShape.circle, - ), - height: 32, - width: 32, - child: Icon( - Icons.send_rounded, - size: 18, - color: controller.text.length > 2 ? appTheme.whiteA700 : appTheme.black, + const SizedBox(width: 8), + GestureDetector( + onTap: () { + widget.edit(controller.text); + GoRouter.of(context).pop(); + }, + child: Container( + decoration: BoxDecoration(color: controller.text.length > 2 ? appTheme.blueA400 : appTheme.gray50, shape: BoxShape.circle), + height: 32, + width: 32, + child: Icon(Icons.send_rounded, size: 18, color: controller.text.length > 2 ? appTheme.whiteA700 : appTheme.black), ), ), - ) - ]) + ], + ), ], ), ) : Column( mainAxisSize: MainAxisSize.min, children: [ - const SizedBox( - height: 7, - ), + const SizedBox(height: 7), const BottomSheetHandlerBar(), if (widget.isMyComment) BottomSheetActionWidget( @@ -131,43 +118,22 @@ class _CommentsActionsBottomSheetState extends State }), ) else if (widget.reply != null) - BottomSheetActionWidget( - text: s.reply, - iconPath: 'assets/icons/reply.svg', - onTap: widget.reply!, - ), + BottomSheetActionWidget(text: s.reply, iconPath: 'assets/icons/reply.svg', onTap: widget.reply!), if (widget.reply != null) const CustomDivider(), - BottomSheetActionWidget( - text: s.copy, - iconPath: 'assets/icons/copy2.svg', - onTap: widget.copy, - ), + BottomSheetActionWidget(text: s.copy, iconPath: 'assets/icons/copy2.svg', onTap: widget.copy), if (widget.isMyComment) ...[ if (widget.reply != null) ...[ const CustomDivider(), - BottomSheetActionWidget( - text: s.reply, - iconPath: 'assets/icons/reply.svg', - onTap: widget.reply!, - ), + BottomSheetActionWidget(text: s.reply, iconPath: 'assets/icons/reply.svg', onTap: widget.reply!), ], const CustomDivider(), - BottomSheetActionWidget( - text: s.deleteComment, - iconPath: 'assets/icons/red_trash.svg', - onTap: widget.delete, - ), + BottomSheetActionWidget(text: s.deleteComment, iconPath: 'assets/icons/red_trash.svg', onTap: widget.delete), ] else ...[ const CustomDivider(), - BottomSheetActionWidget( - text: s.complain, - iconPath: 'assets/icons/complain.svg', - onTap: widget.complain, - ), - ] + BottomSheetActionWidget(text: s.complain, iconPath: 'assets/icons/complain.svg', onTap: widget.complain), + ], ], ), ); } } - diff --git a/lib/features/feed/presentation/widgets/comments_widget.dart b/lib/features/feed/presentation/widgets/comments_widget.dart index e36bdf52..3617fe33 100644 --- a/lib/features/feed/presentation/widgets/comments_widget.dart +++ b/lib/features/feed/presentation/widgets/comments_widget.dart @@ -25,13 +25,7 @@ class CommentsWidget extends StatefulWidget { final CommentModel comment; final int? replyId; - const CommentsWidget({ - super.key, - required this.id, - required this.commentSourceType, - required this.comment, - this.replyId, - }); + const CommentsWidget({super.key, required this.id, required this.commentSourceType, required this.comment, this.replyId}); @override State createState() => _CommentsWidgetState(); @@ -47,10 +41,7 @@ class _CommentsWidgetState extends State { _comment(s, widget.comment.data[i]), if (widget.comment.data[i].childrenComments.isNotEmpty) const SizedBox(height: 8), for (int j = 0; j < widget.comment.data[i].childrenComments.length; j++) - Padding( - padding: const EdgeInsets.only(left: 40), - child: _comment(s, widget.comment.data[i].childrenComments[j], 16, true), - ), + Padding(padding: const EdgeInsets.only(left: 40), child: _comment(s, widget.comment.data[i].childrenComments[j], 16, true)), const SizedBox(height: 8), ], const SizedBox(height: 48), @@ -72,7 +63,7 @@ class _CommentsWidgetState extends State { : null, copy: () { Clipboard.setData(ClipboardData(text: comment.body)); - showInfoToast(s.link_was_copied); + showInfoToast(s.text_was_copied); }, edit: (String text) { context.read().updateComment(id: comment.id, body: text); @@ -84,10 +75,7 @@ class _CommentsWidgetState extends State { showModalBottomSheet( context: context, isScrollControlled: true, - builder: (context) => ComplainingBottomSheet( - complainedContentType: ComplainedContent.comment, - id: comment.id, - ), + builder: (context) => ComplainingBottomSheet(complainedContentType: ComplainedContent.comment, id: comment.id), ); }, isMyComment: isMe, @@ -117,9 +105,7 @@ class _CommentsWidgetState extends State { ); }, child: CircleAvatar( - foregroundImage: NetworkImage( - comment.authorProfileIcon.contains('https') ? comment.authorProfileIcon : baseUrl + comment.authorProfileIcon, - ), + foregroundImage: NetworkImage(comment.authorProfileIcon.contains('https') ? comment.authorProfileIcon : baseUrl + comment.authorProfileIcon), backgroundImage: const AssetImage('assets/images/no_avatar.png'), radius: radius, ), @@ -131,24 +117,14 @@ class _CommentsWidgetState extends State { children: [ Text( comment.authorName, - style: TextStyle( - height: 1, - color: appTheme.gray, - fontSize: 14, - fontWeight: FontWeight.w500, - ), + style: TextStyle(height: 1, color: appTheme.gray, fontSize: 14, fontWeight: FontWeight.w500), ), const SizedBox(height: 4), SizedBox( width: isChild ? MediaQuery.of(context).size.width * 0.72 : MediaQuery.of(context).size.width * 0.8, child: Text( comment.body, - style: TextStyle( - height: 1, - color: appTheme.black, - fontSize: 16, - fontWeight: FontWeight.w500, - ), + style: TextStyle(height: 1, color: appTheme.black, fontSize: 16, fontWeight: FontWeight.w500), ), ), const SizedBox(height: 4), @@ -156,12 +132,7 @@ class _CommentsWidgetState extends State { children: [ Text( comment.createdAt.format('dd MMMM', 'ru'), - style: TextStyle( - height: 1, - color: appTheme.gray, - fontSize: 14, - fontWeight: FontWeight.w500, - ), + style: TextStyle(height: 1, color: appTheme.gray, fontSize: 14, fontWeight: FontWeight.w500), ), const SizedBox(width: 16), if (!isChild) @@ -171,12 +142,7 @@ class _CommentsWidgetState extends State { }, child: Text( s.answer, - style: TextStyle( - height: 1, - color: appTheme.gray, - fontSize: 14, - fontWeight: FontWeight.w600, - ), + style: TextStyle(height: 1, color: appTheme.gray, fontSize: 14, fontWeight: FontWeight.w600), ), ), ], diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart index 6403bc49..c054a4b5 100644 --- a/lib/generated/intl/messages_en.dart +++ b/lib/generated/intl/messages_en.dart @@ -694,6 +694,9 @@ class MessageLookup extends MessageLookupByLibrary { "tell_your_schedule": MessageLookupByLibrary.simpleMessage( "Расскажите в свободной форме о том как просыпаетесь, когда едите, когда ложитесь спать:", ), + "text_was_copied": MessageLookupByLibrary.simpleMessage( + "Text copied successfully", + ), "there_is_a_video": MessageLookupByLibrary.simpleMessage("Есть видео"), "three_options": MessageLookupByLibrary.simpleMessage("3 опции"), "time": MessageLookupByLibrary.simpleMessage("Время прохождения"), diff --git a/lib/generated/intl/messages_ru.dart b/lib/generated/intl/messages_ru.dart index b7db0a45..097e1180 100644 --- a/lib/generated/intl/messages_ru.dart +++ b/lib/generated/intl/messages_ru.dart @@ -326,6 +326,9 @@ class MessageLookup extends MessageLookupByLibrary { "tell_your_schedule": MessageLookupByLibrary.simpleMessage( "Расскажите в свободной форме о том как просыпаетесь, когда едите, когда ложитесь спать:", ), + "text_was_copied": MessageLookupByLibrary.simpleMessage( + "Текст успешно скопирован", + ), "three_options": MessageLookupByLibrary.simpleMessage("3 опции"), "toOffer": MessageLookupByLibrary.simpleMessage("Предложить"), "to_draft": MessageLookupByLibrary.simpleMessage("В черновик"), diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index 105b40c7..85f74918 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -1054,6 +1054,16 @@ class S { ); } + /// `Text copied successfully` + String get text_was_copied { + return Intl.message( + 'Text copied successfully', + name: 'text_was_copied', + desc: '', + args: [], + ); + } + /// `Почта успешно скопирована` String get mail_was_copied { return Intl.message( diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index c6898871..57145fb9 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -129,6 +129,7 @@ "carbohydrates": "Углеводы", "answer": "Ответить", "link_was_copied": "Ссылка успешно скопирована", + "text_was_copied": "Text copied successfully", "mail_was_copied": "Почта успешно скопирована", "choose_start_diet_date": "Выберите дату старты диеты. Она отобразиться у Вас в дневнике", "continue_": "Продолжить", diff --git a/lib/l10n/intl_ru.arb b/lib/l10n/intl_ru.arb index b52f418f..e5d6f7c9 100644 --- a/lib/l10n/intl_ru.arb +++ b/lib/l10n/intl_ru.arb @@ -129,6 +129,7 @@ "carbohydrates": "Углеводы", "answer": "Ответить", "link_was_copied": "Ссылка успешно скопирована", + "text_was_copied": "Текст успешно скопирован", "mail_was_copied": "Почта успешно скопирована", "choose_start_diet_date": "Выберите дату старты диеты. Она отобразиться у Вас в дневнике", "continue_": "Продолжить", From ff49562e869bf759029bf5ed667e5ca49455b6c1 Mon Sep 17 00:00:00 2001 From: misha Date: Thu, 11 Sep 2025 18:04:35 +0300 Subject: [PATCH 07/28] feat(views): added view handling for diets, recipes, plans and posts --- .gitignore | 3 +- ios/Runner.xcodeproj/project.pbxproj | 3 + lib/core/const.dart | 2 +- lib/features/feed/models/view_model.dart | 16 ++ .../bloc/feed_cubit/feed_cubit.dart | 8 + .../feed/presentation/pages/feed_screen.dart | 102 +++++++++++- .../feed/presentation/widgets/blog_posts.dart | 26 +-- .../feed/presentation/widgets/diets.dart | 26 +-- .../feed/presentation/widgets/plans.dart | 27 ++-- .../feed/presentation/widgets/recipes.dart | 19 ++- .../feed/repositories/feed_repository.dart | 150 ++++-------------- lib/features/feed/services/feed_service.dart | 6 + 12 files changed, 222 insertions(+), 166 deletions(-) create mode 100644 lib/features/feed/models/view_model.dart diff --git a/.gitignore b/.gitignore index 4f05e3a1..06e89a5e 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,5 @@ *.mocks.dart android/build/ ios/XCBuildData/ -env/.env \ No newline at end of file +env/.env +.env \ No newline at end of file diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 198e6f7b..668075b4 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -410,6 +410,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044"; + SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; @@ -560,6 +561,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044"; + SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -598,6 +600,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044"; + SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; diff --git a/lib/core/const.dart b/lib/core/const.dart index 98f7bc4a..a5f06149 100644 --- a/lib/core/const.dart +++ b/lib/core/const.dart @@ -1,6 +1,6 @@ import 'package:eatvio_app/core/env_config.dart'; -String baseUrl = EnvConfig.baseUrl; +String baseUrl = EnvConfig.devBaseUrl; bool hidePlans = const bool.fromEnvironment('HIDE_PLANS', defaultValue: false); const buildDate = String.fromEnvironment('BUILD_DATE', defaultValue: ''); diff --git a/lib/features/feed/models/view_model.dart b/lib/features/feed/models/view_model.dart new file mode 100644 index 00000000..f04022e9 --- /dev/null +++ b/lib/features/feed/models/view_model.dart @@ -0,0 +1,16 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'view_model.freezed.dart'; +part 'view_model.g.dart'; + +@freezed +abstract class ViewModel with _$ViewModel { + + factory ViewModel({ + required String type, + List? ids, + List? slugs, + }) = _ViewModel; + + factory ViewModel.fromJson(Map json) => _$ViewModelFromJson(json); +} \ No newline at end of file diff --git a/lib/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart b/lib/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart index d3ba615a..39ccdc6a 100644 --- a/lib/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart +++ b/lib/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart @@ -1,7 +1,9 @@ import 'dart:developer'; import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart'; +import 'package:eatvio_app/features/feed/models/view_model.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_state.dart'; +import 'package:eatvio_app/features/feed/presentation/pages/feed_screen.dart'; import 'package:eatvio_app/features/feed/repositories/feed_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -81,4 +83,10 @@ class FeedCubit extends Cubit { Future viewRecipe(String slug) async => await feedRepository.viewRecipe(slug); Future viewDiet(int id) async => await feedRepository.viewDiet(id); Future viewPlan(String slug) async => await feedRepository.viewPlan(slug); + + Future viewContent({required ViewableContentType type, List? ids, List? slugs}) async { + feedRepository.viewContent( + viewModel: ViewModel(type: type.getStringifiedVersion(), ids: ids, slugs: slugs), + ); + } } diff --git a/lib/features/feed/presentation/pages/feed_screen.dart b/lib/features/feed/presentation/pages/feed_screen.dart index ce29b884..2bfaf897 100644 --- a/lib/features/feed/presentation/pages/feed_screen.dart +++ b/lib/features/feed/presentation/pages/feed_screen.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:easy_debounce/easy_debounce.dart'; import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/widgets/add_item_button.dart'; @@ -60,18 +62,19 @@ class _FeedScreenContentState extends State { final consultantsController = TextEditingController(); late final List chipsLabelList; + late final AppLifecycleListener _listener; int _selectedChipIndex = 0; final blogPostsScrollController = ScrollController(); final recipesScrollController = ScrollController(); final dietsScrollController = ScrollController(); final plansScrollController = ScrollController(); final consultantsScrollController = ScrollController(); - + final _feedCubit = sl(); @override void initState() { // Initialize chipsLabelList based on hidePlans setting chipsLabelList = hidePlans ? ['Все', 'Рецепты', 'Диеты', 'Консультанты'] : ['Все', 'Рецепты', 'Диеты', 'Планы питания', 'Консультанты']; - + _listener = AppLifecycleListener(onDetach: _sendViews); consultantsScrollController.addListener(_onScrollConsultants); super.initState(); } @@ -80,6 +83,21 @@ class _FeedScreenContentState extends State { if (consultantsScrollController.offset >= consultantsScrollController.position.maxScrollExtent && !consultantsScrollController.position.outOfRange) {} } + void _sendViews() { + if (postsIds.isNotEmpty) { + _feedCubit.viewContent(type: ViewableContentType.posts, ids: postsIds.toList()); + } + if (plansIds.isNotEmpty) { + _feedCubit.viewContent(type: ViewableContentType.plans, ids: plansIds.toList()); + } + if (dietsIds.isNotEmpty) { + _feedCubit.viewContent(type: ViewableContentType.diets, ids: dietsIds.toList()); + } + if (recipesIds.isNotEmpty) { + _feedCubit.viewContent(type: ViewableContentType.recipes, ids: recipesIds.toList()); + } + } + @override void dispose() { blogPostsScrollController.dispose(); @@ -87,9 +105,64 @@ class _FeedScreenContentState extends State { dietsScrollController.dispose(); plansScrollController.dispose(); consultantsScrollController.dispose(); + for (final map in _visibilityTimers.values) { + for (final t in map.values) { + t.cancel(); + } + map.clear(); + } + _listener.dispose(); + _sendViews(); super.dispose(); } + final Set postsIds = {}; + final Set plansIds = {}; + final Set dietsIds = {}; + final Set recipesIds = {}; + + final Map> _visibilityTimers = { + ViewableContentType.posts: {}, + ViewableContentType.plans: {}, + ViewableContentType.diets: {}, + ViewableContentType.recipes: {}, + }; + + Set _idsForType(ViewableContentType type) { + switch (type) { + case ViewableContentType.posts: + return postsIds; + case ViewableContentType.plans: + return plansIds; + case ViewableContentType.diets: + return dietsIds; + case ViewableContentType.recipes: + return recipesIds; + } + } + + void _handleVisibility({required double visibleFraction, required int id, required ViewableContentType type}) { + final timers = _visibilityTimers[type]!; + final ids = _idsForType(type); + + if (ids.contains(id)) { + timers[id]?.cancel(); + timers.remove(id); + return; + } + + if (visibleFraction >= 0.5) { + if (timers.containsKey(id)) return; + timers[id] = Timer(const Duration(seconds: 1), () { + ids.add(id); + timers.remove(id); + }); + } else { + timers[id]?.cancel(); + timers.remove(id); + } + } + @override Widget build(BuildContext context) { FlutterNativeSplash.remove(); @@ -228,10 +301,10 @@ class _FeedScreenContentState extends State { ]; }, body: switch (_selectedChipIndex) { - 0 => const BlogPosts(), - 1 => const Recipes(), - 2 => const Diets(), - 3 when !hidePlans => const Plans(), + 0 => BlogPosts(handleVisibility: _handleVisibility), + 1 => Recipes(handleVisibility: _handleVisibility), + 2 => Diets(handleVisibility: _handleVisibility), + 3 when !hidePlans => Plans(handleVisibility: _handleVisibility), 3 when hidePlans => const Consultants(), 4 when !hidePlans => const Consultants(), _ => Center(child: Text(chipsLabelList[_selectedChipIndex])), @@ -240,3 +313,20 @@ class _FeedScreenContentState extends State { ); } } + +enum ViewableContentType { + plans, + posts, + diets, + recipes; + + String getStringifiedVersion() => switch (this) { + ViewableContentType.plans => 'plans', + + ViewableContentType.posts => 'blog_posts', + + ViewableContentType.diets => 'diets', + + ViewableContentType.recipes => 'recipes', + }; +} diff --git a/lib/features/feed/presentation/widgets/blog_posts.dart b/lib/features/feed/presentation/widgets/blog_posts.dart index 8c918515..b9da2ecc 100644 --- a/lib/features/feed/presentation/widgets/blog_posts.dart +++ b/lib/features/feed/presentation/widgets/blog_posts.dart @@ -3,13 +3,16 @@ import 'package:eatvio_app/core/widgets/data_error_widget.dart'; import 'package:eatvio_app/core/widgets/loader.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_state.dart'; +import 'package:eatvio_app/features/feed/presentation/pages/feed_screen.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/blog_post_widget.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:visibility_detector/visibility_detector.dart'; class BlogPosts extends StatelessWidget { - const BlogPosts({super.key}); - + const BlogPosts({super.key, required this.handleVisibility}); + final Function({required double visibleFraction,required int id, required ViewableContentType type}) handleVisibility; + @override Widget build(BuildContext context) { return RefreshIndicator( @@ -29,25 +32,26 @@ class BlogPosts extends StatelessWidget { if (index == state.feedBlogPostList.length) { return const Loader(); } - return BlogPostWidget(index: index); + return VisibilityDetector( + onVisibilityChanged: (info) { + final id = state.feedBlogPostList[index].id; + handleVisibility(visibleFraction: info.visibleFraction, id: id, type: ViewableContentType.posts); + }, + key: ValueKey(index), + child: BlogPostWidget(index: index), + ); }, separatorBuilder: (context, index) => const SizedBox(height: 8), itemCount: state.isLoading ? state.feedBlogPostList.length + 1 : state.feedBlogPostList.length, ), ); } else if (state is FeedStateError) { - return DataErrorWidget( - onReload: () => context.read().loadFeedBlogPostList(), - ); + return DataErrorWidget(onReload: () => context.read().loadFeedBlogPostList()); } else { - return Scaffold( - backgroundColor: Colors.grey.shade200, - body: const Loader(), - ); + return Scaffold(backgroundColor: Colors.grey.shade200, body: const Loader()); } }, ), ); } } - diff --git a/lib/features/feed/presentation/widgets/diets.dart b/lib/features/feed/presentation/widgets/diets.dart index c08a82a6..c5e5592a 100644 --- a/lib/features/feed/presentation/widgets/diets.dart +++ b/lib/features/feed/presentation/widgets/diets.dart @@ -3,15 +3,17 @@ import 'package:eatvio_app/core/widgets/data_error_widget.dart'; import 'package:eatvio_app/core/widgets/loader.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_state.dart'; +import 'package:eatvio_app/features/feed/presentation/pages/feed_screen.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/diet_filter_bottom_sheet.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/diets_widget.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/filter_badge.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:visibility_detector/visibility_detector.dart'; class Diets extends StatelessWidget { - const Diets({super.key}); - + const Diets({super.key, required this.handleVisibility}); + final Function({required double visibleFraction, required int id, required ViewableContentType type}) handleVisibility; @override Widget build(BuildContext context) { return RefreshIndicator( @@ -19,10 +21,7 @@ class Diets extends StatelessWidget { child: BlocBuilder( builder: (BuildContext context, DietsState state) { if (state.isLoading) { - return Scaffold( - backgroundColor: Colors.grey.shade200, - body: const Loader(), - ); + return Scaffold(backgroundColor: Colors.grey.shade200, body: const Loader()); } else if (state.errorMessage != null) { return DataErrorWidget(onReload: () => context.read().loadDiets()); } else { @@ -39,7 +38,14 @@ class Diets extends StatelessWidget { shrinkWrap: true, physics: const BouncingScrollPhysics(), itemBuilder: (context, index) { - return DietsWidget(diet: state.dietsList[index], index: index); + return VisibilityDetector( + key: ValueKey(index), + onVisibilityChanged: (info) { + final id = state.dietsList[index].id; + handleVisibility(visibleFraction: info.visibleFraction, id: id, type: ViewableContentType.posts); + }, + child: DietsWidget(diet: state.dietsList[index], index: index), + ); }, separatorBuilder: (context, index) { return const SizedBox(height: 8); @@ -50,11 +56,7 @@ class Diets extends StatelessWidget { Positioned( right: 0, bottom: 74, - child: FilterBadge( - filterBottomSheet: DietFilterBottomSheet( - dietsCubit: context.read(), - ), - ), + child: FilterBadge(filterBottomSheet: DietFilterBottomSheet(dietsCubit: context.read())), ), ], ); diff --git a/lib/features/feed/presentation/widgets/plans.dart b/lib/features/feed/presentation/widgets/plans.dart index 005c9ee9..4a620945 100644 --- a/lib/features/feed/presentation/widgets/plans.dart +++ b/lib/features/feed/presentation/widgets/plans.dart @@ -3,15 +3,17 @@ import 'package:eatvio_app/core/widgets/data_error_widget.dart'; import 'package:eatvio_app/core/widgets/loader.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_state.dart'; +import 'package:eatvio_app/features/feed/presentation/pages/feed_screen.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:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:visibility_detector/visibility_detector.dart'; class Plans extends StatelessWidget { - const Plans({super.key}); - + const Plans({super.key, required this.handleVisibility}); + final Function({required double visibleFraction, required int id, required ViewableContentType type}) handleVisibility; @override Widget build(BuildContext context) { return RefreshIndicator( @@ -19,10 +21,7 @@ class Plans extends StatelessWidget { child: BlocBuilder( builder: (BuildContext context, PlansState state) { if (state.isLoading) { - return Scaffold( - backgroundColor: Colors.grey.shade200, - body: const Loader(), - ); + return Scaffold(backgroundColor: Colors.grey.shade200, body: const Loader()); } else if (state.errorMessage != null) { return DataErrorWidget(onReload: () => context.read().loadPlans()); } @@ -39,9 +38,13 @@ class Plans extends StatelessWidget { shrinkWrap: true, physics: const BouncingScrollPhysics(), itemBuilder: (context, index) { - return PlanWidget( - plan: state.plans[index], - index: index, + return VisibilityDetector( + key: ValueKey(index), + onVisibilityChanged: (info) { + final id = state.plans[index].id; + handleVisibility(visibleFraction: info.visibleFraction, id: id, type: ViewableContentType.posts); + }, + child: PlanWidget(plan: state.plans[index], index: index), ); }, separatorBuilder: (context, index) { @@ -53,11 +56,7 @@ class Plans extends StatelessWidget { Positioned( right: 0, bottom: 74, - child: FilterBadge( - filterBottomSheet: PlansFilterBottomSheet( - plansCubit: context.read(), - ), - ), + child: FilterBadge(filterBottomSheet: PlansFilterBottomSheet(plansCubit: context.read())), ), ], ); diff --git a/lib/features/feed/presentation/widgets/recipes.dart b/lib/features/feed/presentation/widgets/recipes.dart index 914c31fe..d286949e 100644 --- a/lib/features/feed/presentation/widgets/recipes.dart +++ b/lib/features/feed/presentation/widgets/recipes.dart @@ -3,13 +3,15 @@ import 'package:eatvio_app/core/widgets/data_error_widget.dart'; import 'package:eatvio_app/core/widgets/loader.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes_state.dart'; +import 'package:eatvio_app/features/feed/presentation/pages/feed_screen.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/recipes_widget.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:visibility_detector/visibility_detector.dart'; class Recipes extends StatelessWidget { - const Recipes({super.key}); - + const Recipes({super.key, required this.handleVisibility}); + final Function({required double visibleFraction,required int id, required ViewableContentType type}) handleVisibility; @override Widget build(BuildContext context) { return RefreshIndicator( @@ -40,9 +42,16 @@ class Recipes extends StatelessWidget { shrinkWrap: true, physics: const BouncingScrollPhysics(), itemBuilder: (context, index) { - return RecipesWidget( - recipe: state.recipesList[index], - index: index, + return VisibilityDetector( + key: ValueKey(index), + onVisibilityChanged: (info) { + final id = state.recipesList[index].id; + handleVisibility(visibleFraction: info.visibleFraction, id: id, type: ViewableContentType.posts); + }, + child: RecipesWidget( + recipe: state.recipesList[index], + index: index, + ), ); }, separatorBuilder: (context, index) { diff --git a/lib/features/feed/repositories/feed_repository.dart b/lib/features/feed/repositories/feed_repository.dart index e6e95969..108a8b11 100644 --- a/lib/features/feed/repositories/feed_repository.dart +++ b/lib/features/feed/repositories/feed_repository.dart @@ -5,103 +5,55 @@ import 'package:eatvio_app/features/feed/models/feed_blog_post_settings_model.da import 'package:eatvio_app/features/feed/models/feed_consultant_model.dart'; import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart'; import 'package:eatvio_app/features/feed/models/recipes_model.dart'; +import 'package:eatvio_app/features/feed/models/view_model.dart'; import 'package:eatvio_app/features/feed/services/feed_service.dart'; class FeedRepository { final FeedService feedService; - FeedRepository({ - required this.feedService, - }); + FeedRepository({required this.feedService}); - Future getFeedBlogPosts({ - String? query, - int page = 1, - }) async { + Future getFeedBlogPosts({String? query, int page = 1}) async { try { - return (await feedService.getBlogPostList( - page: page, - query: query, - )); + return (await feedService.getBlogPostList(page: page, query: query)); } catch (e) { rethrow; } } - Future getBlogPost({ - required int id - }) async { + Future getBlogPost({required int id}) async { try { - return await feedService.getBlogPost( - postId: id - ); + return await feedService.getBlogPost(postId: id); } catch (e) { rethrow; } } - Future> getRecipes({ - String? query, - List? tags, - int page = 1, - }) async { + Future> getRecipes({String? query, List? tags, int page = 1}) async { try { - return (await feedService.getRecipes( - query: query, - tags: tags, - page: page, - )) - .recipesList; + return (await feedService.getRecipes(query: query, tags: tags, page: page)).recipesList; } catch (e) { rethrow; } } - Future getDiets({ - int page = 1, - String? query, - String? types, - int? duration, - String? sorting, - }) async { + Future getDiets({int page = 1, String? query, String? types, int? duration, String? sorting}) async { try { - return (await feedService.getDiets( - page: page, - query: query, - types: types, - duration: duration, - sorting: sorting, - )); + return (await feedService.getDiets(page: page, query: query, types: types, duration: duration, sorting: sorting)); } catch (e) { rethrow; } } - Future getPlans({ - int page = 1, - String? query, - String? types, - int? duration, - int? minPrice, - int? maxPrice, - }) async { + Future getPlans({int page = 1, String? query, String? types, int? duration, int? minPrice, int? maxPrice}) async { try { - return (await feedService.getPlans( - page: page, - query: query, - types: types, - duration: duration, - minPrice: minPrice, - maxPrice: maxPrice, - )); + return (await feedService.getPlans(page: page, query: query, types: types, duration: duration, minPrice: minPrice, maxPrice: maxPrice)); } catch (e) { rethrow; } } - Future getBlogPostComments({ - required String id, - }) async { + Future getBlogPostComments({required String id}) async { try { return (await feedService.getBlogPostComments(id: id)); } catch (e) { @@ -109,25 +61,15 @@ class FeedRepository { } } - Future sendBlogPostComment({ - required String id, - required String commentText, - int? parentCommentId, - }) async { + Future sendBlogPostComment({required String id, required String commentText, int? parentCommentId}) async { try { - await feedService.sendBlogPostComment( - id: id, - body: commentText, - parentCommentId: parentCommentId, - ); + await feedService.sendBlogPostComment(id: id, body: commentText, parentCommentId: parentCommentId); } catch (e) { rethrow; } } - Future getDietsComments({ - required String id, - }) async { + Future getDietsComments({required String id}) async { try { return (await feedService.getDietsComments(id: id)); } catch (e) { @@ -135,25 +77,15 @@ class FeedRepository { } } - Future sendDietsComment({ - required String id, - required String commentText, - int? parentCommentId, - }) async { + Future sendDietsComment({required String id, required String commentText, int? parentCommentId}) async { try { - await feedService.sendDietsComment( - id: id, - body: commentText, - parentCommentId: parentCommentId, - ); + await feedService.sendDietsComment(id: id, body: commentText, parentCommentId: parentCommentId); } catch (e) { rethrow; } } - Future getRecipesComments({ - required String id, - }) async { + Future getRecipesComments({required String id}) async { try { return (await feedService.getRecipesComments(id: id)); } catch (e) { @@ -161,25 +93,15 @@ class FeedRepository { } } - Future sendRecipesComment({ - required String id, - required String commentText, - int? parentCommentId, - }) async { + Future sendRecipesComment({required String id, required String commentText, int? parentCommentId}) async { try { - await feedService.sendRecipesComment( - id: id, - body: commentText, - parentCommentId: parentCommentId, - ); + await feedService.sendRecipesComment(id: id, body: commentText, parentCommentId: parentCommentId); } catch (e) { rethrow; } } - Future getPlansComments({ - required String slug, - }) async { + Future getPlansComments({required String slug}) async { try { return (await feedService.getPlansComments(slug: slug)); } catch (e) { @@ -187,25 +109,15 @@ class FeedRepository { } } - Future sendPlansComment({ - required String slug, - required String commentText, - int? parentCommentId, - }) async { + Future sendPlansComment({required String slug, required String commentText, int? parentCommentId}) async { try { - await feedService.sendPlansComment( - slug: slug, - body: commentText, - parentCommentId: parentCommentId, - ); + await feedService.sendPlansComment(slug: slug, body: commentText, parentCommentId: parentCommentId); } catch (e) { rethrow; } } - Future likeBlogPost({ - required int id, - }) async { + Future likeBlogPost({required int id}) async { try { await feedService.likeBlogPost(id: id); } catch (e) { @@ -213,9 +125,7 @@ class FeedRepository { } } - Future unlikedBlogPost({ - required int id, - }) async { + Future unlikedBlogPost({required int id}) async { try { await feedService.unlikedBlogPost(id: id); } catch (e) { @@ -262,4 +172,12 @@ class FeedRepository { rethrow; } } + + Future viewContent({required ViewModel viewModel}) async { + try { + await feedService.viewContent(viewModel: viewModel); + } catch (e) { + rethrow; + } + } } diff --git a/lib/features/feed/services/feed_service.dart b/lib/features/feed/services/feed_service.dart index cb5de3d0..265c2d38 100644 --- a/lib/features/feed/services/feed_service.dart +++ b/lib/features/feed/services/feed_service.dart @@ -6,6 +6,7 @@ import 'package:eatvio_app/features/feed/models/feed_blog_post_settings_model.da import 'package:eatvio_app/features/feed/models/feed_consultant_model.dart'; import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart'; import 'package:eatvio_app/features/feed/models/recipes_pagination_model.dart'; +import 'package:eatvio_app/features/feed/models/view_model.dart'; import 'package:retrofit/retrofit.dart'; part 'feed_service.g.dart'; @@ -131,4 +132,9 @@ abstract class FeedService { Future viewPlan({ @Path() required String slug, }); + + @POST('/api/views') + Future viewContent({ + @Body() required ViewModel viewModel, + }); } From f3dbbf35377e236deeae6b3d7733321e68075cd8 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Thu, 11 Sep 2025 19:51:16 +0300 Subject: [PATCH 08/28] fix(settings): Issue-236 --- .../presentation/pages/settings_screen.dart | 55 +------------------ 1 file changed, 3 insertions(+), 52 deletions(-) diff --git a/lib/features/settings/presentation/pages/settings_screen.dart b/lib/features/settings/presentation/pages/settings_screen.dart index 1c304f29..41066d54 100644 --- a/lib/features/settings/presentation/pages/settings_screen.dart +++ b/lib/features/settings/presentation/pages/settings_screen.dart @@ -58,8 +58,8 @@ class _SettingsScreenState extends State { children: [ _additionalInfo(state), const SizedBox(height: 12), - // _notifications(state), - // const SizedBox(height: 12), + _notifications(state), + const SizedBox(height: 12), _becomeConsultantButton(state), _deleteAccountButton(state, settingsCubit), ], @@ -147,63 +147,14 @@ class _SettingsScreenState extends State { ); } - // ignore: unused_element Column _notifications(SettingsStateLoaded state) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Padding( padding: EdgeInsets.symmetric(horizontal: 16), - child: Text('Уведомления', style: TextStyle(fontSize: 16, height: 1, color: Colors.grey)), + child: Text('При использовании дневника', style: TextStyle(fontSize: 16, height: 1, color: Colors.grey)), ), - _tile( - padding: const EdgeInsets.symmetric(horizontal: 16), - onTap: () { - setState(() { - updatesNotificationsSwitchState = !updatesNotificationsSwitchState; - }); - settingsCubit.updateProfileSettings(profileSettingsModel: state.profileSettings.copyWith(newUpdatesNotifications: updatesNotificationsSwitchState)); - }, - title: 'Уведомление о новых обновлениях', - trailing: CupertinoSwitch( - value: updatesNotificationsSwitchState, - activeTrackColor: appTheme.blueA400, - onChanged: (newValue) { - setState(() { - updatesNotificationsSwitchState = newValue; - }); - settingsCubit.updateProfileSettings( - profileSettingsModel: state.profileSettings.copyWith(newUpdatesNotifications: updatesNotificationsSwitchState), - ); - }, - ), - ), - _divider(context), - _tile( - padding: const EdgeInsets.symmetric(horizontal: 16), - onTap: () { - setState(() { - fillDiaryNotificationsSwitchState = !fillDiaryNotificationsSwitchState; - }); - settingsCubit.updateProfileSettings( - profileSettingsModel: state.profileSettings.copyWith(diaryReminderNotifications: fillDiaryNotificationsSwitchState), - ); - }, - title: 'Напоминание о заполнении дневника', - trailing: CupertinoSwitch( - value: fillDiaryNotificationsSwitchState, - activeTrackColor: appTheme.blueA400, - onChanged: (newValue) { - setState(() { - fillDiaryNotificationsSwitchState = newValue; - }); - settingsCubit.updateProfileSettings( - profileSettingsModel: state.profileSettings.copyWith(diaryReminderNotifications: fillDiaryNotificationsSwitchState), - ); - }, - ), - ), - _divider(context), _tile( padding: const EdgeInsets.symmetric(horizontal: 16), onTap: () { From b840c936bf9dc52f11600d1db7d177c9dcfcc3a8 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Thu, 11 Sep 2025 20:02:48 +0300 Subject: [PATCH 09/28] fix(finances): Issue-332 --- .../presentation/user_purchases_screen.dart | 88 +++++-------------- 1 file changed, 24 insertions(+), 64 deletions(-) diff --git a/lib/features/finances_and_consultations_archive/presentation/user_purchases_screen.dart b/lib/features/finances_and_consultations_archive/presentation/user_purchases_screen.dart index 6f137211..67d4652f 100644 --- a/lib/features/finances_and_consultations_archive/presentation/user_purchases_screen.dart +++ b/lib/features/finances_and_consultations_archive/presentation/user_purchases_screen.dart @@ -33,21 +33,12 @@ class _UserPurchasesScreen extends State { return Scaffold( backgroundColor: const Color(0xFFF0F2F5), appBar: AppBar( - shape: const RoundedRectangleBorder( - borderRadius: BorderRadiusDirectional.vertical( - bottom: Radius.circular(12), - ), - ), - title: Text( - 'Финансы', - style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), - ), + shape: const RoundedRectangleBorder(borderRadius: BorderRadiusDirectional.vertical(bottom: Radius.circular(12))), + title: Text('Финансы', style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)), centerTitle: true, ), body: state.isLoading - ? const Center( - child: Loader(), - ) + ? const Center(child: Loader()) : SingleChildScrollView( padding: const EdgeInsets.symmetric(vertical: 8), child: Column( @@ -67,10 +58,7 @@ class _UserPurchasesScreen extends State { .map( (value) => DropdownMenuItem( value: value, - child: Text( - value.name, - style: TextStyles.textMMedium.copyWith(color: appTheme.black), - ), + child: Text(value.name, style: TextStyles.textMMedium.copyWith(color: appTheme.black)), ), ) .toList(), @@ -105,60 +93,41 @@ class _UserPurchasesScreen extends State { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Builder(builder: (context) { - final idLen = state.purchasesModels[index].bankId.length; - return Text( - '${state.purchasesModels[index].bankId.substring(0, 4)}...${state.purchasesModels[index].bankId.substring(idLen - 4, idLen)} ', - style: TextStyles.textSMedium.copyWith(color: appTheme.black), - ); - }), + Builder( + builder: (context) { + final idLen = state.purchasesModels[index].bankId.length; + return Text( + '${state.purchasesModels[index].bankId.substring(0, 4)}...${state.purchasesModels[index].bankId.substring(idLen - 4, idLen)} ', + style: TextStyles.textSMedium.copyWith(color: appTheme.black), + ); + }, + ), Text( - state.purchasesModels[index].startDate.format('yyyy.MM.dd'), - style: const TextStyle( - color: Color(0xFF6D6F71), - height: 1, - fontWeight: FontWeight.w500, - ), + state.purchasesModels[index].startDate.format('dd.MM.yyyy'), + style: const TextStyle(color: Color(0xFF6D6F71), height: 1, fontWeight: FontWeight.w500), ), ], ), const SizedBox(height: 8), Text( state.purchasesModels[index].title, - style: TextStyle( - color: appTheme.blueA400, - height: 1, - fontSize: 18, - fontWeight: FontWeight.w600, - ), + style: TextStyle(color: appTheme.blueA400, height: 1, fontSize: 18, fontWeight: FontWeight.w600), ), const SizedBox(height: 8), Text( 'Продавец: ${state.purchasesModels[index].consultantName}', - style: const TextStyle( - color: Color(0xFF323F4B), - height: 1, - fontSize: 14, - fontWeight: FontWeight.w500, - ), + style: const TextStyle(color: Color(0xFF323F4B), height: 1, fontSize: 14, fontWeight: FontWeight.w500), ), const SizedBox(height: 8), Text( '${state.purchasesModels[index].duration} ${getDayForm(state.purchasesModels[index].duration, s)}', - style: const TextStyle( - color: Color(0xFF323F4B), - height: 1, - fontSize: 14, - fontWeight: FontWeight.w500, - ), + style: const TextStyle(color: Color(0xFF323F4B), height: 1, fontSize: 14, fontWeight: FontWeight.w500), ), const SizedBox(height: 8), Container( height: 1, width: double.infinity, - decoration: const BoxDecoration( - color: Color(0xFFDEDEDF), - ), + decoration: const BoxDecoration(color: Color(0xFFDEDEDF)), ), const SizedBox(height: 8), Row( @@ -166,16 +135,9 @@ class _UserPurchasesScreen extends State { children: [ Text( '${state.purchasesModels[index].paymentPrice} ₽', - style: const TextStyle( - color: Color(0xFF323F4B), - height: 1, - fontSize: 18, - fontWeight: FontWeight.w600, - ), - ), - StatusWidget( - status: purchaseStatus, + style: const TextStyle(color: Color(0xFF323F4B), height: 1, fontSize: 18, fontWeight: FontWeight.w600), ), + StatusWidget(status: purchaseStatus), ], ), ], @@ -189,10 +151,7 @@ class _UserPurchasesScreen extends State { else const Padding( padding: EdgeInsets.symmetric(vertical: 40.0), - child: Text( - 'У вас нет таких консультаций', - style: TextStyles.textMMedium, - ), + child: Text('У вас нет таких консультаций', style: TextStyles.textMMedium), ), ], ), @@ -200,7 +159,8 @@ class _UserPurchasesScreen extends State { ), ), ], - )), + ), + ), ); }, ), From 74b5c6732d0cd85d0a4690fe466f8e93a0b1837c Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Thu, 11 Sep 2025 20:11:37 +0300 Subject: [PATCH 10/28] fix(settings): Issue-256 --- lib/core/locator_service.dart | 2 +- .../bloc/settings_cubit/settings_cubit.dart | 47 +++++++++++++------ .../bloc/settings_cubit/settings_state.dart | 2 + .../presentation/pages/settings_screen.dart | 39 ++++++++++++++- 4 files changed, 73 insertions(+), 17 deletions(-) diff --git a/lib/core/locator_service.dart b/lib/core/locator_service.dart index 971ebb2c..e4a13866 100644 --- a/lib/core/locator_service.dart +++ b/lib/core/locator_service.dart @@ -137,7 +137,7 @@ Future init() async { sl.registerFactory(() => DietsCubit(feedRepository: sl(), dietsRepository: sl())); sl.registerFactory(() => CommentsCubit(feedRepository: sl(), commentsRepository: sl())); sl.registerFactory(() => PlansCubit(feedRepository: sl(), planRepository: sl())); - sl.registerFactory(() => SettingsCubit(profileRepository: sl(), settingsRepository: sl(), consultantRepository: sl())); + sl.registerFactory(() => SettingsCubit(profileRepository: sl(), settingsRepository: sl(), consultantRepository: sl(), consultationsRepository: sl())); sl.registerFactory(() => RegisterConsultantCubit(consultantRepository: sl())); sl.registerFactory( () => DiaryCubit( diff --git a/lib/features/settings/presentation/bloc/settings_cubit/settings_cubit.dart b/lib/features/settings/presentation/bloc/settings_cubit/settings_cubit.dart index ae498824..2a64e2e9 100644 --- a/lib/features/settings/presentation/bloc/settings_cubit/settings_cubit.dart +++ b/lib/features/settings/presentation/bloc/settings_cubit/settings_cubit.dart @@ -1,5 +1,7 @@ import 'package:eatvio_app/features/consultant/become_consultant/models/consultation_request_model.dart'; import 'package:eatvio_app/features/consultant/become_consultant/repositories/consultant_repository.dart'; +import 'package:eatvio_app/features/consultations/repositories/consultations_repository.dart'; +import 'package:eatvio_app/features/plans/enums/chosen_plan_state_enum.dart'; import 'package:eatvio_app/features/profile/models/profile_model.dart'; import 'package:eatvio_app/features/profile/repositories/profile_repository.dart'; import 'package:eatvio_app/features/settings/models/profile_settings_model.dart'; @@ -11,16 +13,20 @@ class SettingsCubit extends Cubit { final ProfileRepository profileRepository; final SettingsRepository settingsRepository; final ConsultantRepository consultantRepository; + final ConsultationsRepository consultationsRepository; SettingsCubit({ - required this.profileRepository, - required this.settingsRepository, + required this.profileRepository, + required this.settingsRepository, required this.consultantRepository, + required this.consultationsRepository, }) : super(const SettingsState.loading()); Future init() async { final profile = await profileRepository.getMyProfile(); late final ConsultationRequestModel? consultantInfo; + late final bool hasActiveConsultation; + try { if (profile.phoneConfirmationState.contains('confirmed')) { consultantInfo = await consultantRepository.getMyRequestStatus(profile.slug); @@ -30,6 +36,13 @@ class SettingsCubit extends Cubit { } catch (e) { consultantInfo = null; } + + try { + hasActiveConsultation = await _checkActiveConsultation(); + } catch (e) { + hasActiveConsultation = false; + } + try { emit( SettingsState.loaded( @@ -37,6 +50,7 @@ class SettingsCubit extends Cubit { profileDetails: await profileRepository.getMyProfileDetails(), profile: profile, consultationRequestModel: consultantInfo, + hasActiveConsultation: hasActiveConsultation, ), ); } catch (e) { @@ -44,13 +58,9 @@ class SettingsCubit extends Cubit { } } - Future updateProfileDetails({ - required ProfileDetailsModel profileDetailsModel, - }) async { + Future updateProfileDetails({required ProfileDetailsModel profileDetailsModel}) async { try { - await settingsRepository.updateProfileDetails( - profileDetailsModel: profileDetailsModel, - ); + await settingsRepository.updateProfileDetails(profileDetailsModel: profileDetailsModel); } catch (e) { rethrow; } finally { @@ -58,13 +68,9 @@ class SettingsCubit extends Cubit { } } - Future updateProfileSettings({ - required ProfileSettingsModel profileSettingsModel, - }) async { + Future updateProfileSettings({required ProfileSettingsModel profileSettingsModel}) async { try { - await settingsRepository.updateProfileSettings( - profileSettingsModel: profileSettingsModel, - ); + await settingsRepository.updateProfileSettings(profileSettingsModel: profileSettingsModel); } catch (e) { rethrow; } finally { @@ -80,4 +86,17 @@ class SettingsCubit extends Cubit { return false; } } + + Future _checkActiveConsultation() async { + try { + final userPurchases = await consultationsRepository.getUserPurchases(null, null); + final activeStates = ChosenPlanStateEnum.getActiveStates(); + + return userPurchases.any((purchase) => + activeStates.contains(purchase.chosenPlanState) + ); + } catch (e) { + return false; + } + } } diff --git a/lib/features/settings/presentation/bloc/settings_cubit/settings_state.dart b/lib/features/settings/presentation/bloc/settings_cubit/settings_state.dart index f220fe5e..4b232166 100644 --- a/lib/features/settings/presentation/bloc/settings_cubit/settings_state.dart +++ b/lib/features/settings/presentation/bloc/settings_cubit/settings_state.dart @@ -1,5 +1,6 @@ import 'package:eatvio_app/features/consultant/become_consultant/models/consultation_request_model.dart'; import 'package:eatvio_app/features/profile/models/profile_model.dart'; +import 'package:eatvio_app/features/profile/models/profile_details_model.dart'; import 'package:eatvio_app/features/settings/models/profile_settings_model.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -16,6 +17,7 @@ abstract class SettingsState with _$SettingsState { required ProfileDetailsModel profileDetails, required ProfileModel profile, ConsultationRequestModel? consultationRequestModel, + bool hasActiveConsultation, }) = SettingsStateLoaded; const factory SettingsState.error({ diff --git a/lib/features/settings/presentation/pages/settings_screen.dart b/lib/features/settings/presentation/pages/settings_screen.dart index 41066d54..3e82fab3 100644 --- a/lib/features/settings/presentation/pages/settings_screen.dart +++ b/lib/features/settings/presentation/pages/settings_screen.dart @@ -32,6 +32,24 @@ class _SettingsScreenState extends State { late bool bedTimeSwitchState; late bool moodSwitchState; + void _showConsultationWarning() { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Предупреждение'), + content: const Text('Во время консультации этот параметр нельзя отключить'), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Понятно'), + ), + ], + ); + }, + ); + } + @override Widget build(BuildContext context) { return BlocProvider( @@ -158,16 +176,24 @@ class _SettingsScreenState extends State { _tile( padding: const EdgeInsets.symmetric(horizontal: 16), onTap: () { + if (state.hasActiveConsultation && !moodSwitchState) { + _showConsultationWarning(); + return; + } setState(() { moodSwitchState = !moodSwitchState; }); settingsCubit.updateProfileSettings(profileSettingsModel: state.profileSettings.copyWith(isMoodFillable: moodSwitchState)); }, - title: 'Указывать “настроение” в заполнение дневника', + title: 'Указывать "настроение" в заполнение дневника', trailing: CupertinoSwitch( value: moodSwitchState, activeTrackColor: appTheme.blueA400, onChanged: (newValue) { + if (state.hasActiveConsultation && !newValue) { + _showConsultationWarning(); + return; + } setState(() { moodSwitchState = newValue; }); @@ -179,16 +205,25 @@ class _SettingsScreenState extends State { _tile( padding: const EdgeInsets.symmetric(horizontal: 16), onTap: () { + + if (state.hasActiveConsultation && !bedTimeSwitchState) { + _showConsultationWarning(); + return; + } setState(() { bedTimeSwitchState = !bedTimeSwitchState; }); settingsCubit.updateProfileSettings(profileSettingsModel: state.profileSettings.copyWith(isBedtimeFillable: bedTimeSwitchState)); }, - title: 'Указывать “отход ко сну” в заполнение дневника', + title: 'Указывать "отход ко сну" в заполнение дневника', trailing: CupertinoSwitch( value: bedTimeSwitchState, activeTrackColor: appTheme.blueA400, onChanged: (newValue) { + if (state.hasActiveConsultation && !newValue) { + _showConsultationWarning(); + return; + } setState(() { bedTimeSwitchState = newValue; }); From 8c348f61b1871038440ace13ff61c8614cf62e8a Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Thu, 11 Sep 2025 21:35:28 +0300 Subject: [PATCH 11/28] fix(settings): Issue-256 --- .../bloc/settings_cubit/settings_state.dart | 3 +-- .../presentation/pages/settings_screen.dart | 13 ++++++------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/lib/features/settings/presentation/bloc/settings_cubit/settings_state.dart b/lib/features/settings/presentation/bloc/settings_cubit/settings_state.dart index 4b232166..86471f31 100644 --- a/lib/features/settings/presentation/bloc/settings_cubit/settings_state.dart +++ b/lib/features/settings/presentation/bloc/settings_cubit/settings_state.dart @@ -1,6 +1,5 @@ import 'package:eatvio_app/features/consultant/become_consultant/models/consultation_request_model.dart'; import 'package:eatvio_app/features/profile/models/profile_model.dart'; -import 'package:eatvio_app/features/profile/models/profile_details_model.dart'; import 'package:eatvio_app/features/settings/models/profile_settings_model.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -17,7 +16,7 @@ abstract class SettingsState with _$SettingsState { required ProfileDetailsModel profileDetails, required ProfileModel profile, ConsultationRequestModel? consultationRequestModel, - bool hasActiveConsultation, + required bool hasActiveConsultation, }) = SettingsStateLoaded; const factory SettingsState.error({ diff --git a/lib/features/settings/presentation/pages/settings_screen.dart b/lib/features/settings/presentation/pages/settings_screen.dart index 3e82fab3..ad42a7b4 100644 --- a/lib/features/settings/presentation/pages/settings_screen.dart +++ b/lib/features/settings/presentation/pages/settings_screen.dart @@ -176,7 +176,7 @@ class _SettingsScreenState extends State { _tile( padding: const EdgeInsets.symmetric(horizontal: 16), onTap: () { - if (state.hasActiveConsultation && !moodSwitchState) { + if (state.hasActiveConsultation) { _showConsultationWarning(); return; } @@ -187,10 +187,10 @@ class _SettingsScreenState extends State { }, title: 'Указывать "настроение" в заполнение дневника', trailing: CupertinoSwitch( - value: moodSwitchState, + value: state.hasActiveConsultation ? true : moodSwitchState, activeTrackColor: appTheme.blueA400, onChanged: (newValue) { - if (state.hasActiveConsultation && !newValue) { + if (state.hasActiveConsultation) { _showConsultationWarning(); return; } @@ -205,8 +205,7 @@ class _SettingsScreenState extends State { _tile( padding: const EdgeInsets.symmetric(horizontal: 16), onTap: () { - - if (state.hasActiveConsultation && !bedTimeSwitchState) { + if (state.hasActiveConsultation) { _showConsultationWarning(); return; } @@ -217,10 +216,10 @@ class _SettingsScreenState extends State { }, title: 'Указывать "отход ко сну" в заполнение дневника', trailing: CupertinoSwitch( - value: bedTimeSwitchState, + value: state.hasActiveConsultation ? true : bedTimeSwitchState, activeTrackColor: appTheme.blueA400, onChanged: (newValue) { - if (state.hasActiveConsultation && !newValue) { + if (state.hasActiveConsultation) { _showConsultationWarning(); return; } From 4bc8cba9413a4397118316373147ca10a59fae97 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Fri, 12 Sep 2025 13:34:16 +0300 Subject: [PATCH 12/28] fix(profile): Issue-416 --- .../presentation/pages/profile_screen.dart | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/lib/features/profile/presentation/pages/profile_screen.dart b/lib/features/profile/presentation/pages/profile_screen.dart index 990beed1..cc8f7245 100644 --- a/lib/features/profile/presentation/pages/profile_screen.dart +++ b/lib/features/profile/presentation/pages/profile_screen.dart @@ -1,3 +1,4 @@ +import 'dart:developer'; import 'dart:io'; import 'package:cached_network_image/cached_network_image.dart'; @@ -93,7 +94,7 @@ class _ProfileScreenState extends State { } isFirstFetch = false; } - + // Проверяем, заблокировал ли пользователь меня if (state.profile.isBlockedByUser) { return Scaffold( @@ -104,20 +105,12 @@ class _ProfileScreenState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon( - Icons.block, - size: 64, - color: Colors.grey[400], - ), + Icon(Icons.block, size: 64, color: Colors.grey[400]), const SizedBox(height: 16), Text( 'Пользователь ограничил доступ к своей странице', textAlign: TextAlign.center, - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w500, - color: Colors.grey[600], - ), + style: TextStyle(fontSize: 18, fontWeight: FontWeight.w500, color: Colors.grey[600]), ), ], ), @@ -125,7 +118,7 @@ class _ProfileScreenState extends State { ), ); } - + return widget.author == null || widget.author!.authorIsMe ? _myProfileScreen(state) : _profileScreen(state); } else if (state is ProfileStateError) { return Scaffold( @@ -211,6 +204,14 @@ class _ProfileScreenState extends State { if (state.profile.roles.contains('consultant') && state.consultantInfo != null && !state.profile.isBlockedByMe) _education(state), if (state.profile.roles.contains('consultant') && state.consultantInfo != null && !state.profile.isBlockedByMe) _diplomasAndCertificates(state), if (state.profile.roles.contains('consultant') && state.consultantInfo != null && !hidePlans && !state.profile.isBlockedByMe) _activePlans(state), + if (state.blogPosts.isNotEmpty) + const Padding( + padding: EdgeInsets.only(left: 8, top: 16, bottom: 8), + child: Text( + 'Блог', + style: TextStyle(color: Color(0xFF323F4B), fontSize: 24, fontWeight: FontWeight.w600), + ), + ), _blogPosts(state), ], ), @@ -277,9 +278,9 @@ class _ProfileScreenState extends State { _subscriptionsAndSubscribers(state), if (state.profile.badges.isNotEmpty) Padding(padding: const EdgeInsets.only(bottom: 16), child: _badges(state)), if (state.profile.roles.contains('consultant')) _socialLinks(state), + Padding(padding: const EdgeInsets.only(bottom: 16), child: _charts()), if (state.profile.roles.contains('consultant')) _education(state), if (state.profile.roles.contains('consultant')) _diplomasAndCertificates(state), - Padding(padding: const EdgeInsets.only(bottom: 16), child: _charts()), if (state.profile.roles.contains('consultant') && !hidePlans) _activePlans(state), _newBlogPostButton(), _blogPosts(state), @@ -512,6 +513,7 @@ class _ProfileScreenState extends State { children: [ ListView.separated( shrinkWrap: true, + padding: const EdgeInsets.all(0), physics: const NeverScrollableScrollPhysics(), itemBuilder: (context, index) => _educationBlock( state.consultantInfo!.educationModel.completedCourses![index].values.first.title, @@ -766,7 +768,7 @@ class _ProfileScreenState extends State { ListView _blogPosts(ProfileStateLoaded state) { return ListView.separated( - padding: const EdgeInsets.symmetric(vertical: 16), + padding: const EdgeInsets.only(bottom: 16), shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemBuilder: (context, index) => ProfileBlogPostWidget(index: index, profileCubit: profileCubit), From 0fce3f9d30f1b4d8eeeee68fa214136463d461a1 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Fri, 12 Sep 2025 13:38:50 +0300 Subject: [PATCH 13/28] fix(diary): Issue-675 --- .../diary/presentation/widgets/meal_summary_widget.dart | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/features/diary/presentation/widgets/meal_summary_widget.dart b/lib/features/diary/presentation/widgets/meal_summary_widget.dart index dd7108e6..f4537b79 100644 --- a/lib/features/diary/presentation/widgets/meal_summary_widget.dart +++ b/lib/features/diary/presentation/widgets/meal_summary_widget.dart @@ -82,6 +82,11 @@ class MealSummaryWidget extends StatelessWidget { continue; } + // Для продуктов из диеты учитываем только съеденные (с галочкой) + if (portion.fromDiet && portion.portionState != PortionState.eatenFromDiet) { + continue; + } + final caloriesInfo = portion.productCaloriesInfo; if (caloriesInfo == null) { continue; From 2300bc4ab604df35bdb43df748040a64dcea0561 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Fri, 12 Sep 2025 16:29:29 +0300 Subject: [PATCH 14/28] fix(consultations): Issue-372 --- .../widgets/consultation_card_widget.dart | 16 ++++++++++++++++ .../widgets/notification_widget.dart | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/lib/features/consultations/presentation/widgets/consultation_card_widget.dart b/lib/features/consultations/presentation/widgets/consultation_card_widget.dart index 760a8783..3d870e8d 100644 --- a/lib/features/consultations/presentation/widgets/consultation_card_widget.dart +++ b/lib/features/consultations/presentation/widgets/consultation_card_widget.dart @@ -3,9 +3,12 @@ import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/widgets/custom_divider.dart'; +import 'package:eatvio_app/core/widgets/custom_outlined_button.dart'; import 'package:eatvio_app/core/widgets/show_error_dialog.dart'; import 'package:eatvio_app/features/consultations/models/consultation_model.dart'; import 'package:eatvio_app/features/diary/presentation/pages/diary_screen.dart'; +import 'package:eatvio_app/features/my_finance/presentation/pages/card_settings.dart'; +import 'package:eatvio_app/features/plans/enums/chosen_plan_state_enum.dart'; import 'package:eatvio_app/features/user_details/presentation/user_details_screen.dart'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/material.dart'; @@ -284,6 +287,19 @@ class _ConsultationCardWidgetState extends State { ), ], ), + // Кнопка получения выплаты + if (widget.consultationsModel.chosenPlanState == ChosenPlanStateEnum.stateDone) ...[ + const SizedBox(height: 16), + CustomOutlinedButton( + text: s.collectCharge, + height: 52, + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), + buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.green)), + onPressed: () async { + GoRouter.of(context).push(CardSettingsScreen.path, extra: widget.consultationsModel.id); + }, + ), + ], ], ), ), diff --git a/lib/features/notifications/presentation/widgets/notification_widget.dart b/lib/features/notifications/presentation/widgets/notification_widget.dart index ed196b00..6b675382 100644 --- a/lib/features/notifications/presentation/widgets/notification_widget.dart +++ b/lib/features/notifications/presentation/widgets/notification_widget.dart @@ -307,7 +307,7 @@ class _ChosenPlanFinishedToConsultantWidget extends StatelessWidget { buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.green)), onPressed: () { - GoRouter.of(context).pushNamed(CardSettingsScreen.name, extra: notification.planId); + GoRouter.of(context).push(CardSettingsScreen.path, extra: notification.planId); }, ), ], From 1a6fb39d846c636f64b5bb1386b233cea182d265 Mon Sep 17 00:00:00 2001 From: misha Date: Mon, 15 Sep 2025 14:12:05 +0300 Subject: [PATCH 15/28] gitignore --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 4f05e3a1..06e89a5e 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,5 @@ *.mocks.dart android/build/ ios/XCBuildData/ -env/.env \ No newline at end of file +env/.env +.env \ No newline at end of file From 41f77bd373375eb5e6ab1985a3ae7ade26eb2c6e Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Tue, 16 Sep 2025 09:28:17 +0300 Subject: [PATCH 16/28] feat(;auth): social --- .../auth/models/social_exchange_request.dart | 11 ++ .../bloc/auth_cubit/auth_cubit.dart | 34 +++-- .../auth/presentation/pages/auth_screen.dart | 130 ++++++++++++++++++ .../auth/repositories/auth_repository.dart | 14 ++ lib/features/auth/sdk/social_auth_facade.dart | 24 ++++ lib/features/auth/services/auth_service.dart | 6 + 6 files changed, 204 insertions(+), 15 deletions(-) create mode 100644 lib/features/auth/models/social_exchange_request.dart create mode 100644 lib/features/auth/sdk/social_auth_facade.dart diff --git a/lib/features/auth/models/social_exchange_request.dart b/lib/features/auth/models/social_exchange_request.dart new file mode 100644 index 00000000..85bf7330 --- /dev/null +++ b/lib/features/auth/models/social_exchange_request.dart @@ -0,0 +1,11 @@ +class SocialExchangeRequest { + final String provider; + final String accessToken; + + SocialExchangeRequest({required this.provider, required this.accessToken}); + + Map toJson() { + return {'provider': provider, 'access_token': accessToken}; + } +} + diff --git a/lib/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart b/lib/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart index 20686cce..7b58a4b0 100644 --- a/lib/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart +++ b/lib/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart @@ -6,28 +6,17 @@ import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_state import 'package:eatvio_app/features/auth/repositories/auth_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; - class AuthCubit extends Cubit { final AuthRepository authRepository; - AuthCubit({ - required this.authRepository, - }) : super(const AuthState()); + AuthCubit({required this.authRepository}) : super(const AuthState()); Future getTokenByCredentials(String email, String password) async { emit(const AuthState.loading()); try { - final token = (await authRepository.getTokenByCredentials( - email, - password, - )) - .token; - emit( - AuthState.loaded( - token: token, - ), - ); - await TokenStorage.saveTokenInStorage(token); + final token = (await authRepository.getTokenByCredentials(email, password)).token; + emit(AuthState.loaded(token: token)); + await TokenStorage.saveTokenInStorage(token); return true; } catch (e) { log(e.toString()); @@ -36,4 +25,19 @@ class AuthCubit extends Cubit { return false; } } + + Future getTokenBySocial({required String provider, required String accessToken}) async { + emit(const AuthState.loading()); + try { + final token = (await authRepository.getTokenBySocial(provider: provider, accessToken: accessToken)).token; + emit(AuthState.loaded(token: token)); + await TokenStorage.saveTokenInStorage(token); + return true; + } catch (e) { + log(e.toString()); + showErrorToast('Не удалось войти через $provider'); + emit(AuthState.error(errorMessage: '$e')); + return false; + } + } } diff --git a/lib/features/auth/presentation/pages/auth_screen.dart b/lib/features/auth/presentation/pages/auth_screen.dart index 73162bd2..61976300 100644 --- a/lib/features/auth/presentation/pages/auth_screen.dart +++ b/lib/features/auth/presentation/pages/auth_screen.dart @@ -24,6 +24,7 @@ import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:go_router/go_router.dart'; import 'package:url_launcher/url_launcher.dart'; +import 'package:eatvio_app/features/auth/sdk/social_auth_facade.dart'; class AuthScreen extends StatefulWidget { const AuthScreen({super.key}); @@ -165,6 +166,52 @@ class _AuthScreenState extends State { ), ], ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _SocialButton( + onTap: _onVkPressed, + child: SvgPicture.asset( + 'assets/icons/vk.svg', + height: 24, + width: 24, + colorFilter: const ColorFilter.mode(Colors.white, BlendMode.srcIn), + ), + backgroundColor: const Color(0xFF2787F5), + tooltip: 'VK ID', + ), + _SocialButton( + onTap: _onOkPressed, + child: SvgPicture.asset( + 'assets/icons/ok.svg', + height: 24, + width: 24, + colorFilter: const ColorFilter.mode(Colors.white, BlendMode.srcIn), + ), + backgroundColor: const Color(0xFFF58220), + tooltip: 'Одноклассники', + ), + _SocialButton( + onTap: _onMailRuPressed, + child: const Text( + 'Mail', + style: TextStyle(color: Colors.white, fontWeight: FontWeight.w700), + ), + backgroundColor: const Color(0xFF005FF9), + tooltip: 'Mail.ru', + ), + _SocialButton( + onTap: _onYandexPressed, + child: const Text( + 'Yandex', + style: TextStyle(color: Colors.black, fontWeight: FontWeight.w700), + ), + backgroundColor: const Color(0xFFFFCC00), + tooltip: 'Яндекс ID', + ), + ], + ), const Spacer(), Center( child: RichText( @@ -317,3 +364,86 @@ class _AuthScreenState extends State { ); } } + +class _SocialButton extends StatelessWidget { + const _SocialButton({required this.onTap, required this.child, required this.backgroundColor, required this.tooltip}); + + final VoidCallback onTap; + final Widget child; + final Color backgroundColor; + final String tooltip; + + @override + Widget build(BuildContext context) { + return Tooltip( + message: tooltip, + child: InkWell( + borderRadius: BorderRadius.circular(12), + onTap: onTap, + child: Ink( + height: 48, + width: (MediaQuery.of(context).size.width - 16 * 2 - 12 * 3) / 4, + decoration: BoxDecoration(color: backgroundColor, borderRadius: BorderRadius.circular(12)), + child: Center(child: child), + ), + ), + ); + } +} + +void _onVkPressed() async { + final accessToken = await SocialAuthFacade.signInWithVk(); + if (accessToken == null) return; + final isAuthenticated = await sl().getTokenBySocial(provider: 'vk', accessToken: accessToken); + if (isAuthenticated) { + if (AppRouter.navigatorKey.currentContext != null) { + // Reuse success flow + // ignore: use_build_context_synchronously + await _reconnectAfterAuth(); + } + } +} + +void _onOkPressed() async { + final accessToken = await SocialAuthFacade.signInWithOk(); + if (accessToken == null) return; + final isAuthenticated = await sl().getTokenBySocial(provider: 'ok', accessToken: accessToken); + if (isAuthenticated) { + await _reconnectAfterAuth(); + } +} + +void _onMailRuPressed() async { + final accessToken = await SocialAuthFacade.signInWithMailRu(); + if (accessToken == null) return; + final isAuthenticated = await sl().getTokenBySocial(provider: 'mailru', accessToken: accessToken); + if (isAuthenticated) { + await _reconnectAfterAuth(); + } +} + +void _onYandexPressed() async { + final accessToken = await SocialAuthFacade.signInWithYandex(); + if (accessToken == null) return; + final isAuthenticated = await sl().getTokenBySocial(provider: 'yandex', accessToken: accessToken); + if (isAuthenticated) { + await _reconnectAfterAuth(); + } +} + +Future _reconnectAfterAuth() async { + final context = AppRouter.navigatorKey.currentContext; + if (context == null) return; + try { + await sl().clear(); + await sl().initialize(); + await sl().getMyProfile(); + await sl().getChatRoomsWithLoading(); + await sl().connectToWebSocket(); + await sl().getNotifications(); + await sl().connectToWebSocket(); + await sl().registerPushToken(); + showSuccessToast('Успешный вход'); + } catch (_) {} + AppRouter.navigatorKey.currentContext?.go('/feed-screen', extra: {'needToInitializeWebSocket': false}); +} diff --git a/lib/features/auth/repositories/auth_repository.dart b/lib/features/auth/repositories/auth_repository.dart index d0bfcce5..84df92f6 100644 --- a/lib/features/auth/repositories/auth_repository.dart +++ b/lib/features/auth/repositories/auth_repository.dart @@ -1,6 +1,7 @@ import 'package:eatvio_app/features/auth/models/credentials_model.dart'; import 'package:eatvio_app/features/auth/models/token_model.dart'; import 'package:eatvio_app/features/auth/services/auth_service.dart'; +import 'package:eatvio_app/features/auth/models/social_exchange_request.dart'; class AuthRepository { final AuthService authService; @@ -22,4 +23,17 @@ class AuthRepository { rethrow; } } + + Future getTokenBySocial({ + required String provider, + required String accessToken, + }) async { + try { + return await authService.getTokenBySocial( + body: SocialExchangeRequest(provider: provider, accessToken: accessToken), + ); + } catch (e) { + rethrow; + } + } } diff --git a/lib/features/auth/sdk/social_auth_facade.dart b/lib/features/auth/sdk/social_auth_facade.dart new file mode 100644 index 00000000..975f4196 --- /dev/null +++ b/lib/features/auth/sdk/social_auth_facade.dart @@ -0,0 +1,24 @@ +import 'dart:async'; + +class SocialAuthFacade { + static Future signInWithVk() async { + // TODO: Integrate official VK ID SDK and return access token + return null; + } + + static Future signInWithOk() async { + // TODO: Integrate official OK SDK and return access token + return null; + } + + static Future signInWithMailRu() async { + // TODO: Integrate official Mail.ru SDK and return access token + return null; + } + + static Future signInWithYandex() async { + // TODO: Integrate official Yandex ID SDK and return access token + return null; + } +} + diff --git a/lib/features/auth/services/auth_service.dart b/lib/features/auth/services/auth_service.dart index 62256f8b..7dabb65b 100644 --- a/lib/features/auth/services/auth_service.dart +++ b/lib/features/auth/services/auth_service.dart @@ -1,6 +1,7 @@ import 'package:dio/dio.dart'; import 'package:eatvio_app/features/auth/models/credentials_model.dart'; import 'package:eatvio_app/features/auth/models/token_model.dart'; +import 'package:eatvio_app/features/auth/models/social_exchange_request.dart'; import 'package:retrofit/retrofit.dart'; part 'auth_service.g.dart'; @@ -13,4 +14,9 @@ abstract class AuthService { Future getTokenByCredentials({ @Body() required CredentialsModel credentials, }); + + @POST('/api/social/login') + Future getTokenBySocial({ + @Body() required SocialExchangeRequest body, + }); } From b146295996fbac0298f54ebe2d36f189880e4119 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Tue, 16 Sep 2025 19:00:53 +0300 Subject: [PATCH 17/28] fix(profile): Issue-582 --- .../auth/models/social_exchange_request.dart | 1 + lib/features/auth/sdk/social_auth_facade.dart | 1 + .../bloc/profile_cubit/profile_cubit.dart | 39 +++++-------------- 3 files changed, 12 insertions(+), 29 deletions(-) diff --git a/lib/features/auth/models/social_exchange_request.dart b/lib/features/auth/models/social_exchange_request.dart index 85bf7330..c322fab6 100644 --- a/lib/features/auth/models/social_exchange_request.dart +++ b/lib/features/auth/models/social_exchange_request.dart @@ -9,3 +9,4 @@ class SocialExchangeRequest { } } + diff --git a/lib/features/auth/sdk/social_auth_facade.dart b/lib/features/auth/sdk/social_auth_facade.dart index 975f4196..62985d5d 100644 --- a/lib/features/auth/sdk/social_auth_facade.dart +++ b/lib/features/auth/sdk/social_auth_facade.dart @@ -22,3 +22,4 @@ class SocialAuthFacade { } } + 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 d0d7a932..5ad4a948 100644 --- a/lib/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart +++ b/lib/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart @@ -18,11 +18,7 @@ class ProfileCubit extends Cubit { final ConsultantRepository consultantRepository; String _currentPeriod = '1'; - ProfileCubit({ - required this.profileRepository, - required this.chatRepository, - required this.consultantRepository, - }) : super(const ProfileState.loading()); + ProfileCubit({required this.profileRepository, required this.chatRepository, required this.consultantRepository}) : super(const ProfileState.loading()); Future getProfileBySlug(String slug) async { emit(const ProfileState.loading()); @@ -42,9 +38,7 @@ class ProfileCubit extends Cubit { } } - Future getMyProfile({ - String period = '1', - }) async { + Future getMyProfile({String period = '1'}) async { emit(const ProfileState.loading()); _currentPeriod = period; try { @@ -77,15 +71,14 @@ class ProfileCubit extends Cubit { profile: (state as ProfileStateLoaded).profile, profileDetails: (state as ProfileStateLoaded).profileDetails, blogPosts: feedBlogPostList, + weightPoints: (state as ProfileStateLoaded).weightPoints, + circumferencePoints: (state as ProfileStateLoaded).circumferencePoints, + consultantInfo: (state as ProfileStateLoaded).consultantInfo, ), ); } - Future addCircumference({ - required int chestCircumference, - required int waistCircumference, - required int hipCircumference, - }) async { + Future addCircumference({required int chestCircumference, required int waistCircumference, required int hipCircumference}) async { try { await profileRepository.addCircumference( chestCircumference: chestCircumference, @@ -98,9 +91,7 @@ class ProfileCubit extends Cubit { } } - Future addWeight({ - required double weight, - }) async { + Future addWeight({required double weight}) async { try { await profileRepository.addWeight(weight: weight); await getMyProfile(period: _currentPeriod); @@ -109,25 +100,15 @@ class ProfileCubit extends Cubit { } } - Future getChatRoomIdBySlug({ - required Author author, - }) async { + Future getChatRoomIdBySlug({required Author author}) async { try { final chatIdOrError = await chatRepository.getChatByUserSlug(author.authorSlug); if (chatIdOrError == '404') { - AppRouter.navigatorKey.currentContext?.pushNamed( - 'emptyChatScreen', - extra: author, - ); + AppRouter.navigatorKey.currentContext?.pushNamed('emptyChatScreen', extra: author); } else if (chatIdOrError == 'error') { showErrorToast('Ошибка создания чата'); } else { - AppRouter.navigatorKey.currentContext?.pushNamed( - 'chatScreen', - extra: { - 'uuid': chatIdOrError, - }, - ); + AppRouter.navigatorKey.currentContext?.pushNamed('chatScreen', extra: {'uuid': chatIdOrError}); return chatIdOrError; } } catch (e, st) { From c295c0656525e16cdbc1fa6f5cedeeb5fe5df458 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Tue, 16 Sep 2025 19:09:41 +0300 Subject: [PATCH 18/28] fix(my_plans): Issue-189 --- .../auth/presentation/pages/auth_screen.dart | 24 +-- .../presentation/my_plans_screen.dart | 41 +--- .../presentation/widgets/my_plan_widget.dart | 188 +++++------------- .../presentation/widgets/my_plans_tab.dart | 72 +------ .../presentation/pages/profile_screen.dart | 2 - 5 files changed, 77 insertions(+), 250 deletions(-) diff --git a/lib/features/auth/presentation/pages/auth_screen.dart b/lib/features/auth/presentation/pages/auth_screen.dart index 61976300..c32b8a39 100644 --- a/lib/features/auth/presentation/pages/auth_screen.dart +++ b/lib/features/auth/presentation/pages/auth_screen.dart @@ -172,43 +172,43 @@ class _AuthScreenState extends State { children: [ _SocialButton( onTap: _onVkPressed, + backgroundColor: const Color(0xFF2787F5), + tooltip: 'VK ID', child: SvgPicture.asset( 'assets/icons/vk.svg', height: 24, width: 24, colorFilter: const ColorFilter.mode(Colors.white, BlendMode.srcIn), ), - backgroundColor: const Color(0xFF2787F5), - tooltip: 'VK ID', ), _SocialButton( onTap: _onOkPressed, + backgroundColor: const Color(0xFFF58220), + tooltip: 'Одноклассники', child: SvgPicture.asset( 'assets/icons/ok.svg', height: 24, width: 24, colorFilter: const ColorFilter.mode(Colors.white, BlendMode.srcIn), ), - backgroundColor: const Color(0xFFF58220), - tooltip: 'Одноклассники', ), - _SocialButton( + const _SocialButton( onTap: _onMailRuPressed, - child: const Text( + backgroundColor: Color(0xFF005FF9), + tooltip: 'Mail.ru', + child: Text( 'Mail', style: TextStyle(color: Colors.white, fontWeight: FontWeight.w700), ), - backgroundColor: const Color(0xFF005FF9), - tooltip: 'Mail.ru', ), - _SocialButton( + const _SocialButton( onTap: _onYandexPressed, - child: const Text( + backgroundColor: Color(0xFFFFCC00), + tooltip: 'Яндекс ID', + child: Text( 'Yandex', style: TextStyle(color: Colors.black, fontWeight: FontWeight.w700), ), - backgroundColor: const Color(0xFFFFCC00), - tooltip: 'Яндекс ID', ), ], ), diff --git a/lib/features/my_plans/presentation/my_plans_screen.dart b/lib/features/my_plans/presentation/my_plans_screen.dart index 3ca9ca3e..f7bdb456 100644 --- a/lib/features/my_plans/presentation/my_plans_screen.dart +++ b/lib/features/my_plans/presentation/my_plans_screen.dart @@ -12,7 +12,6 @@ import 'package:eatvio_app/features/feed/presentation/widgets/plans_filter_botto import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_cubit.dart'; import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_state.dart'; import 'package:eatvio_app/features/my_plans/presentation/widgets/my_plan_widget.dart'; -import 'package:eatvio_app/features/my_plans/presentation/widgets/my_plans_tab.dart'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -64,9 +63,7 @@ class MyPlansScreenContent extends StatelessWidget { headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { return [ SliverAppBar( - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(bottom: Radius.circular(12)), - ), + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(bottom: Radius.circular(12))), backgroundColor: appTheme.gray100, foregroundColor: appTheme.gray100, automaticallyImplyLeading: false, @@ -76,21 +73,12 @@ class MyPlansScreenContent extends StatelessWidget { snap: true, scrolledUnderElevation: 0, bottom: PreferredSize( - preferredSize: Size( - MediaQuery.sizeOf(context).width, - 64, - ), + preferredSize: Size(MediaQuery.sizeOf(context).width, 32), child: Padding( padding: const EdgeInsets.only(bottom: 0), child: Column( mainAxisSize: MainAxisSize.min, children: [ - const SizedBox(height: 8), - const Padding( - padding: EdgeInsets.symmetric(horizontal: 4), - child: MyPlansTab(), - ), - const SizedBox(height: 8), Row( children: [ const SizedBox(width: 4), @@ -104,20 +92,14 @@ class MyPlansScreenContent extends StatelessWidget { _ => activePlansSearchController, }, onChanged: (query) { - EasyDebounce.debounce( - 'searchPlans', - const Duration(milliseconds: 500), - () { - context.read().loadPlans(query: query == '' ? null : query); - }, - ); + EasyDebounce.debounce('searchPlans', const Duration(milliseconds: 500), () { + context.read().loadPlans(query: query == '' ? null : query); + }); }, ), ), const SizedBox(width: 12), - AddItemButton( - onTap: () => GoRouter.of(context).pushNamed(CreatePlanScreen.name), - ), + AddItemButton(onTap: () => GoRouter.of(context).pushNamed(CreatePlanScreen.name)), const SizedBox(width: 4), ], ), @@ -134,10 +116,7 @@ class MyPlansScreenContent extends StatelessWidget { child: BlocBuilder( builder: (BuildContext context, MyPlansState state) { if (state.isLoading) { - return Scaffold( - backgroundColor: Colors.grey.shade200, - body: const Loader(), - ); + return Scaffold(backgroundColor: Colors.grey.shade200, body: const Loader()); } else if (state.error != null) { return DataErrorWidget(onReload: () => context.read().loadPlans()); } else { @@ -162,11 +141,7 @@ class MyPlansScreenContent extends StatelessWidget { Positioned( right: 0, bottom: 74, - child: FilterBadge( - filterBottomSheet: PlansFilterBottomSheet( - myPlansCubit: context.read(), - ), - ), + child: FilterBadge(filterBottomSheet: PlansFilterBottomSheet(myPlansCubit: context.read())), ), ], ); diff --git a/lib/features/my_plans/presentation/widgets/my_plan_widget.dart b/lib/features/my_plans/presentation/widgets/my_plan_widget.dart index ce91acdc..7ecdc653 100644 --- a/lib/features/my_plans/presentation/widgets/my_plan_widget.dart +++ b/lib/features/my_plans/presentation/widgets/my_plan_widget.dart @@ -14,16 +14,13 @@ import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_html/flutter_html.dart'; -import 'package:flutter_svg/svg.dart'; +// Removed SVG icon usage after switching to text buttons import 'package:go_router/go_router.dart'; import 'package:share_plus/share_plus.dart'; class MyPlanWidget extends StatefulWidget { final PlanModel plan; - const MyPlanWidget({ - super.key, - required this.plan, - }); + const MyPlanWidget({super.key, required this.plan}); @override State createState() => _MyPlanWidgetState(); @@ -34,10 +31,7 @@ class _MyPlanWidgetState extends State { Widget build(BuildContext context) { final s = S.of(context); return DecoratedBox( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - color: Colors.white, - ), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(12), color: Colors.white), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -48,23 +42,14 @@ class _MyPlanWidgetState extends State { onTap: () => GoRouter.of(context).pushNamed('aboutPlanScreen', extra: {'slug': widget.plan.slug, 'navigateToComments': false}), child: ClipRRect( borderRadius: const BorderRadius.vertical(top: Radius.circular(12)), - child: CachedNetworkImage( - imageUrl: '$baseUrl/storage/${widget.plan.image}', - fit: BoxFit.fitWidth, - width: MediaQuery.of(context).size.width, - ), + child: CachedNetworkImage(imageUrl: '$baseUrl/storage/${widget.plan.image}', fit: BoxFit.fitWidth, width: MediaQuery.of(context).size.width), ), ), Positioned( bottom: 16, child: SizedBox( width: MediaQuery.sizeOf(context).width, - child: InfoRow( - plan: widget.plan, - itemsBackgroundColor: appTheme.whiteA700, - disableDescriptionOfTypes: true, - type: ProductType.plan, - ), + child: InfoRow(plan: widget.plan, itemsBackgroundColor: appTheme.whiteA700, disableDescriptionOfTypes: true, type: ProductType.plan), ), ), ], @@ -75,19 +60,16 @@ class _MyPlanWidgetState extends State { children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 16), - child: Text( - widget.plan.name, - style: TextStyles.textMSemibold.copyWith(color: appTheme.black), - ), + child: Text(widget.plan.name, style: TextStyles.textMSemibold.copyWith(color: appTheme.black)), ), if (widget.plan.details.isNotEmpty) ...[ - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Html( - data: widget.plan.details.length > 200 ? '${widget.plan.details.substring(0, 200).replaceAll('\n', '
')}...' : widget.plan.details.replaceAll('\n', '
'), + data: widget.plan.details.length > 200 + ? '${widget.plan.details.substring(0, 200).replaceAll('\n', '
')}...' + : widget.plan.details.replaceAll('\n', '
'), style: { "*": Style( margin: Margins.zero, @@ -116,26 +98,61 @@ class _MyPlanWidgetState extends State { padding: const EdgeInsets.symmetric(horizontal: 16.0), child: Column( children: [ + // Buttons aligned with MyRecipeWidget: Delete (gray) and Share (blue) Row( children: [ Expanded( child: CustomOutlinedButton( - text: switch (state.selectedStateIndex) { - 1 => s.publish, - _ => s.edit, + onPressed: () async { + final result = await context.read().deletePlan(widget.plan.slug); + if (result) { + Future.delayed(const Duration(seconds: 3), () { + if (!context.mounted) return; + showBottomSheetWithBlur( + context: context, + child: Column( + children: [ + const SizedBox(height: 20), + const BottomSheetHandlerBar(), + const SizedBox(height: 20), + Text(widget.plan.name, style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)), + Text(s.deleted, style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)), + const SizedBox(height: 20), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: CustomOutlinedButton( + onPressed: () { + context.pop(); + }, + text: 'Хорошо', + height: 52, + buttonTextStyle: TextStyles.textSSemibold.copyWith(color: appTheme.whiteA700), + buttonStyle: ButtonStyle( + side: const WidgetStatePropertyAll(BorderSide.none), + backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), + ), + ), + ), + const SizedBox(height: 20), + ], + ), + ); + }); + } }, + text: 'Удалить', height: 52, - buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.black), buttonStyle: ButtonStyle( shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))), - backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), + backgroundColor: WidgetStatePropertyAll(appTheme.lightGray0), side: const WidgetStatePropertyAll(BorderSide.none), ), ), ), - if (state.selectedStateIndex == 0) ...[ - const SizedBox(width: 6), - CustomOutlinedButton( + const SizedBox(width: 8), + Expanded( + child: CustomOutlinedButton( onPressed: () async { final box = context.findRenderObject() as RenderBox?; await SharePlus.instance.share( @@ -146,109 +163,14 @@ class _MyPlanWidgetState extends State { ); }, height: 52, - width: 52, - leftIcon: SvgPicture.asset( - 'assets/icons/share2.svg', - width: 20, - height: 20, - colorFilter: ColorFilter.mode(appTheme.whiteA700, BlendMode.srcIn), - ), - buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.red), buttonStyle: ButtonStyle( shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))), backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), side: const WidgetStatePropertyAll(BorderSide.none), ), - text: '', + text: s.share, + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), ), - ], - ], - ), - const SizedBox(height: 8), - Row( - children: [ - Expanded( - child: CustomOutlinedButton( - text: switch (state.selectedStateIndex) { - 0 => s.removeFromActive, - _ => s.edit, - }, - height: 52, - buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.black), - buttonStyle: ButtonStyle( - shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))), - backgroundColor: WidgetStatePropertyAll(appTheme.lightGray0), - side: const WidgetStatePropertyAll(BorderSide.none), - ), - ), - ), - const SizedBox(width: 6), - CustomOutlinedButton( - onPressed: () async { - final result = await context.read().deletePlan(widget.plan.slug); - if (result) { - Future.delayed(const Duration(seconds: 3), () { - if (!context.mounted) return; - showBottomSheetWithBlur( - context: context, - child: Column( - children: [ - const SizedBox( - height: 20, - ), - const BottomSheetHandlerBar(), - const SizedBox( - height: 20, - ), - Text( - widget.plan.name, - style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), - ), - Text( - s.deleted, - style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), - ), - const SizedBox( - height: 20, - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: CustomOutlinedButton( - onPressed: () { - context.pop(); - }, - text: 'Хорошо', - height: 52, - buttonTextStyle: TextStyles.textSSemibold.copyWith(color: appTheme.whiteA700), - buttonStyle: ButtonStyle( - side: const WidgetStatePropertyAll(BorderSide.none), - backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), - ), - ), - ), - const SizedBox( - height: 20, - ), - ], - ), - ); - }); - } - }, - height: 52, - width: 52, - leftIcon: SvgPicture.asset( - 'assets/icons/trash_can.svg', - width: 20, - height: 20, - ), - buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.red), - buttonStyle: ButtonStyle( - shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))), - backgroundColor: WidgetStatePropertyAll(appTheme.lightGray0), - side: const WidgetStatePropertyAll(BorderSide.none), - ), - text: '', ), ], ), diff --git a/lib/features/my_plans/presentation/widgets/my_plans_tab.dart b/lib/features/my_plans/presentation/widgets/my_plans_tab.dart index c681efd1..2635bef1 100644 --- a/lib/features/my_plans/presentation/widgets/my_plans_tab.dart +++ b/lib/features/my_plans/presentation/widgets/my_plans_tab.dart @@ -1,71 +1,3 @@ -import 'package:eatvio_app/core/theme/custom_text_style.dart'; -import 'package:eatvio_app/core/theme/theme_helper.dart'; -import 'package:eatvio_app/core/widgets/animated_option_button.dart'; -import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_cubit.dart'; -import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_state.dart'; -import 'package:eatvio_app/generated/l10n.dart'; -import 'package:flutter/widgets.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; +// File intentionally left minimal after removing tabs for My Plans -class MyPlansTab extends StatefulWidget { - const MyPlansTab({super.key, this.additionalOnTap}); - final void Function()? additionalOnTap; - @override - State createState() => _MyPlansTabState(); -} - -class _MyPlansTabState extends State { - final List tabTitles = []; - @override - void didChangeDependencies() { - final s = S.of(context); - if (tabTitles.isEmpty) { - tabTitles.add(s.published); - tabTitles.add(s.drafted); - } - super.didChangeDependencies(); - } - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - return Align( - alignment: Alignment.centerLeft, - child: Row( - children: [ - Expanded( - child: Container( - height: 48, - decoration: BoxDecoration( - color: appTheme.whiteA700, - borderRadius: BorderRadius.circular(100), - ), - padding: const EdgeInsets.all(4), - child: Row( - children: [ - ...List.generate( - 2, - (index) => Expanded( - child: AnimatedOptionButton( - textStyle: TextStyles.textSMedium, - isSelected: state.selectedStateIndex == index, - text: tabTitles[index], - onTap: () { - widget.additionalOnTap?.call(); - context.read().changeSelectedStateIndex(index); - }, - ), - ), - ), - ], - ), - ), - ), - ], - ), - ); - }, - ); - } -} +// Drafts/published tab UI removed per requirements diff --git a/lib/features/profile/presentation/pages/profile_screen.dart b/lib/features/profile/presentation/pages/profile_screen.dart index cc8f7245..98966234 100644 --- a/lib/features/profile/presentation/pages/profile_screen.dart +++ b/lib/features/profile/presentation/pages/profile_screen.dart @@ -1,6 +1,4 @@ -import 'dart:developer'; import 'dart:io'; - import 'package:cached_network_image/cached_network_image.dart'; import 'package:dismissible_page/dismissible_page.dart'; import 'package:eatvio_app/core/app_router.dart'; From 087436da1ccbb89d23fa4f16dea3ee789bedc2b1 Mon Sep 17 00:00:00 2001 From: misha Date: Wed, 17 Sep 2025 15:13:16 +0300 Subject: [PATCH 19/28] changed logic of view requests sending --- lib/core/const.dart | 2 +- lib/core/locator_service.dart | 6 + lib/core/views/cubit/views_cubit.dart | 128 ++++++++++++++++++ lib/core/views/cubit/views_state.dart | 3 + .../views/repository/views_repository.dart | 16 +++ lib/core/views/service/views_service.dart | 15 ++ .../bloc/feed_cubit/feed_cubit.dart | 7 - .../feed/presentation/pages/feed_screen.dart | 83 +----------- .../feed/presentation/widgets/diets.dart | 2 +- .../feed/presentation/widgets/plans.dart | 2 +- .../feed/presentation/widgets/recipes.dart | 2 +- .../presentation/pages/profile_screen.dart | 1 - lib/main.dart | 6 +- 13 files changed, 183 insertions(+), 90 deletions(-) create mode 100644 lib/core/views/cubit/views_cubit.dart create mode 100644 lib/core/views/cubit/views_state.dart create mode 100644 lib/core/views/repository/views_repository.dart create mode 100644 lib/core/views/service/views_service.dart diff --git a/lib/core/const.dart b/lib/core/const.dart index a5f06149..98f7bc4a 100644 --- a/lib/core/const.dart +++ b/lib/core/const.dart @@ -1,6 +1,6 @@ import 'package:eatvio_app/core/env_config.dart'; -String baseUrl = EnvConfig.devBaseUrl; +String baseUrl = EnvConfig.baseUrl; bool hidePlans = const bool.fromEnvironment('HIDE_PLANS', defaultValue: false); const buildDate = String.fromEnvironment('BUILD_DATE', defaultValue: ''); diff --git a/lib/core/locator_service.dart b/lib/core/locator_service.dart index e4a13866..37713f29 100644 --- a/lib/core/locator_service.dart +++ b/lib/core/locator_service.dart @@ -5,6 +5,9 @@ import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/dio/phone_verify_error_interceptor.dart'; import 'package:eatvio_app/core/firebase_api.dart'; import 'package:eatvio_app/core/interceptors/token_interceptor.dart'; +import 'package:eatvio_app/core/views/cubit/views_cubit.dart'; +import 'package:eatvio_app/core/views/repository/views_repository.dart'; +import 'package:eatvio_app/core/views/service/views_service.dart'; import 'package:eatvio_app/core/websockets/ws_manager.dart'; import 'package:eatvio_app/core/websockets/ws_service.dart'; import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart'; @@ -179,6 +182,7 @@ Future init() async { sl.registerFactory(() => BlockedUsersCubit(blockedUsersRepository: sl())); sl.registerFactory(() => NotificationSettingsCubit(notificationSettingsRepository: sl())); sl.registerFactory(() => ConsultantRatingCubit(sl())); + sl.registerFactory(() => ViewsCubit(viewsRepository: sl())); // Repository sl.registerLazySingleton(() => ChatRepository(chatService: sl())); @@ -212,6 +216,7 @@ Future init() async { sl.registerLazySingleton(() => CommentsRepository(service: sl())); sl.registerLazySingleton(() => NotificationSettingsRepository(sl())); sl.registerFactory(() => ConsultantRatingRepository(sl())); + sl.registerFactory(() => ViewsRepository(viewsService: sl())); // Services sl.registerLazySingleton(() => ChatService(sl())); @@ -247,6 +252,7 @@ Future init() async { sl.registerLazySingleton(() => MyRecipesService(sl())); sl.registerLazySingleton(() => ConsultantRatingService(sl())); sl.registerLazySingleton(() => NotificationSettingsService(sl())); + sl.registerLazySingleton(() => ViewsService(sl())); // External sl.registerLazySingleton( diff --git a/lib/core/views/cubit/views_cubit.dart b/lib/core/views/cubit/views_cubit.dart new file mode 100644 index 00000000..ee15af22 --- /dev/null +++ b/lib/core/views/cubit/views_cubit.dart @@ -0,0 +1,128 @@ +import 'dart:async'; +import 'package:eatvio_app/core/views/cubit/views_state.dart'; +import 'package:eatvio_app/core/views/repository/views_repository.dart'; +import 'package:eatvio_app/features/feed/models/view_model.dart'; +import 'package:eatvio_app/features/feed/presentation/pages/feed_screen.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class ViewsCubit extends Cubit { + final ViewsRepository viewsRepository; + ViewsCubit({required this.viewsRepository}) : super(ViewsCubitState()) { + _listener = AppLifecycleListener(onPause: _sendViews, onInactive: _sendViews); + } + + late final AppLifecycleListener _listener; + + final Set postsIds = {}; + final Set plansIds = {}; + final Set dietsIds = {}; + final Set recipesIds = {}; + + final Set _sentPosts = {}; + final Set _sentPlans = {}; + final Set _sentDiets = {}; + final Set _sentRecipes = {}; + + final Map> _visibilityTimers = { + ViewableContentType.posts: {}, + ViewableContentType.plans: {}, + ViewableContentType.diets: {}, + ViewableContentType.recipes: {}, + }; + + Timer? _debounceTimer; + + Set _idsForType(ViewableContentType type) { + switch (type) { + case ViewableContentType.posts: + return postsIds; + case ViewableContentType.plans: + return plansIds; + case ViewableContentType.diets: + return dietsIds; + case ViewableContentType.recipes: + return recipesIds; + } + } + + Set _sentIdsForType(ViewableContentType type) { + switch (type) { + case ViewableContentType.posts: + return _sentPosts; + case ViewableContentType.plans: + return _sentPlans; + case ViewableContentType.diets: + return _sentDiets; + case ViewableContentType.recipes: + return _sentRecipes; + } + } + + void handleVisibility({required double visibleFraction, required int id, required ViewableContentType type}) { + final timers = _visibilityTimers[type]!; + final ids = _idsForType(type); + final sent = _sentIdsForType(type); + + if (sent.contains(id)) { + timers[id]?.cancel(); + timers.remove(id); + return; + } + + if (ids.contains(id)) return; + + if (visibleFraction >= 0.5) { + if (timers.containsKey(id)) return; + timers[id] = Timer(const Duration(seconds: 1), () { + ids.add(id); + timers.remove(id); + _scheduleDebouncedSend(); + }); + } else { + timers[id]?.cancel(); + timers.remove(id); + } + } + + Future _viewContent({required ViewableContentType type, required List ids}) async { + await viewsRepository.viewContent( + viewModel: ViewModel(type: type.getStringifiedVersion(), ids: ids), + ); + } + + void _scheduleDebouncedSend() { + _debounceTimer?.cancel(); + _debounceTimer = Timer(const Duration(seconds: 4), _sendViews); + } + + void _send(Set ids, Set sent, ViewableContentType type) { + final newIds = ids.difference(sent); + if (newIds.isNotEmpty) { + _viewContent(type: type, ids: newIds.toList()); + sent.addAll(newIds); + ids.removeAll(newIds); + } + } + + void _sendViews() { + _send(postsIds, _sentPosts, ViewableContentType.posts); + _send(plansIds, _sentPlans, ViewableContentType.plans); + _send(dietsIds, _sentDiets, ViewableContentType.diets); + _send(recipesIds, _sentRecipes, ViewableContentType.recipes); + } + + @override + Future close() async { + for (final map in _visibilityTimers.values) { + for (final t in map.values) { + t.cancel(); + } + map.clear(); + } + _debounceTimer?.cancel(); + _listener.dispose(); + _sendViews(); + await super.close(); + } +} diff --git a/lib/core/views/cubit/views_state.dart b/lib/core/views/cubit/views_state.dart new file mode 100644 index 00000000..e43d3ff5 --- /dev/null +++ b/lib/core/views/cubit/views_state.dart @@ -0,0 +1,3 @@ +class ViewsCubitState { + ViewsCubitState(); +} diff --git a/lib/core/views/repository/views_repository.dart b/lib/core/views/repository/views_repository.dart new file mode 100644 index 00000000..165a4956 --- /dev/null +++ b/lib/core/views/repository/views_repository.dart @@ -0,0 +1,16 @@ +import 'package:eatvio_app/core/views/service/views_service.dart'; +import 'package:eatvio_app/features/feed/models/view_model.dart'; + +class ViewsRepository { + final ViewsService viewsService; + + ViewsRepository({required this.viewsService}); + + Future viewContent({required ViewModel viewModel}) async { + try { + await viewsService.viewContent(viewModel: viewModel); + } catch (e) { + rethrow; + } + } +} diff --git a/lib/core/views/service/views_service.dart b/lib/core/views/service/views_service.dart new file mode 100644 index 00000000..e89990fa --- /dev/null +++ b/lib/core/views/service/views_service.dart @@ -0,0 +1,15 @@ +import 'package:dio/dio.dart'; +import 'package:eatvio_app/features/feed/models/view_model.dart'; +import 'package:retrofit/retrofit.dart'; + +part 'views_service.g.dart'; + +@RestApi() +abstract class ViewsService { + factory ViewsService(Dio dio, {String baseUrl}) = _ViewsService; + + @POST('/api/views') + Future viewContent({ + @Body() required ViewModel viewModel, + }); +} diff --git a/lib/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart b/lib/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart index 39ccdc6a..947b9a5d 100644 --- a/lib/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart +++ b/lib/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart @@ -1,9 +1,7 @@ import 'dart:developer'; import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart'; -import 'package:eatvio_app/features/feed/models/view_model.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_state.dart'; -import 'package:eatvio_app/features/feed/presentation/pages/feed_screen.dart'; import 'package:eatvio_app/features/feed/repositories/feed_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -84,9 +82,4 @@ class FeedCubit extends Cubit { Future viewDiet(int id) async => await feedRepository.viewDiet(id); Future viewPlan(String slug) async => await feedRepository.viewPlan(slug); - Future viewContent({required ViewableContentType type, List? ids, List? slugs}) async { - feedRepository.viewContent( - viewModel: ViewModel(type: type.getStringifiedVersion(), ids: ids, slugs: slugs), - ); - } } diff --git a/lib/features/feed/presentation/pages/feed_screen.dart b/lib/features/feed/presentation/pages/feed_screen.dart index 2bfaf897..844788fe 100644 --- a/lib/features/feed/presentation/pages/feed_screen.dart +++ b/lib/features/feed/presentation/pages/feed_screen.dart @@ -1,7 +1,6 @@ -import 'dart:async'; - import 'package:easy_debounce/easy_debounce.dart'; import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/views/cubit/views_cubit.dart'; import 'package:eatvio_app/core/widgets/add_item_button.dart'; import 'package:eatvio_app/core/widgets/custom_search_bar.dart'; import 'package:eatvio_app/features/create_diet/presentation/create_diet_screen.dart'; @@ -62,19 +61,15 @@ class _FeedScreenContentState extends State { final consultantsController = TextEditingController(); late final List chipsLabelList; - late final AppLifecycleListener _listener; int _selectedChipIndex = 0; final blogPostsScrollController = ScrollController(); final recipesScrollController = ScrollController(); final dietsScrollController = ScrollController(); final plansScrollController = ScrollController(); final consultantsScrollController = ScrollController(); - final _feedCubit = sl(); @override void initState() { - // Initialize chipsLabelList based on hidePlans setting chipsLabelList = hidePlans ? ['Все', 'Рецепты', 'Диеты', 'Консультанты'] : ['Все', 'Рецепты', 'Диеты', 'Планы питания', 'Консультанты']; - _listener = AppLifecycleListener(onDetach: _sendViews); consultantsScrollController.addListener(_onScrollConsultants); super.initState(); } @@ -83,20 +78,7 @@ class _FeedScreenContentState extends State { if (consultantsScrollController.offset >= consultantsScrollController.position.maxScrollExtent && !consultantsScrollController.position.outOfRange) {} } - void _sendViews() { - if (postsIds.isNotEmpty) { - _feedCubit.viewContent(type: ViewableContentType.posts, ids: postsIds.toList()); - } - if (plansIds.isNotEmpty) { - _feedCubit.viewContent(type: ViewableContentType.plans, ids: plansIds.toList()); - } - if (dietsIds.isNotEmpty) { - _feedCubit.viewContent(type: ViewableContentType.diets, ids: dietsIds.toList()); - } - if (recipesIds.isNotEmpty) { - _feedCubit.viewContent(type: ViewableContentType.recipes, ids: recipesIds.toList()); - } - } + @override void dispose() { @@ -105,63 +87,10 @@ class _FeedScreenContentState extends State { dietsScrollController.dispose(); plansScrollController.dispose(); consultantsScrollController.dispose(); - for (final map in _visibilityTimers.values) { - for (final t in map.values) { - t.cancel(); - } - map.clear(); - } - _listener.dispose(); - _sendViews(); super.dispose(); } - final Set postsIds = {}; - final Set plansIds = {}; - final Set dietsIds = {}; - final Set recipesIds = {}; - final Map> _visibilityTimers = { - ViewableContentType.posts: {}, - ViewableContentType.plans: {}, - ViewableContentType.diets: {}, - ViewableContentType.recipes: {}, - }; - - Set _idsForType(ViewableContentType type) { - switch (type) { - case ViewableContentType.posts: - return postsIds; - case ViewableContentType.plans: - return plansIds; - case ViewableContentType.diets: - return dietsIds; - case ViewableContentType.recipes: - return recipesIds; - } - } - - void _handleVisibility({required double visibleFraction, required int id, required ViewableContentType type}) { - final timers = _visibilityTimers[type]!; - final ids = _idsForType(type); - - if (ids.contains(id)) { - timers[id]?.cancel(); - timers.remove(id); - return; - } - - if (visibleFraction >= 0.5) { - if (timers.containsKey(id)) return; - timers[id] = Timer(const Duration(seconds: 1), () { - ids.add(id); - timers.remove(id); - }); - } else { - timers[id]?.cancel(); - timers.remove(id); - } - } @override Widget build(BuildContext context) { @@ -301,10 +230,10 @@ class _FeedScreenContentState extends State { ]; }, body: switch (_selectedChipIndex) { - 0 => BlogPosts(handleVisibility: _handleVisibility), - 1 => Recipes(handleVisibility: _handleVisibility), - 2 => Diets(handleVisibility: _handleVisibility), - 3 when !hidePlans => Plans(handleVisibility: _handleVisibility), + 0 => BlogPosts(handleVisibility: context.read().handleVisibility), + 1 => Recipes(handleVisibility: context.read().handleVisibility), + 2 => Diets(handleVisibility: context.read().handleVisibility), + 3 when !hidePlans => Plans(handleVisibility: context.read().handleVisibility), 3 when hidePlans => const Consultants(), 4 when !hidePlans => const Consultants(), _ => Center(child: Text(chipsLabelList[_selectedChipIndex])), diff --git a/lib/features/feed/presentation/widgets/diets.dart b/lib/features/feed/presentation/widgets/diets.dart index c5e5592a..080b1a14 100644 --- a/lib/features/feed/presentation/widgets/diets.dart +++ b/lib/features/feed/presentation/widgets/diets.dart @@ -42,7 +42,7 @@ class Diets extends StatelessWidget { key: ValueKey(index), onVisibilityChanged: (info) { final id = state.dietsList[index].id; - handleVisibility(visibleFraction: info.visibleFraction, id: id, type: ViewableContentType.posts); + handleVisibility(visibleFraction: info.visibleFraction, id: id, type: ViewableContentType.diets); }, child: DietsWidget(diet: state.dietsList[index], index: index), ); diff --git a/lib/features/feed/presentation/widgets/plans.dart b/lib/features/feed/presentation/widgets/plans.dart index 4a620945..42e107e5 100644 --- a/lib/features/feed/presentation/widgets/plans.dart +++ b/lib/features/feed/presentation/widgets/plans.dart @@ -42,7 +42,7 @@ class Plans extends StatelessWidget { key: ValueKey(index), onVisibilityChanged: (info) { final id = state.plans[index].id; - handleVisibility(visibleFraction: info.visibleFraction, id: id, type: ViewableContentType.posts); + handleVisibility(visibleFraction: info.visibleFraction, id: id, type: ViewableContentType.plans); }, child: PlanWidget(plan: state.plans[index], index: index), ); diff --git a/lib/features/feed/presentation/widgets/recipes.dart b/lib/features/feed/presentation/widgets/recipes.dart index d286949e..1271a493 100644 --- a/lib/features/feed/presentation/widgets/recipes.dart +++ b/lib/features/feed/presentation/widgets/recipes.dart @@ -46,7 +46,7 @@ class Recipes extends StatelessWidget { key: ValueKey(index), onVisibilityChanged: (info) { final id = state.recipesList[index].id; - handleVisibility(visibleFraction: info.visibleFraction, id: id, type: ViewableContentType.posts); + handleVisibility(visibleFraction: info.visibleFraction, id: id, type: ViewableContentType.recipes); }, child: RecipesWidget( recipe: state.recipesList[index], diff --git a/lib/features/profile/presentation/pages/profile_screen.dart b/lib/features/profile/presentation/pages/profile_screen.dart index cc8f7245..c11cb0b0 100644 --- a/lib/features/profile/presentation/pages/profile_screen.dart +++ b/lib/features/profile/presentation/pages/profile_screen.dart @@ -1,4 +1,3 @@ -import 'dart:developer'; import 'dart:io'; import 'package:cached_network_image/cached_network_image.dart'; diff --git a/lib/main.dart b/lib/main.dart index 60293ed8..d716f3fa 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,6 +3,7 @@ import 'package:eatvio_app/core/app_router.dart'; import 'package:eatvio_app/core/locator_service.dart' as di; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/core/views/cubit/views_cubit.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/new_messages_cubit/new_messages_cubit.dart'; import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; @@ -67,7 +68,10 @@ class MyApp extends StatelessWidget { child: MaterialApp.router( theme: ThemeHelper().themeData(), routerConfig: AppRouter.appRouter, - builder: (context, child) => InternetConnectionCheckerScreen(child: child!), + builder: (context, child) => BlocProvider( + create: (context) => sl(), + child: InternetConnectionCheckerScreen(child: child!), + ), debugShowCheckedModeBanner: false, localizationsDelegates: const [ S.delegate, From 2600b8cc79b01020eb19664303ec92b0a30b218b Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Wed, 17 Sep 2025 16:28:13 +0300 Subject: [PATCH 20/28] fix(auth): hide social buttons --- .../auth/presentation/pages/auth_screen.dart | 92 +++++++++---------- 1 file changed, 46 insertions(+), 46 deletions(-) diff --git a/lib/features/auth/presentation/pages/auth_screen.dart b/lib/features/auth/presentation/pages/auth_screen.dart index c32b8a39..8a4e711b 100644 --- a/lib/features/auth/presentation/pages/auth_screen.dart +++ b/lib/features/auth/presentation/pages/auth_screen.dart @@ -166,52 +166,52 @@ class _AuthScreenState extends State { ), ], ), - const SizedBox(height: 16), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - _SocialButton( - onTap: _onVkPressed, - backgroundColor: const Color(0xFF2787F5), - tooltip: 'VK ID', - child: SvgPicture.asset( - 'assets/icons/vk.svg', - height: 24, - width: 24, - colorFilter: const ColorFilter.mode(Colors.white, BlendMode.srcIn), - ), - ), - _SocialButton( - onTap: _onOkPressed, - backgroundColor: const Color(0xFFF58220), - tooltip: 'Одноклассники', - child: SvgPicture.asset( - 'assets/icons/ok.svg', - height: 24, - width: 24, - colorFilter: const ColorFilter.mode(Colors.white, BlendMode.srcIn), - ), - ), - const _SocialButton( - onTap: _onMailRuPressed, - backgroundColor: Color(0xFF005FF9), - tooltip: 'Mail.ru', - child: Text( - 'Mail', - style: TextStyle(color: Colors.white, fontWeight: FontWeight.w700), - ), - ), - const _SocialButton( - onTap: _onYandexPressed, - backgroundColor: Color(0xFFFFCC00), - tooltip: 'Яндекс ID', - child: Text( - 'Yandex', - style: TextStyle(color: Colors.black, fontWeight: FontWeight.w700), - ), - ), - ], - ), + // const SizedBox(height: 16), + // Row( + // mainAxisAlignment: MainAxisAlignment.spaceBetween, + // children: [ + // _SocialButton( + // onTap: _onVkPressed, + // backgroundColor: const Color(0xFF2787F5), + // tooltip: 'VK ID', + // child: SvgPicture.asset( + // 'assets/icons/vk.svg', + // height: 24, + // width: 24, + // colorFilter: const ColorFilter.mode(Colors.white, BlendMode.srcIn), + // ), + // ), + // _SocialButton( + // onTap: _onOkPressed, + // backgroundColor: const Color(0xFFF58220), + // tooltip: 'Одноклассники', + // child: SvgPicture.asset( + // 'assets/icons/ok.svg', + // height: 24, + // width: 24, + // colorFilter: const ColorFilter.mode(Colors.white, BlendMode.srcIn), + // ), + // ), + // const _SocialButton( + // onTap: _onMailRuPressed, + // backgroundColor: Color(0xFF005FF9), + // tooltip: 'Mail.ru', + // child: Text( + // 'Mail', + // style: TextStyle(color: Colors.white, fontWeight: FontWeight.w700), + // ), + // ), + // const _SocialButton( + // onTap: _onYandexPressed, + // backgroundColor: Color(0xFFFFCC00), + // tooltip: 'Яндекс ID', + // child: Text( + // 'Yandex', + // style: TextStyle(color: Colors.black, fontWeight: FontWeight.w700), + // ), + // ), + // ], + // ), const Spacer(), Center( child: RichText( From fafc2aa1f74c7e744bb29a65ecdf0f34447ec1d3 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Wed, 17 Sep 2025 16:32:24 +0300 Subject: [PATCH 21/28] fix: upd ver to 73 --- ios/Runner.xcodeproj/project.pbxproj | 6 +++--- ios/Runner/Info.plist | 2 +- pubspec.yaml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 668075b4..1fee690d 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -388,7 +388,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 72; + CURRENT_PROJECT_VERSION = 73; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -539,7 +539,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 72; + CURRENT_PROJECT_VERSION = 73; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -578,7 +578,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 72; + CURRENT_PROJECT_VERSION = 73; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index b05e5fac..f0f66aff 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -21,7 +21,7 @@ CFBundleSignature ???? CFBundleVersion - 72 + 73 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/pubspec.yaml b/pubspec.yaml index acf68969..6861a603 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: eatvio_app description: A new Flutter project. -version: 1.2.12+72 +version: 1.2.13+73 publish_to: 'none' environment: From fef4451f4e70584782e020dc948e5bb52a1ce066 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Wed, 17 Sep 2025 17:00:44 +0300 Subject: [PATCH 22/28] fix(profile): Issue-523 --- ios/Runner.xcodeproj/project.pbxproj | 6 +- ios/Runner/Info.plist | 2 +- lib/core/theme/theme_helper.dart | 2 + .../auth/presentation/pages/auth_screen.dart | 4 ++ .../my_finance/models/card_model.dart | 3 + .../profile/models/profile_model.dart | 1 + .../presentation/pages/profile_screen.dart | 60 +++++++++---------- pubspec.yaml | 2 +- .../services/profile_service_test.dart | 13 ++-- 9 files changed, 49 insertions(+), 44 deletions(-) diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 1fee690d..8e9edd22 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -388,7 +388,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 73; + CURRENT_PROJECT_VERSION = 74; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -539,7 +539,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 73; + CURRENT_PROJECT_VERSION = 74; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -578,7 +578,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 73; + CURRENT_PROJECT_VERSION = 74; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index f0f66aff..7e82c5b2 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -21,7 +21,7 @@ CFBundleSignature ???? CFBundleVersion - 73 + 74 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/lib/core/theme/theme_helper.dart b/lib/core/theme/theme_helper.dart index c0c0af29..c202129e 100644 --- a/lib/core/theme/theme_helper.dart +++ b/lib/core/theme/theme_helper.dart @@ -204,6 +204,8 @@ class PrimaryColors { Color get commentBg => const Color(0XFFFFFDD8); Color get cardNotificationsBG => const Color(0xFFA4A4A4); + + Color get grey7B => const Color(0xFF7B7B7B); } PrimaryColors get appTheme => ThemeHelper().themeColor(); diff --git a/lib/features/auth/presentation/pages/auth_screen.dart b/lib/features/auth/presentation/pages/auth_screen.dart index 8a4e711b..ea457d6c 100644 --- a/lib/features/auth/presentation/pages/auth_screen.dart +++ b/lib/features/auth/presentation/pages/auth_screen.dart @@ -1,3 +1,5 @@ +// ignore_for_file: unused_element, duplicate_ignore + import 'dart:async'; import 'package:eatvio_app/core/app_router.dart'; import 'package:eatvio_app/core/const.dart'; @@ -365,6 +367,7 @@ class _AuthScreenState extends State { } } +// ignore: unused_element class _SocialButton extends StatelessWidget { const _SocialButton({required this.onTap, required this.child, required this.backgroundColor, required this.tooltip}); @@ -391,6 +394,7 @@ class _SocialButton extends StatelessWidget { } } +// ignore: unused_element void _onVkPressed() async { final accessToken = await SocialAuthFacade.signInWithVk(); if (accessToken == null) return; diff --git a/lib/features/my_finance/models/card_model.dart b/lib/features/my_finance/models/card_model.dart index 0435ea56..d248cace 100644 --- a/lib/features/my_finance/models/card_model.dart +++ b/lib/features/my_finance/models/card_model.dart @@ -15,6 +15,9 @@ abstract class CardModel with _$CardModel { } bool parseBool(dynamic json) { + if (json == null) { + throw Exception('parseBool: json is null'); + } if (json == 1 || json == true || json == '1') { return true; } diff --git a/lib/features/profile/models/profile_model.dart b/lib/features/profile/models/profile_model.dart index 03fd57f3..17eaa671 100644 --- a/lib/features/profile/models/profile_model.dart +++ b/lib/features/profile/models/profile_model.dart @@ -19,6 +19,7 @@ abstract class ProfileModel with _$ProfileModel { @JsonKey(name: "updated_at") DateTime? updatedAt, @JsonKey(name: "is_public_profile", fromJson: parseBool) required bool isPublicProfile, @JsonKey(name: "is_blocked_by_me", fromJson: parseBool) required bool isBlockedByMe, + @JsonKey(name: "is_blocked_by_author", fromJson: parseBool) required bool isBlockedByAuthor, @JsonKey(name: "is_blocked_by_user", fromJson: parseBool, defaultValue: false) required bool isBlockedByUser, @JsonKey(name: 'is_entity_subscribed_by_me') required bool isEntitySubscribedByMe, @JsonKey(name: "slug") required String slug, diff --git a/lib/features/profile/presentation/pages/profile_screen.dart b/lib/features/profile/presentation/pages/profile_screen.dart index 98966234..11ae9b7a 100644 --- a/lib/features/profile/presentation/pages/profile_screen.dart +++ b/lib/features/profile/presentation/pages/profile_screen.dart @@ -5,6 +5,7 @@ import 'package:eatvio_app/core/app_router.dart'; import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/enums/complained_content.dart'; import 'package:eatvio_app/core/permission_handler/permission_handler.dart'; +import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/utils/date_time_utils.dart'; import 'package:eatvio_app/core/widgets/data_error_widget.dart'; import 'package:eatvio_app/core/widgets/loader.dart'; @@ -93,30 +94,6 @@ class _ProfileScreenState extends State { isFirstFetch = false; } - // Проверяем, заблокировал ли пользователь меня - if (state.profile.isBlockedByUser) { - return Scaffold( - appBar: AppBar(), - body: Center( - child: Padding( - padding: const EdgeInsets.all(32.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.block, size: 64, color: Colors.grey[400]), - const SizedBox(height: 16), - Text( - 'Пользователь ограничил доступ к своей странице', - textAlign: TextAlign.center, - style: TextStyle(fontSize: 18, fontWeight: FontWeight.w500, color: Colors.grey[600]), - ), - ], - ), - ), - ), - ); - } - return widget.author == null || widget.author!.authorIsMe ? _myProfileScreen(state) : _profileScreen(state); } else if (state is ProfileStateError) { return Scaffold( @@ -198,11 +175,34 @@ class _ProfileScreenState extends State { ), _subscriptionsAndSubscribers(state), if (state.profile.badges.isNotEmpty) Padding(padding: const EdgeInsets.only(bottom: 16), child: _badges(state)), - if (state.profile.roles.contains('consultant') && state.consultantInfo != null && !state.profile.isBlockedByMe) _socialLinks(state), - if (state.profile.roles.contains('consultant') && state.consultantInfo != null && !state.profile.isBlockedByMe) _education(state), - if (state.profile.roles.contains('consultant') && state.consultantInfo != null && !state.profile.isBlockedByMe) _diplomasAndCertificates(state), - if (state.profile.roles.contains('consultant') && state.consultantInfo != null && !hidePlans && !state.profile.isBlockedByMe) _activePlans(state), - if (state.blogPosts.isNotEmpty) + if (state.profile.isBlockedByAuthor) + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.block, size: 48, color: appTheme.grey7B), + const SizedBox(height: 16), + Center( + child: Text( + 'Пользователь ограничил доступ\nк своей странице', + textAlign: TextAlign.center, + style: TextStyles.textMSemibold.copyWith(color: appTheme.grey7B), + ), + ), + ], + ), + if (state.profile.roles.contains('consultant') && state.consultantInfo != null && !state.profile.isBlockedByMe && !state.profile.isBlockedByAuthor) + _socialLinks(state), + if (state.profile.roles.contains('consultant') && state.consultantInfo != null && !state.profile.isBlockedByMe && !state.profile.isBlockedByAuthor) + _education(state), + if (state.profile.roles.contains('consultant') && state.consultantInfo != null && !state.profile.isBlockedByMe && !state.profile.isBlockedByAuthor) + _diplomasAndCertificates(state), + if (state.profile.roles.contains('consultant') && + state.consultantInfo != null && + !hidePlans && + !state.profile.isBlockedByMe && + !state.profile.isBlockedByAuthor) + _activePlans(state), + if (state.blogPosts.isNotEmpty && !state.profile.isBlockedByAuthor && !state.profile.isBlockedByMe) const Padding( padding: EdgeInsets.only(left: 8, top: 16, bottom: 8), child: Text( @@ -210,7 +210,7 @@ class _ProfileScreenState extends State { style: TextStyle(color: Color(0xFF323F4B), fontSize: 24, fontWeight: FontWeight.w600), ), ), - _blogPosts(state), + if (state.blogPosts.isNotEmpty && !state.profile.isBlockedByAuthor && !state.profile.isBlockedByMe) _blogPosts(state), ], ), ), diff --git a/pubspec.yaml b/pubspec.yaml index 6861a603..5fec1116 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: eatvio_app description: A new Flutter project. -version: 1.2.13+73 +version: 1.2.14+74 publish_to: 'none' environment: diff --git a/test/features/profile/services/profile_service_test.dart b/test/features/profile/services/profile_service_test.dart index 8c666344..5c773bc3 100644 --- a/test/features/profile/services/profile_service_test.dart +++ b/test/features/profile/services/profile_service_test.dart @@ -30,6 +30,7 @@ void main() { badges: [], subscriptions: [], subscribers: [], + isBlockedByAuthor: false, ), ), ); @@ -72,23 +73,17 @@ void main() { }); test('getBlogPostByAuthorSlug', () async { - when(mockProfileService.getBlogPostByAuthorSlug(slug: '')).thenAnswer( - (_) async => [], - ); + when(mockProfileService.getBlogPostByAuthorSlug(slug: '')).thenAnswer((_) async => []); expect(await mockProfileService.getBlogPostByAuthorSlug(slug: 'slug'), isA>()); }); test('getWeight', () async { - when(mockProfileService.getWeight(period: '')).thenAnswer( - (_) async => [], - ); + when(mockProfileService.getWeight(period: '')).thenAnswer((_) async => []); expect(await mockProfileService.getWeight(period: ''), isA>()); }); test('getCircumferences', () async { - when(mockProfileService.getCircumferences(period: '')).thenAnswer( - (_) async => [], - ); + when(mockProfileService.getCircumferences(period: '')).thenAnswer((_) async => []); expect(await mockProfileService.getCircumferences(period: ''), isA>()); }); }); From e6df89c98ad0587f26a2a21c869cb7a0cc1b8449 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Fri, 19 Sep 2025 15:55:07 +0300 Subject: [PATCH 23/28] fix(feed): Issue-591 --- .../presentation/cubit/blog_post_cubit.dart | 22 +++++ .../presentation/pages/blog_post_screen.dart | 90 +++++++++-------- .../widgets/consultation_card_widget.dart | 3 +- .../bloc/feed_cubit/feed_cubit.dart | 14 ++- .../feed/presentation/pages/feed_screen.dart | 5 - .../widgets/blog_post_widget.dart | 99 +++++-------------- .../my_finance/models/card_model.dart | 3 - 7 files changed, 100 insertions(+), 136 deletions(-) diff --git a/lib/features/blog_post/presentation/cubit/blog_post_cubit.dart b/lib/features/blog_post/presentation/cubit/blog_post_cubit.dart index 59665002..02196288 100644 --- a/lib/features/blog_post/presentation/cubit/blog_post_cubit.dart +++ b/lib/features/blog_post/presentation/cubit/blog_post_cubit.dart @@ -18,4 +18,26 @@ class BlogPostCubit extends Cubit { emit(state.copyWith(isLoading: false)); } } + + Future toggleLike() async { + final current = state.blogPostModel; + if (current == null) return; + + // optimistic update + final wasLiked = current.wasLikedByMe; + final updated = current.copyWith(wasLikedByMe: !wasLiked, totalLikes: wasLiked ? current.totalLikes - 1 : current.totalLikes + 1); + emit(state.copyWith(blogPostModel: updated)); + + try { + if (wasLiked) { + await feedRepository.unlikedBlogPost(id: current.id); + } else { + await feedRepository.likeBlogPost(id: current.id); + } + } catch (e, st) { + // rollback on error + log(e.toString(), stackTrace: st); + emit(state.copyWith(blogPostModel: current)); + } + } } diff --git a/lib/features/blog_post/presentation/pages/blog_post_screen.dart b/lib/features/blog_post/presentation/pages/blog_post_screen.dart index 25636a57..61b64f9e 100644 --- a/lib/features/blog_post/presentation/pages/blog_post_screen.dart +++ b/lib/features/blog_post/presentation/pages/blog_post_screen.dart @@ -32,8 +32,6 @@ class _BlogPostScreenState extends State { final CommentsCubit commentsCubit = sl(); final textEditingController = TextEditingController(); int? replyId; - bool isLiked = false; - int totalLikes = 0; @override void initState() { @@ -60,26 +58,30 @@ class _BlogPostScreenState extends State { builder: (BuildContext context, CommentsState state) { if (blogPostState.isLoading) { return Scaffold( - appBar: AppBar( - leading: BackButton( - onPressed: () { - if (context.canPop()) { - GoRouter.of(context).pop(); - return; - } - GoRouter.of(context).go('/feed-screen'); - }, - ), + appBar: AppBar( + leading: BackButton( + onPressed: () { + if (context.canPop()) { + GoRouter.of(context).pop(); + return; + } + GoRouter.of(context).go('/feed-screen'); + }, ), - body: const Loader()); + ), + body: const Loader(), + ); } - isLiked = blogPostState.blogPostModel?.wasLikedByMe ?? false; - totalLikes = blogPostState.blogPostModel?.totalLikes ?? 0; + final bool isLiked = blogPostState.blogPostModel?.wasLikedByMe ?? false; + final int totalLikes = blogPostState.blogPostModel?.totalLikes ?? 0; return Scaffold( resizeToAvoidBottomInset: true, bottomSheet: Container( padding: const EdgeInsets.symmetric(vertical: 12) + (Platform.isIOS ? const EdgeInsets.only(bottom: 16) : EdgeInsets.zero), - decoration: BoxDecoration(color: Colors.white, border: Border(top: BorderSide(color: Colors.black.withValues(alpha: 0.1)))), + decoration: BoxDecoration( + color: Colors.white, + border: Border(top: BorderSide(color: Colors.black.withValues(alpha: 0.1))), + ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -114,24 +116,14 @@ class _BlogPostScreenState extends State { maxLines: 5, minLines: 1, keyboardType: TextInputType.multiline, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w400, - color: Colors.black, - height: 1.2, - ), + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, color: Colors.black, height: 1.2), decoration: InputDecoration( isDense: false, // убираем true, чтобы padding учитывался корректно contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), // вертикальный паддинг увеличен hintText: 'Комментарий', - hintStyle: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w400, - height: 1.2, - color: Color(0xFFAEAEB2), - ), + hintStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, height: 1.2, color: Color(0xFFAEAEB2)), filled: true, border: OutlineInputBorder( borderRadius: BorderRadius.circular(16), @@ -196,7 +188,7 @@ class _BlogPostScreenState extends State { id: widget.id, feedBlogPostModel: blogPostState.blogPostModel, userSlug: blogPostState.blogPostModel?.author.authorSlug ?? '', - ), + ), const SizedBox(width: 16), ], ), @@ -215,9 +207,7 @@ class _BlogPostScreenState extends State { children: [ CircleAvatar( radius: 16, - foregroundImage: NetworkImage( - baseUrl + (blogPostState.blogPostModel?.author.authorProfileIcon ?? ''), - ), + foregroundImage: NetworkImage(baseUrl + (blogPostState.blogPostModel?.author.authorProfileIcon ?? '')), backgroundImage: const AssetImage('assets/images/no_avatar.png'), ), const SizedBox(width: 8), @@ -242,30 +232,32 @@ class _BlogPostScreenState extends State { if (blogPostState.blogPostModel?.description.isNotEmpty ?? false) Padding( padding: const EdgeInsets.symmetric(horizontal: 8), - child: Html(data: blogPostState.blogPostModel?.description.replaceAll('\n', '
'), style: {"*": Style(color: Colors.black87)}), + child: Html( + data: blogPostState.blogPostModel?.description.replaceAll('\n', '
'), + style: {"*": Style(color: Colors.black87)}, + ), ), const SizedBox(height: 8), if (blogPostState.blogPostModel?.image.isNotEmpty ?? false) - CachedNetworkImage( - imageUrl: '$baseUrl/storage/${blogPostState.blogPostModel?.image ?? ''}', - ), + CachedNetworkImage(imageUrl: '$baseUrl/storage/${blogPostState.blogPostModel?.image ?? ''}'), Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Row( children: [ _iconButton( - icon: Icons.favorite_border_rounded, + icon: isLiked ? Icons.favorite_rounded : Icons.favorite_border_rounded, text: totalLikes.toString(), iconColor: isLiked ? Colors.red.withValues(alpha: 0.8) : null, backgroundColor: isLiked ? Colors.red.withValues(alpha: 0.1) : null, textColor: isLiked ? Colors.red.withValues(alpha: 0.8) : null, onTap: () async { - setState(() { - isLiked = !isLiked; - isLiked ? totalLikes++ : totalLikes--; - }); - isLiked ? await context.read().likePost(widget.id) : await context.read().unlikedPost(widget.id); - sl().updateFeedBlogPostList(blogPostState.blogPostModel, isLiked); + await context.read().toggleLike(); + // ignore: use_build_context_synchronously + final updated = context.read().state.blogPostModel; + if (updated != null) { + // ignore: use_build_context_synchronously + context.read().updateFeedBlogPostList(updated, updated.wasLikedByMe); + } }, ), const SizedBox(width: 8), @@ -284,8 +276,10 @@ class _BlogPostScreenState extends State { const Spacer(), Icon(Icons.visibility_rounded, size: 16, color: appTheme.gray500), const SizedBox(width: 4), - Text(blogPostState.blogPostModel?.totalViews.toString() ?? '', - style: TextStyle(fontSize: 12, color: appTheme.gray500, fontWeight: FontWeight.w400)), + Text( + blogPostState.blogPostModel?.totalViews.toString() ?? '', + style: TextStyle(fontSize: 12, color: appTheme.gray500, fontWeight: FontWeight.w400), + ), ], ), ), @@ -332,7 +326,11 @@ class _BlogPostScreenState extends State { children: [ Icon(icon, size: 20, color: iconColor ?? appTheme.gray600), if (text != null) const SizedBox(width: 8), - if (text != null) Text(text, style: TextStyle(color: textColor ?? appTheme.gray600, fontWeight: FontWeight.w500)), + if (text != null) + Text( + text, + style: TextStyle(color: textColor ?? appTheme.gray600, fontWeight: FontWeight.w500), + ), ], ), ), diff --git a/lib/features/consultations/presentation/widgets/consultation_card_widget.dart b/lib/features/consultations/presentation/widgets/consultation_card_widget.dart index 3d870e8d..f9fa4143 100644 --- a/lib/features/consultations/presentation/widgets/consultation_card_widget.dart +++ b/lib/features/consultations/presentation/widgets/consultation_card_widget.dart @@ -1,5 +1,4 @@ import 'package:collection/collection.dart'; -import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/widgets/custom_divider.dart'; @@ -66,7 +65,7 @@ class _ConsultationCardWidgetState extends State { children: [ Row( children: [ - CircleAvatar(radius: 30, backgroundImage: NetworkImage(baseUrl + widget.consultationsModel.userProfilePhotoUrl)), + CircleAvatar(radius: 30, backgroundImage: NetworkImage(widget.consultationsModel.userProfilePhotoUrl)), const SizedBox(width: 12), Column( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/lib/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart b/lib/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart index 947b9a5d..d1bf399f 100644 --- a/lib/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart +++ b/lib/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart @@ -15,13 +15,12 @@ class FeedCubit extends Cubit { FeedCubit({required this.feedRepository}) : super(const FeedState.loading()); void updateFeedBlogPostList(FeedBlogPostModel? feedBlogPostModel, bool wasLikedByMe) { - if (feedBlogPostModel == null || isClosed) return; - List feedBlogPostList = [...(state as FeedStateLoaded).feedBlogPostList]; - final int index = feedBlogPostList.indexOf(feedBlogPostModel); - feedBlogPostList[index] = feedBlogPostModel.copyWith( - wasLikedByMe: wasLikedByMe, - totalLikes: wasLikedByMe == false ? feedBlogPostList[index].totalLikes - 1 : feedBlogPostList[index].totalLikes + 1, - ); + if (feedBlogPostModel == null || isClosed || state is! FeedStateLoaded) return; + final List feedBlogPostList = [...(state as FeedStateLoaded).feedBlogPostList]; + final int index = feedBlogPostList.indexWhere((e) => e.id == feedBlogPostModel.id); + if (index == -1) return; + final current = feedBlogPostList[index]; + feedBlogPostList[index] = current.copyWith(wasLikedByMe: wasLikedByMe, totalLikes: wasLikedByMe ? current.totalLikes + 1 : current.totalLikes - 1); if (!isClosed) { emit(FeedState.loaded(feedBlogPostList: feedBlogPostList, isLoading: isLoading)); } @@ -81,5 +80,4 @@ class FeedCubit extends Cubit { Future viewRecipe(String slug) async => await feedRepository.viewRecipe(slug); Future viewDiet(int id) async => await feedRepository.viewDiet(id); Future viewPlan(String slug) async => await feedRepository.viewPlan(slug); - } diff --git a/lib/features/feed/presentation/pages/feed_screen.dart b/lib/features/feed/presentation/pages/feed_screen.dart index 844788fe..796b4a3b 100644 --- a/lib/features/feed/presentation/pages/feed_screen.dart +++ b/lib/features/feed/presentation/pages/feed_screen.dart @@ -36,7 +36,6 @@ class FeedScreen extends StatelessWidget { providers: [ BlocProvider(create: (context) => sl()..loadRecipesList()), BlocProvider(create: (context) => sl()..getProfile()), - BlocProvider(create: (context) => sl()..loadFeedBlogPostList()), BlocProvider(create: (context) => sl()..loadDiets()), if (!hidePlans) BlocProvider(create: (context) => sl()..loadPlans()), BlocProvider(create: (context) => sl()..getConsultants()), @@ -78,8 +77,6 @@ class _FeedScreenContentState extends State { if (consultantsScrollController.offset >= consultantsScrollController.position.maxScrollExtent && !consultantsScrollController.position.outOfRange) {} } - - @override void dispose() { blogPostsScrollController.dispose(); @@ -90,8 +87,6 @@ class _FeedScreenContentState extends State { super.dispose(); } - - @override Widget build(BuildContext context) { FlutterNativeSplash.remove(); diff --git a/lib/features/feed/presentation/widgets/blog_post_widget.dart b/lib/features/feed/presentation/widgets/blog_post_widget.dart index 23eaeded..e001a038 100644 --- a/lib/features/feed/presentation/widgets/blog_post_widget.dart +++ b/lib/features/feed/presentation/widgets/blog_post_widget.dart @@ -21,10 +21,7 @@ import 'package:share_plus/share_plus.dart'; class BlogPostWidget extends StatefulWidget { final int index; - const BlogPostWidget({ - super.key, - required this.index, - }); + const BlogPostWidget({super.key, required this.index}); @override State createState() => _BlogPostWidgetState(); @@ -44,10 +41,7 @@ class _BlogPostWidgetState extends State { totalLikes = state.feedBlogPostList[widget.index].totalLikes; return InkWell( borderRadius: BorderRadius.circular(12), - onTap: () => GoRouter.of(context).pushNamed( - 'blogPostScreen', - extra: state.feedBlogPostList[widget.index].id, - ), + onTap: () => GoRouter.of(context).pushNamed('blogPostScreen', extra: state.feedBlogPostList[widget.index].id), child: Material( borderRadius: BorderRadius.circular(12), color: Colors.white, @@ -95,21 +89,12 @@ class _BlogPostWidgetState extends State { Text( state.feedBlogPostList[widget.index].author.authorName, overflow: TextOverflow.ellipsis, - style: const TextStyle( - height: 1, - color: Colors.black87, - fontWeight: FontWeight.w500, - fontSize: 16, - ), + style: const TextStyle(height: 1, color: Colors.black87, fontWeight: FontWeight.w500, fontSize: 16), ), const SizedBox(height: 4), Text( '${calculateDifference(state.feedBlogPostList[widget.index].createdAt)} в ${state.feedBlogPostList[widget.index].createdAt.hour.toString().padLeft(2, '0')}:${state.feedBlogPostList[widget.index].createdAt.minute.toString().padLeft(2, '0')}', - style: const TextStyle( - height: 1, - color: Colors.black45, - fontSize: 14, - ), + style: const TextStyle(height: 1, color: Colors.black45, fontSize: 14), ), ], ), @@ -119,9 +104,7 @@ class _BlogPostWidgetState extends State { !state.feedBlogPostList[widget.index].author.authorIsMe) Padding( padding: const EdgeInsets.only(right: 8), - child: SubscribeButtonWidget( - slug: state.feedBlogPostList[widget.index].author.authorSlug, - ), + child: SubscribeButtonWidget(slug: state.feedBlogPostList[widget.index].author.authorSlug), ), // Кнопка меню CustomPopup( @@ -141,11 +124,7 @@ class _BlogPostWidgetState extends State { padding: const EdgeInsets.symmetric(horizontal: 12), child: Text( state.feedBlogPostList[widget.index].title, - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - color: Colors.black87, - ), + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Colors.black87), ), ), ], @@ -156,15 +135,12 @@ class _BlogPostWidgetState extends State { Padding( padding: const EdgeInsets.symmetric(horizontal: 4), child: Html( - data: (state.feedBlogPostList[widget.index].description.length > 200 - ? state.feedBlogPostList[widget.index].description.substring(0, 200) - : state.feedBlogPostList[widget.index].description).replaceAll('\n', '
'), - style: { - "*": Style( - fontSize: FontSize(16), - color: Colors.black87, - ), - }, + data: + (state.feedBlogPostList[widget.index].description.length > 200 + ? state.feedBlogPostList[widget.index].description.substring(0, 200) + : state.feedBlogPostList[widget.index].description) + .replaceAll('\n', '
'), + style: {"*": Style(fontSize: FontSize(16), color: Colors.black87)}, ), ), if (state.feedBlogPostList[widget.index].description.length > 200) @@ -172,12 +148,7 @@ class _BlogPostWidgetState extends State { padding: EdgeInsets.symmetric(horizontal: 12), child: Text( 'Показать ещё', - style: TextStyle( - height: 1, - color: Color(0xFF8B8B8B), - fontSize: 14, - fontWeight: FontWeight.w600, - ), + style: TextStyle(height: 1, color: Color(0xFF8B8B8B), fontSize: 14, fontWeight: FontWeight.w600), ), ), if (state.feedBlogPostList[widget.index].title.isNotEmpty || state.feedBlogPostList[widget.index].description.isNotEmpty) @@ -186,10 +157,7 @@ class _BlogPostWidgetState extends State { onTap: () { context.pushTransparentRoute( rootNavigator: true, - PhotoViewScreen( - imageUrls: ['$baseUrl/storage/${state.feedBlogPostList[widget.index].image}'], - index: 0, - ), + PhotoViewScreen(imageUrls: ['$baseUrl/storage/${state.feedBlogPostList[widget.index].image}'], index: 0), ); }, child: Hero( @@ -218,11 +186,14 @@ class _BlogPostWidgetState extends State { isLiked = !isLiked; isLiked ? totalLikes++ : totalLikes--; }); - isLiked - ? await context.read().likePost(state.feedBlogPostList[widget.index].id) - : await context.read().unlikedPost(state.feedBlogPostList[widget.index].id); - if (!context.mounted) return; - await context.read().loadFeedBlogPostList(); + // Update feed list immediately by ID (no reload) + context.read().updateFeedBlogPostList(state.feedBlogPostList[widget.index], isLiked); + // Fire network call without introducing UI delay + if (isLiked) { + await context.read().likePost(state.feedBlogPostList[widget.index].id); + } else { + await context.read().unlikedPost(state.feedBlogPostList[widget.index].id); + } }, ), const SizedBox(width: 8), @@ -231,16 +202,10 @@ class _BlogPostWidgetState extends State { 'assets/icons/comment.svg', height: 20, width: 20, - colorFilter: ColorFilter.mode( - appTheme.gray600, - BlendMode.srcIn, - ), + colorFilter: ColorFilter.mode(appTheme.gray600, BlendMode.srcIn), ), text: state.feedBlogPostList[widget.index].totalComments.toString(), - onTap: () => GoRouter.of(context).pushNamed( - 'blogPostScreen', - extra: state.feedBlogPostList[widget.index].id, - ), + onTap: () => GoRouter.of(context).pushNamed('blogPostScreen', extra: state.feedBlogPostList[widget.index].id), ), const SizedBox(width: 8), FeedIconButton( @@ -254,28 +219,18 @@ class _BlogPostWidgetState extends State { final box = context.findRenderObject() as RenderBox?; SharePlus.instance.share( ShareParams( - uri: Uri.parse( - '$baseUrl/ru/blog_post/${state.feedBlogPostList[widget.index].id}', - ), + uri: Uri.parse('$baseUrl/ru/blog_post/${state.feedBlogPostList[widget.index].id}'), sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, ), ); }, ), const Spacer(), - Icon( - Icons.visibility_rounded, - size: 16, - color: appTheme.gray500, - ), + Icon(Icons.visibility_rounded, size: 16, color: appTheme.gray500), const SizedBox(width: 4), Text( state.feedBlogPostList[widget.index].totalViews.toString(), - style: TextStyle( - fontSize: 12, - color: appTheme.gray500, - fontWeight: FontWeight.w400, - ), + style: TextStyle(fontSize: 12, color: appTheme.gray500, fontWeight: FontWeight.w400), ), ], ), diff --git a/lib/features/my_finance/models/card_model.dart b/lib/features/my_finance/models/card_model.dart index d248cace..0435ea56 100644 --- a/lib/features/my_finance/models/card_model.dart +++ b/lib/features/my_finance/models/card_model.dart @@ -15,9 +15,6 @@ abstract class CardModel with _$CardModel { } bool parseBool(dynamic json) { - if (json == null) { - throw Exception('parseBool: json is null'); - } if (json == 1 || json == true || json == '1') { return true; } From ad777c695bd38fd3a308b095c853bf4f8474e62a Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Mon, 22 Sep 2025 17:08:41 +0300 Subject: [PATCH 24/28] fix(consultations): Issue-393 --- .../widgets/mood_rating_widget.dart | 93 +++++-------------- .../widgets/satiety_rating_widget.dart | 93 +++++-------------- .../diary/repositories/diary_repository.dart | 48 +++++++++- .../presentation/user_details_screen.dart | 4 +- 4 files changed, 99 insertions(+), 139 deletions(-) diff --git a/lib/features/diary/presentation/widgets/mood_rating_widget.dart b/lib/features/diary/presentation/widgets/mood_rating_widget.dart index 41640b78..d943eac2 100644 --- a/lib/features/diary/presentation/widgets/mood_rating_widget.dart +++ b/lib/features/diary/presentation/widgets/mood_rating_widget.dart @@ -23,9 +23,12 @@ class _MoodRatingWidgetState extends State { builder: (BuildContext context, DiaryState state) { if (state is DiaryStateLoaded) { final selectedIndex = state.selectedMeal - 1; - if (isFirstFetch[selectedIndex]) { - currentSelectedRating[selectedIndex] = state.diary.meals[selectedIndex].mood.toDouble() + 1; - isFirstFetch[selectedIndex] = false; + final meals = state.diary.meals; + if (selectedIndex >= 0 && selectedIndex < meals.length) { + if (isFirstFetch[selectedIndex]) { + currentSelectedRating[selectedIndex] = meals[selectedIndex].mood.toDouble() + 1; + isFirstFetch[selectedIndex] = false; + } } return Padding( padding: const EdgeInsets.only(top: 24), @@ -35,21 +38,17 @@ class _MoodRatingWidgetState extends State { Text( state.isConsultantView ? 'Настроение пользователя после приема пищи' : 'Ваше настроение после приёма пищи', textAlign: TextAlign.center, - style: const TextStyle( - fontSize: 16, - ), + style: const TextStyle(fontSize: 16), ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - SvgPicture.asset( - 'assets/icons/mood_1.svg', - height: 32, - width: 32, - ), + SvgPicture.asset('assets/icons/mood_1.svg', height: 32, width: 32), RatingBar.builder( ignoreGestures: state.isConsultantView || state.isDiaryFinished || state.isDiaryCommented, - initialRating: state.diary.meals[selectedIndex].mood.toDouble() + 1, + initialRating: (selectedIndex >= 0 && selectedIndex < state.diary.meals.length) + ? state.diary.meals[selectedIndex].mood.toDouble() + 1 + : 3, itemCount: 6, itemSize: MediaQuery.of(context).size.width / 6 - (124 / 6), updateOnDrag: true, @@ -63,20 +62,11 @@ class _MoodRatingWidgetState extends State { decoration: BoxDecoration( color: const Color(0xFFC82222), border: currentSelectedRating[selectedIndex] == 1 - ? Border.all( - width: 2, - color: const Color(0xFFAA3333), - strokeAlign: BorderSide.strokeAlignOutside, - ) + ? Border.all(width: 2, color: const Color(0xFFAA3333), strokeAlign: BorderSide.strokeAlignOutside) : null, borderRadius: currentSelectedRating[selectedIndex] == 1 - ? const BorderRadius.horizontal( - left: Radius.circular(8), - right: Radius.circular(2), - ) - : const BorderRadius.horizontal( - left: Radius.circular(8), - ), + ? const BorderRadius.horizontal(left: Radius.circular(8), right: Radius.circular(2)) + : const BorderRadius.horizontal(left: Radius.circular(8)), ), ); case 1: @@ -87,11 +77,7 @@ class _MoodRatingWidgetState extends State { decoration: BoxDecoration( color: const Color(0xFFFF3E3E), border: currentSelectedRating[selectedIndex] == 2 - ? Border.all( - width: 2, - color: const Color(0xFF550000), - strokeAlign: BorderSide.strokeAlignOutside, - ) + ? Border.all(width: 2, color: const Color(0xFF550000), strokeAlign: BorderSide.strokeAlignOutside) : null, borderRadius: currentSelectedRating[selectedIndex] == 2 ? BorderRadius.circular(2) : null, ), @@ -104,11 +90,7 @@ class _MoodRatingWidgetState extends State { decoration: BoxDecoration( color: const Color(0xFFF8C926), border: currentSelectedRating[selectedIndex] == 3 - ? Border.all( - width: 2, - color: const Color(0xFF8C5D00), - strokeAlign: BorderSide.strokeAlignOutside, - ) + ? Border.all(width: 2, color: const Color(0xFF8C5D00), strokeAlign: BorderSide.strokeAlignOutside) : null, borderRadius: currentSelectedRating[selectedIndex] == 3 ? BorderRadius.circular(2) : null, ), @@ -121,11 +103,7 @@ class _MoodRatingWidgetState extends State { decoration: BoxDecoration( color: const Color(0xFFFFF3BF), border: currentSelectedRating[selectedIndex] == 4 - ? Border.all( - width: 2, - color: const Color(0xFFB5911B), - strokeAlign: BorderSide.strokeAlignOutside, - ) + ? Border.all(width: 2, color: const Color(0xFFB5911B), strokeAlign: BorderSide.strokeAlignOutside) : null, borderRadius: currentSelectedRating[selectedIndex] == 4 ? BorderRadius.circular(2) : null, ), @@ -138,11 +116,7 @@ class _MoodRatingWidgetState extends State { decoration: BoxDecoration( color: const Color(0xFF62E833), border: currentSelectedRating[selectedIndex] == 5 - ? Border.all( - width: 2, - color: const Color(0xFF276918), - strokeAlign: BorderSide.strokeAlignOutside, - ) + ? Border.all(width: 2, color: const Color(0xFF276918), strokeAlign: BorderSide.strokeAlignOutside) : null, borderRadius: currentSelectedRating[selectedIndex] == 5 ? BorderRadius.circular(2) : null, ), @@ -155,20 +129,11 @@ class _MoodRatingWidgetState extends State { decoration: BoxDecoration( color: const Color(0xFF4DCE20), border: currentSelectedRating[selectedIndex] == 6 - ? Border.all( - width: 2, - color: const Color(0xFF184D12), - strokeAlign: BorderSide.strokeAlignOutside, - ) + ? Border.all(width: 2, color: const Color(0xFF184D12), strokeAlign: BorderSide.strokeAlignOutside) : null, borderRadius: currentSelectedRating[selectedIndex] == 6 - ? const BorderRadius.horizontal( - left: Radius.circular(2), - right: Radius.circular(8), - ) - : const BorderRadius.horizontal( - right: Radius.circular(8), - ), + ? const BorderRadius.horizontal(left: Radius.circular(2), right: Radius.circular(8)) + : const BorderRadius.horizontal(right: Radius.circular(8)), ), ); } @@ -177,20 +142,12 @@ class _MoodRatingWidgetState extends State { setState(() { currentSelectedRating[selectedIndex] = value; }); - EasyDebounce.debounce( - 'change-satiety', - const Duration(milliseconds: 250), - () { - context.read().fillMood(diaryId: state.diary.id, mood: currentSelectedRating[selectedIndex].toInt() - 1); - }, - ); + EasyDebounce.debounce('change-satiety', const Duration(milliseconds: 250), () { + context.read().fillMood(diaryId: state.diary.id, mood: currentSelectedRating[selectedIndex].toInt() - 1); + }); }, ), - SvgPicture.asset( - 'assets/icons/mood_2.svg', - height: 32, - width: 32, - ), + SvgPicture.asset('assets/icons/mood_2.svg', height: 32, width: 32), ], ), ], diff --git a/lib/features/diary/presentation/widgets/satiety_rating_widget.dart b/lib/features/diary/presentation/widgets/satiety_rating_widget.dart index d473f691..d36ac75b 100644 --- a/lib/features/diary/presentation/widgets/satiety_rating_widget.dart +++ b/lib/features/diary/presentation/widgets/satiety_rating_widget.dart @@ -23,9 +23,12 @@ class _SatietyRatingWidgetState extends State { builder: (BuildContext context, DiaryState state) { if (state is DiaryStateLoaded) { final selectedIndex = state.selectedMeal - 1; - if (isFirstFetch[selectedIndex]) { - currentSelectedRating[selectedIndex] = state.diary.meals[selectedIndex].satiety.toDouble() + 1; - isFirstFetch[selectedIndex] = false; + final meals = state.diary.meals; + if (selectedIndex >= 0 && selectedIndex < meals.length) { + if (isFirstFetch[selectedIndex]) { + currentSelectedRating[selectedIndex] = meals[selectedIndex].satiety.toDouble() + 1; + isFirstFetch[selectedIndex] = false; + } } return Padding( padding: const EdgeInsets.only(top: 24), @@ -35,21 +38,17 @@ class _SatietyRatingWidgetState extends State { Text( state.isConsultantView ? 'Сытость пользователя после приема пищи' : 'Ваша сытость после приёма пищи', textAlign: TextAlign.center, - style: const TextStyle( - fontSize: 16, - ), + style: const TextStyle(fontSize: 16), ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - SvgPicture.asset( - 'assets/icons/satiety_emoji_left.svg', - height: 32, - width: 32, - ), + SvgPicture.asset('assets/icons/satiety_emoji_left.svg', height: 32, width: 32), RatingBar.builder( ignoreGestures: state.isConsultantView || state.isDiaryFinished || state.isDiaryCommented, - initialRating: state.diary.meals[selectedIndex].satiety.toDouble() + 1, + initialRating: (selectedIndex >= 0 && selectedIndex < state.diary.meals.length) + ? state.diary.meals[selectedIndex].satiety.toDouble() + 1 + : 3, itemCount: 6, itemSize: MediaQuery.of(context).size.width / 6 - (124 / 6), updateOnDrag: true, @@ -63,20 +62,11 @@ class _SatietyRatingWidgetState extends State { decoration: BoxDecoration( color: const Color(0xFFC82222), border: currentSelectedRating[selectedIndex] == 1 - ? Border.all( - width: 2, - color: const Color(0xFFAA3333), - strokeAlign: BorderSide.strokeAlignOutside, - ) + ? Border.all(width: 2, color: const Color(0xFFAA3333), strokeAlign: BorderSide.strokeAlignOutside) : null, borderRadius: currentSelectedRating[selectedIndex] == 1 - ? const BorderRadius.horizontal( - left: Radius.circular(8), - right: Radius.circular(2), - ) - : const BorderRadius.horizontal( - left: Radius.circular(8), - ), + ? const BorderRadius.horizontal(left: Radius.circular(8), right: Radius.circular(2)) + : const BorderRadius.horizontal(left: Radius.circular(8)), ), ); case 1: @@ -87,11 +77,7 @@ class _SatietyRatingWidgetState extends State { decoration: BoxDecoration( color: const Color(0xFFFF3E3E), border: currentSelectedRating[selectedIndex] == 2 - ? Border.all( - width: 2, - color: const Color(0xFF550000), - strokeAlign: BorderSide.strokeAlignOutside, - ) + ? Border.all(width: 2, color: const Color(0xFF550000), strokeAlign: BorderSide.strokeAlignOutside) : null, borderRadius: currentSelectedRating[selectedIndex] == 2 ? BorderRadius.circular(2) : null, ), @@ -104,11 +90,7 @@ class _SatietyRatingWidgetState extends State { decoration: BoxDecoration( color: const Color(0xFFF8C926), border: currentSelectedRating[selectedIndex] == 3 - ? Border.all( - width: 2, - color: const Color(0xFF8C5D00), - strokeAlign: BorderSide.strokeAlignOutside, - ) + ? Border.all(width: 2, color: const Color(0xFF8C5D00), strokeAlign: BorderSide.strokeAlignOutside) : null, borderRadius: currentSelectedRating[selectedIndex] == 3 ? BorderRadius.circular(2) : null, ), @@ -121,11 +103,7 @@ class _SatietyRatingWidgetState extends State { decoration: BoxDecoration( color: const Color(0xFFFFF3BF), border: currentSelectedRating[selectedIndex] == 4 - ? Border.all( - width: 2, - color: const Color(0xFFB5911B), - strokeAlign: BorderSide.strokeAlignOutside, - ) + ? Border.all(width: 2, color: const Color(0xFFB5911B), strokeAlign: BorderSide.strokeAlignOutside) : null, borderRadius: currentSelectedRating[selectedIndex] == 4 ? BorderRadius.circular(2) : null, ), @@ -138,11 +116,7 @@ class _SatietyRatingWidgetState extends State { decoration: BoxDecoration( color: const Color(0xFF62E833), border: currentSelectedRating[selectedIndex] == 5 - ? Border.all( - width: 2, - color: const Color(0xFF276918), - strokeAlign: BorderSide.strokeAlignOutside, - ) + ? Border.all(width: 2, color: const Color(0xFF276918), strokeAlign: BorderSide.strokeAlignOutside) : null, borderRadius: currentSelectedRating[selectedIndex] == 5 ? BorderRadius.circular(2) : null, ), @@ -155,20 +129,11 @@ class _SatietyRatingWidgetState extends State { decoration: BoxDecoration( color: const Color(0xFF4DCE20), border: currentSelectedRating[selectedIndex] == 6 - ? Border.all( - width: 2, - color: const Color(0xFF184D12), - strokeAlign: BorderSide.strokeAlignOutside, - ) + ? Border.all(width: 2, color: const Color(0xFF184D12), strokeAlign: BorderSide.strokeAlignOutside) : null, borderRadius: currentSelectedRating[selectedIndex] == 6 - ? const BorderRadius.horizontal( - left: Radius.circular(2), - right: Radius.circular(8), - ) - : const BorderRadius.horizontal( - right: Radius.circular(8), - ), + ? const BorderRadius.horizontal(left: Radius.circular(2), right: Radius.circular(8)) + : const BorderRadius.horizontal(right: Radius.circular(8)), ), ); } @@ -178,20 +143,12 @@ class _SatietyRatingWidgetState extends State { setState(() { currentSelectedRating[selectedIndex] = value; }); - EasyDebounce.debounce( - 'change-satiety', - const Duration(milliseconds: 250), - () { - context.read().fillSatiety(diaryId: state.diary.id, satiety: currentSelectedRating[selectedIndex].toInt() - 1); - }, - ); + EasyDebounce.debounce('change-satiety', const Duration(milliseconds: 250), () { + context.read().fillSatiety(diaryId: state.diary.id, satiety: currentSelectedRating[selectedIndex].toInt() - 1); + }); }, ), - SvgPicture.asset( - 'assets/icons/satiety_emoji_right.svg', - height: 32, - width: 32, - ), + SvgPicture.asset('assets/icons/satiety_emoji_right.svg', height: 32, width: 32), ], ), ], diff --git a/lib/features/diary/repositories/diary_repository.dart b/lib/features/diary/repositories/diary_repository.dart index 0165ad7f..f3ff8bb5 100644 --- a/lib/features/diary/repositories/diary_repository.dart +++ b/lib/features/diary/repositories/diary_repository.dart @@ -82,7 +82,53 @@ class DiaryRepository { } Future getDiaryDay(int diaryId) async { - return (await diaryService.getDiaryDay(diary: diaryId)).diary; + // Align with getDiaryByDate: enrich portions with glossary nutritional info + final diaryWrapper = await diaryService.getDiaryDay(diary: diaryId); + final DiaryDayModel diaryDay = diaryWrapper.diary; + Set ids = {}; + for (var meal in diaryDay.meals) { + for (var portion in meal.portions) { + if (portion.productNutritionalValueId != null) { + ids.add(portion.productNutritionalValueId!); + } + } + } + if (ids.isNotEmpty) { + final glossary = await getGlossariesIndex(ids: ids.toList()); + final List updatedMeals = List.from(diaryDay.meals); + for (var mealIndex = 0; mealIndex < updatedMeals.length; mealIndex++) { + final meal = updatedMeals[mealIndex]; + final updatedPortions = meal.portions.map((portion) { + if (portion.productNutritionalValueId != null) { + try { + final glossaryItem = glossary.glossariesList.firstWhere((g) => g.id == portion.productNutritionalValueId); + return portion.copyWith( + productCaloriesInfo: ProductCaloriesInfoModel( + id: glossaryItem.id, + productName: glossaryItem.name, + calories: glossaryItem.energyOn100Gr, + proteins: glossaryItem.proteinsOn100Gr, + fats: glossaryItem.fatsOn100Gr, + carbohydrates: glossaryItem.carbohydratesOn100Gr, + energyOnPortion: glossaryItem.energyOnPortion, + proteinsOnPortion: glossaryItem.proteinsOnPortion, + fatsOnPortion: glossaryItem.fatsOnPortion, + carbohydratesOnPortion: glossaryItem.carbohydratesOnPortion, + ), + ); + } catch (e) { + return portion; + } + } + return portion; + }).toList(); + + updatedMeals[mealIndex] = meal.copyWith(portions: updatedPortions); + } + final updatedDiaryDay = diaryDay.copyWith(meals: updatedMeals); + return updatedDiaryDay; + } + return diaryDay; } Future getNutritionData(int index) async { diff --git a/lib/features/user_details/presentation/user_details_screen.dart b/lib/features/user_details/presentation/user_details_screen.dart index 4907e3dd..46e986f4 100644 --- a/lib/features/user_details/presentation/user_details_screen.dart +++ b/lib/features/user_details/presentation/user_details_screen.dart @@ -231,7 +231,7 @@ class _UserDetailsContentState extends State { children: [ Text(s.has_hypo, style: TextStyles.textSMedium.copyWith(color: appTheme.black)), const SizedBox(height: 4), - CustomTextFormField(readOnly: true, controller: TextEditingController(text: profileDetails.hasHypo.toString())), + CustomTextFormField(readOnly: true, controller: TextEditingController(text: profileDetails.hasHypo ? 'да' : 'нет')), ], ), ), @@ -242,7 +242,7 @@ class _UserDetailsContentState extends State { children: [ Text(s.has_hyper, style: TextStyles.textSMedium.copyWith(color: appTheme.black)), const SizedBox(height: 4), - CustomTextFormField(readOnly: true, controller: TextEditingController(text: profileDetails.hasHyper.toString())), + CustomTextFormField(readOnly: true, controller: TextEditingController(text: profileDetails.hasHyper ? 'да' : 'нет')), ], ), ), From 052c4fedca39a480f28a6fffd8b01ddc11123931 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Tue, 23 Sep 2025 23:47:38 +0300 Subject: [PATCH 25/28] feat(hms): Issue-660 --- android/build.gradle | 1 + lib/core/firebase_api.dart | 47 +++++++++++++++++++++++++++++++++++++- pubspec.yaml | 1 + 3 files changed, 48 insertions(+), 1 deletion(-) diff --git a/android/build.gradle b/android/build.gradle index d2ffbffa..9db63328 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -2,6 +2,7 @@ allprojects { repositories { google() mavenCentral() + maven { url 'https://developer.huawei.com/repo/' } } } diff --git a/lib/core/firebase_api.dart b/lib/core/firebase_api.dart index 572b56f6..2ba89290 100644 --- a/lib/core/firebase_api.dart +++ b/lib/core/firebase_api.dart @@ -6,13 +6,24 @@ import 'package:dio/dio.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/token_storage.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:huawei_push/huawei_push.dart'; class FirebaseApi { final _firebaseMessaging = FirebaseMessaging.instance; Future getToken() async { await _firebaseMessaging.requestPermission(); - return await _firebaseMessaging.getToken(); + + String? token = await _firebaseMessaging.getToken(); + + if (token == null && Platform.isAndroid) { + final isHuawei = await _isHuaweiDevice(); + if (isHuawei) { + token = await _getHuaweiPushToken(); + } + } + + return token; } Future registerPushToken() async { @@ -32,4 +43,38 @@ class FirebaseApi { } } } + + Future _isHuaweiDevice() async { + if (!Platform.isAndroid) return false; + try { + final android = await DeviceInfoPlugin().androidInfo; + final brand = android.brand.toLowerCase(); + final manufacturer = android.manufacturer.toLowerCase(); + return brand.contains('huawei') || manufacturer.contains('huawei') || brand.contains('honor') || manufacturer.contains('honor'); + } catch (_) { + return false; + } + } + + Future _getHuaweiPushToken() async { + try { + await Push.turnOnPush(); + + String? resolvedToken; + final sub = Push.getTokenStream.listen((t) => resolvedToken ??= t, onError: (_) {}); + + Push.getToken(''); + + final started = DateTime.now(); + while (resolvedToken == null && DateTime.now().difference(started).inSeconds < 5) { + await Future.delayed(const Duration(milliseconds: 100)); + } + + await sub.cancel(); + return resolvedToken; + } catch (e, st) { + log('Huawei Push token error: $e', stackTrace: st); + return null; + } + } } diff --git a/pubspec.yaml b/pubspec.yaml index 5fec1116..3f1e036e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -74,6 +74,7 @@ dependencies: flutter_dotenv: ^5.2.1 uuid: ^4.5.1 appmetrica_plugin: ^3.3.0 + huawei_push: ^6.11.0+300 dev_dependencies: build_runner: ^2.7.0 From fc09fa36b8ae48c78e1d88c7e39240040b797036 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Wed, 24 Sep 2025 23:47:11 +0300 Subject: [PATCH 26/28] feat(auth): Issue-691 --- android/app/build.gradle | 3 + .../auth/presentation/pages/auth_screen.dart | 94 +++++------ lib/features/auth/sdk/social_auth_facade.dart | 152 ++++++++++++++++-- pubspec.yaml | 2 + 4 files changed, 194 insertions(+), 57 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 9ffa87c4..457960f7 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -13,6 +13,8 @@ if (keystorePropertiesFile.exists()) { keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) } +def yandexClientId = project.hasProperty('YANDEX_CLIENT_ID') ? project.property('YANDEX_CLIENT_ID') : (System.getenv('YANDEX_CLIENT_ID') ?: "") + android { namespace = "com.eatvioapp.app" compileSdk = flutter.compileSdkVersion @@ -33,6 +35,7 @@ android { targetSdk = flutter.targetSdkVersion versionCode = flutter.versionCode versionName = flutter.versionName + manifestPlaceholders = [YANDEX_CLIENT_ID: yandexClientId] } signingConfigs { diff --git a/lib/features/auth/presentation/pages/auth_screen.dart b/lib/features/auth/presentation/pages/auth_screen.dart index ea457d6c..f9eeddf5 100644 --- a/lib/features/auth/presentation/pages/auth_screen.dart +++ b/lib/features/auth/presentation/pages/auth_screen.dart @@ -169,51 +169,51 @@ class _AuthScreenState extends State { ], ), // const SizedBox(height: 16), - // Row( - // mainAxisAlignment: MainAxisAlignment.spaceBetween, - // children: [ - // _SocialButton( - // onTap: _onVkPressed, - // backgroundColor: const Color(0xFF2787F5), - // tooltip: 'VK ID', - // child: SvgPicture.asset( - // 'assets/icons/vk.svg', - // height: 24, - // width: 24, - // colorFilter: const ColorFilter.mode(Colors.white, BlendMode.srcIn), - // ), - // ), - // _SocialButton( - // onTap: _onOkPressed, - // backgroundColor: const Color(0xFFF58220), - // tooltip: 'Одноклассники', - // child: SvgPicture.asset( - // 'assets/icons/ok.svg', - // height: 24, - // width: 24, - // colorFilter: const ColorFilter.mode(Colors.white, BlendMode.srcIn), - // ), - // ), - // const _SocialButton( - // onTap: _onMailRuPressed, - // backgroundColor: Color(0xFF005FF9), - // tooltip: 'Mail.ru', - // child: Text( - // 'Mail', - // style: TextStyle(color: Colors.white, fontWeight: FontWeight.w700), - // ), - // ), - // const _SocialButton( - // onTap: _onYandexPressed, - // backgroundColor: Color(0xFFFFCC00), - // tooltip: 'Яндекс ID', - // child: Text( - // 'Yandex', - // style: TextStyle(color: Colors.black, fontWeight: FontWeight.w700), - // ), - // ), - // ], - // ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _SocialButton( + onTap: () => _onVkPressed(context), + backgroundColor: const Color(0xFF2787F5), + tooltip: 'VK ID', + child: SvgPicture.asset( + 'assets/icons/vk.svg', + height: 24, + width: 24, + colorFilter: const ColorFilter.mode(Colors.white, BlendMode.srcIn), + ), + ), + _SocialButton( + onTap: _onOkPressed, + backgroundColor: const Color(0xFFF58220), + tooltip: 'Одноклассники', + child: SvgPicture.asset( + 'assets/icons/ok.svg', + height: 24, + width: 24, + colorFilter: const ColorFilter.mode(Colors.white, BlendMode.srcIn), + ), + ), + const _SocialButton( + onTap: _onMailRuPressed, + backgroundColor: Color(0xFF005FF9), + tooltip: 'Mail.ru', + child: Text( + 'Mail', + style: TextStyle(color: Colors.white, fontWeight: FontWeight.w700), + ), + ), + const _SocialButton( + onTap: _onYandexPressed, + backgroundColor: Color(0xFFFFCC00), + tooltip: 'Яндекс ID', + child: Text( + 'Yandex', + style: TextStyle(color: Colors.black, fontWeight: FontWeight.w700), + ), + ), + ], + ), const Spacer(), Center( child: RichText( @@ -395,8 +395,8 @@ class _SocialButton extends StatelessWidget { } // ignore: unused_element -void _onVkPressed() async { - final accessToken = await SocialAuthFacade.signInWithVk(); +void _onVkPressed(BuildContext context) async { + final accessToken = await SocialAuthFacade.signInWithVk(context: context); if (accessToken == null) return; final isAuthenticated = await sl().getTokenBySocial(provider: 'vk', accessToken: accessToken); if (isAuthenticated) { diff --git a/lib/features/auth/sdk/social_auth_facade.dart b/lib/features/auth/sdk/social_auth_facade.dart index 62985d5d..e5761498 100644 --- a/lib/features/auth/sdk/social_auth_facade.dart +++ b/lib/features/auth/sdk/social_auth_facade.dart @@ -1,25 +1,157 @@ import 'dart:async'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:flutter_login_yandex/flutter_login_yandex.dart'; +import 'package:eatvio_app/core/app_router.dart'; +import 'package:vk_id/vk_id.dart'; +import 'package:webview_flutter/webview_flutter.dart'; + +class VkAuthResult { + VkAuthResult({required this.accessToken, this.refreshToken, this.userId, this.firstName, this.lastName, this.email, this.phone, this.gender, this.birthDate}); + + final String accessToken; + final String? refreshToken; + final String? userId; + final String? firstName; + final String? lastName; + final String? email; + final String? phone; + final String? gender; + final String? birthDate; +} class SocialAuthFacade { - static Future signInWithVk() async { - // TODO: Integrate official VK ID SDK and return access token - return null; + static Future signInWithVk({required BuildContext context}) async { + final result = await signInWithVkAndFetchProfile(context: context); + return result?.accessToken; + } + + static Future signInWithVkAndFetchProfile({required BuildContext context}) async { + final clientIdStr = dotenv.maybeGet('VK_CLIENT_ID')?.trim() ?? Platform.environment['VK_CLIENT_ID']; + if (clientIdStr == null || clientIdStr.isEmpty) { + return null; + } + + try { + dynamic controller = VkIDController(clID: int.parse(clientIdStr)); + final authUri = controller.generateAuthorizeLinkWithCodeVerifier(); + final redirectScheme = 'vk$clientIdStr://'; + final redirectParams = await _presentOAuthWebView(context: context, initialUrl: authUri.toString(), redirectSchemePrefix: redirectScheme); + if (redirectParams == null) return null; + final code = redirectParams['code']; + final deviceId = redirectParams['device_id']; + if (code == null || deviceId == null) return null; + + final tokenResponse = await controller.exchangeAuthorizationCodeForToken(code: code, deviceId: deviceId); + final accessToken = _extractAccessToken(tokenResponse); + if (accessToken == null) return null; + return VkAuthResult(accessToken: accessToken); + } catch (_) { + return null; + } } static Future signInWithOk() async { - // TODO: Integrate official OK SDK and return access token - return null; + final context = AppRouter.navigatorKey.currentContext; + if (context == null) return null; + return _signInWithVkIdProvider(context: context, provider: 'ok'); } static Future signInWithMailRu() async { - // TODO: Integrate official Mail.ru SDK and return access token - return null; + final context = AppRouter.navigatorKey.currentContext; + if (context == null) return null; + return _signInWithVkIdProvider(context: context, provider: 'mail'); } static Future signInWithYandex() async { - // TODO: Integrate official Yandex ID SDK and return access token + final clientId = dotenv.maybeGet('YANDEX_CLIENT_ID')?.trim() ?? Platform.environment['YANDEX_CLIENT_ID']; + final redirectUri = dotenv.maybeGet('YANDEX_REDIRECT_URI')?.trim() ?? Platform.environment['YANDEX_REDIRECT_URI']; + + if (clientId == null || clientId.isEmpty || redirectUri == null || redirectUri.isEmpty) { + return null; + } + try { + final plugin = FlutterLoginYandex(); + final response = await plugin.signIn(); + final token = (response is Map) ? response['token'] as String? : null; + return (token != null && token.isNotEmpty) ? token : null; + } catch (_) { + return null; + } + } + + static Future _signInWithVkIdProvider({required BuildContext context, required String provider}) async { + final clientIdStr = dotenv.maybeGet('VK_CLIENT_ID')?.trim() ?? Platform.environment['VK_CLIENT_ID']; + if (clientIdStr == null || clientIdStr.isEmpty) { + return null; + } + try { + dynamic controller = VkIDController(clID: int.parse(clientIdStr)); + final authUri = controller.generateAuthorizeLinkWithCodeVerifier(provider: provider); + final redirectScheme = 'vk$clientIdStr://'; + final redirectParams = await _presentOAuthWebView(context: context, initialUrl: authUri.toString(), redirectSchemePrefix: redirectScheme); + if (redirectParams == null) return null; + final code = redirectParams['code']; + final deviceId = redirectParams['device_id']; + if (code == null || deviceId == null) return null; + final tokenResponse = await controller.exchangeAuthorizationCodeForToken(code: code, deviceId: deviceId, provider: provider); + final accessToken = _extractAccessToken(tokenResponse); + return accessToken; + } catch (_) { + return null; + } + } + + static String? _extractAccessToken(dynamic tokenResponse) { + if (tokenResponse == null) return null; + if (tokenResponse is Map) { + return (tokenResponse['access_token'] as String?) ?? (tokenResponse['token'] as String?); + } + try { + final token = (tokenResponse as dynamic).accessToken as String?; + if (token != null && token.isNotEmpty) return token; + } catch (_) {} + try { + final token = (tokenResponse as dynamic).token as String?; + if (token != null && token.isNotEmpty) return token; + } catch (_) {} return null; } + + static Future?> _presentOAuthWebView({ + required BuildContext context, + required String initialUrl, + required String redirectSchemePrefix, + }) async { + final controller = WebViewController() + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setNavigationDelegate( + NavigationDelegate( + onNavigationRequest: (request) { + final url = request.url; + if (url.startsWith(redirectSchemePrefix)) { + final uri = Uri.parse(url); + final params = {}..addAll(uri.queryParameters); + Navigator.of(context, rootNavigator: true).pop(params); + return NavigationDecision.prevent; + } + return NavigationDecision.navigate; + }, + ), + ) + ..loadRequest(Uri.parse(initialUrl)); + + return showDialog>( + context: context, + barrierDismissible: false, + builder: (_) { + return Dialog( + insetPadding: const EdgeInsets.all(16), + child: SizedBox(height: 560, child: WebViewWidget(controller: controller)), + ); + }, + ); + } } - - diff --git a/pubspec.yaml b/pubspec.yaml index 3f1e036e..a62a13af 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -75,6 +75,8 @@ dependencies: uuid: ^4.5.1 appmetrica_plugin: ^3.3.0 huawei_push: ^6.11.0+300 + vk_id: ^1.0.3 + flutter_login_yandex: ^0.0.5 dev_dependencies: build_runner: ^2.7.0 From a0b29f42df50de20d6ab863bcb2c5bbcdf532b4e Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Fri, 26 Sep 2025 10:31:34 +0300 Subject: [PATCH 27/28] feat(hp): Issue-660 --- android/app/agconnect-services.json | 1 + android/app/build.gradle | 17 ++++- android/app/proguard-rules.pro | 21 ++++++ android/app/src/main/AndroidManifest.xml | 10 +++ .../com/eatvioapp/app/HmsPushService.kt | 19 ++++++ .../kotlin/com/eatvioapp/app/MainActivity.kt | 65 +++++++++++++++++++ android/build.gradle | 39 +++++++++++ .../reports/problems/problems-report.html | 4 +- android/settings.gradle | 1 + lib/core/firebase_api.dart | 56 +++++++--------- pubspec.yaml | 1 - 11 files changed, 199 insertions(+), 35 deletions(-) create mode 100644 android/app/agconnect-services.json create mode 100644 android/app/proguard-rules.pro create mode 100644 android/app/src/main/kotlin/com/eatvioapp/app/HmsPushService.kt diff --git a/android/app/agconnect-services.json b/android/app/agconnect-services.json new file mode 100644 index 00000000..8ca899db --- /dev/null +++ b/android/app/agconnect-services.json @@ -0,0 +1 @@ +{"agcgw":{"url":"connect-drru.hispace.dbankcloud.ru","backurl":"connect-drru.hispace.dbankcloud.cn","websocketurl":"connect-ws-drru.hispace.dbankcloud.ru","websocketbackurl":"connect-ws-drru.hispace.dbankcloud.cn"},"agcgw_all":{"SG":"connect-dra.dbankcloud.cn","SG_back":"connect-dra.hispace.hicloud.com","CN":"connect-drcn.dbankcloud.cn","CN_back":"connect-drcn.hispace.hicloud.com","RU":"connect-drru.hispace.dbankcloud.ru","RU_back":"connect-drru.hispace.dbankcloud.cn","DE":"connect-dre.dbankcloud.cn","DE_back":"connect-dre.hispace.hicloud.com"},"websocketgw_all":{"SG":"connect-ws-dra.hispace.dbankcloud.cn","SG_back":"connect-ws-dra.hispace.dbankcloud.com","CN":"connect-ws-drcn.hispace.dbankcloud.cn","CN_back":"connect-ws-drcn.hispace.dbankcloud.com","RU":"connect-ws-drru.hispace.dbankcloud.ru","RU_back":"connect-ws-drru.hispace.dbankcloud.cn","DE":"connect-ws-dre.hispace.dbankcloud.cn","DE_back":"connect-ws-dre.hispace.dbankcloud.com"},"client":{"cp_id":"20007000036693451","product_id":"461323198429446894","client_id":"1595824543740483264","client_secret":"2A1727B0D31AA9BE1F5D9E8A5CDA1A67B9EF084AC9FB68D6C633A3CCB08ECDEE","project_id":"461323198429446894","app_id":"113238569","api_key":"DQEDAJNRITp9M3uc1rPHyT8PzLc7bnVcKJAeavZZk0TQNMx9ifvBebr+JOmZe9tEvjnLwv7c1hAHXmPGR/gWG86iABDrCVotq+NGnw==","package_name":"com.eatvioapp.app"},"oauth_client":{"client_id":"113238569","client_type":1},"app_info":{"app_id":"113238569","package_name":"com.eatvioapp.app"},"service":{"analytics":{"collector_url":"datacollector-drru.dt.dbankcloud.ru,datacollector-drru.dt.hicloud.com","collector_url_cn":"datacollector-drcn.dt.hicloud.com,datacollector-drcn.dt.dbankcloud.cn","collector_url_de":"datacollector-dre.dt.hicloud.com,datacollector-dre.dt.dbankcloud.cn","collector_url_ru":"datacollector-drru.dt.dbankcloud.ru,datacollector-drru.dt.hicloud.com","collector_url_sg":"datacollector-dra.dt.hicloud.com,datacollector-dra.dt.dbankcloud.cn","resource_id":"p1","channel_id":""},"ml":{"mlservice_url":"ml-api-drru.ai.dbankcloud.ru,ml-api-drru.ai.dbankcloud.cn"},"cloudstorage":{"storage_url":"https://agc-storage-drcn.platform.dbankcloud.cn","storage_url_ru":"https://agc-storage-drru.cloud.huawei.ru","storage_url_sg":"https://ops-dra.agcstorage.link","storage_url_de":"https://ops-dre.agcstorage.link","storage_url_cn":"https://agc-storage-drcn.platform.dbankcloud.cn","storage_url_ru_back":"https://agc-storage-drru.cloud.huawei.ru","storage_url_sg_back":"https://agc-storage-dra.cloud.huawei.asia","storage_url_de_back":"https://agc-storage-dre.cloud.huawei.eu","storage_url_cn_back":"https://agc-storage-drcn.cloud.huawei.com.cn"},"search":{"url":"https://search-drru.cloud.huawei.ru"},"edukit":{"edu_url":"edukit.cloud.huawei.com.cn","dh_url":"edukit.cloud.huawei.com.cn"}},"region":"RU","configuration_version":"3.0","appInfos":[{"package_name":"com.eatvioapp.app","client":{"app_id":"113238569"},"app_info":{"package_name":"com.eatvioapp.app","app_id":"113238569"},"oauth_client":{"client_type":1,"client_id":"113238569"}}]} \ No newline at end of file diff --git a/android/app/build.gradle b/android/app/build.gradle index 457960f7..15095cb5 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -5,6 +5,7 @@ plugins { // END: FlutterFire Configuration id "kotlin-android" id "dev.flutter.flutter-gradle-plugin" + id 'com.huawei.agconnect' } def keystoreProperties = new Properties() @@ -35,7 +36,10 @@ android { targetSdk = flutter.targetSdkVersion versionCode = flutter.versionCode versionName = flutter.versionName - manifestPlaceholders = [YANDEX_CLIENT_ID: yandexClientId] + manifestPlaceholders = [ + YANDEX_CLIENT_ID: yandexClientId, + applicationName: "io.flutter.app.FlutterApplication" + ] } signingConfigs { @@ -50,6 +54,9 @@ android { buildTypes { release { signingConfig signingConfigs.release + minifyEnabled true + shrinkResources true + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } debug { signingConfig signingConfigs.debug @@ -60,6 +67,14 @@ android { } } +dependencies { + implementation 'com.huawei.hms:push:6.13.0.300' + // Huawei Analytics dependency to satisfy com.huawei.hianalytics.* references during R8 + implementation 'com.huawei.hms:hianalytics:6.13.0.300' + // BouncyCastle provider required by Huawei secure utils used transitively by HMS + implementation 'org.bouncycastle:bcprov-jdk15to18:1.78.1' +} + flutter { source = "../.." } diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro new file mode 100644 index 00000000..b657fa16 --- /dev/null +++ b/android/app/proguard-rules.pro @@ -0,0 +1,21 @@ +-keep class com.huawei.** { *; } +-dontwarn com.huawei.** + +-keep class org.bouncycastle.** { *; } +-dontwarn org.bouncycastle.** + +-keep class io.flutter.** { *; } +-dontwarn io.flutter.** + +-keep class kotlinx.coroutines.** { *; } +-dontwarn kotlinx.coroutines.** + +-keep class **.R$* { *; } +-keepclassmembers class ** { @androidx.annotation.Keep *; } +-keep @androidx.annotation.Keep class * { *; } + +-keep public class * extends android.app.Application +-keep public class * extends android.content.ContentProvider + +-keep class * implements android.os.Parcelable { *; } +-keepclassmembers enum * { *; } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 0d239126..a99f3deb 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -11,6 +11,16 @@ android:usesCleartextTraffic="true" android:name="${applicationName}" android:icon="@mipmap/ic_launcher"> + + + + + + + when (call.method) { + "getHuaweiToken" -> { + CoroutineScope(Dispatchers.IO).launch { + try { + // Try to read appId from agconnect-services.json + var appId: String? = try { + AGConnectServicesConfig.fromContext(applicationContext) + .getString("client/app_id") + } catch (_: Exception) { + null + } + + // Fallback to manifest meta-data if needed + if (appId.isNullOrBlank()) { + try { + val pm: PackageManager = applicationContext.packageManager + val ai: ApplicationInfo = pm.getApplicationInfo(applicationContext.packageName, PackageManager.GET_META_DATA) + val raw = ai.metaData?.getString("com.huawei.hms.client.appid") + if (!raw.isNullOrBlank()) { + appId = raw.removePrefix("appid=") + } + } catch (_: Exception) { + // ignore and handle below + } + } + + if (appId.isNullOrBlank()) { + runOnUiThread { + result.error("HMS_TOKEN_ERROR", "Missing Huawei appId. Ensure agconnect-services.json or manifest meta-data is configured.", null) + } + return@launch + } + val token = HmsInstanceId.getInstance(applicationContext) + .getToken(appId, "HCM") + runOnUiThread { + result.success(token) + } + } catch (e: Exception) { + runOnUiThread { + result.error("HMS_TOKEN_ERROR", e.message, null) + } + } + } + } + else -> result.notImplemented() + } + } + } } diff --git a/android/build.gradle b/android/build.gradle index 9db63328..03774aff 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,3 +1,16 @@ +buildscript { + repositories { + google() + mavenCentral() + maven { url 'https://developer.huawei.com/repo/' } + gradlePluginPortal() + } + dependencies { + classpath 'com.android.tools.build:gradle:8.12.0' + classpath 'com.huawei.agconnect:agcp:1.9.1.301' + } +} + allprojects { repositories { google() @@ -14,6 +27,32 @@ subprojects { project.evaluationDependsOn(":app") } +// Temporary safeguard: set missing namespaces for Android library modules based on their manifest package +subprojects { Project subproject -> + subproject.pluginManager.withPlugin('com.android.library') { + def androidExt = subproject.extensions.findByName('android') + if (androidExt != null) { + def hasNamespaceProp = androidExt.hasProperty('namespace') + def currentNamespace = hasNamespaceProp ? androidExt.namespace : null + if (hasNamespaceProp && !currentNamespace) { + def manifestFile = file("${subproject.projectDir}/src/main/AndroidManifest.xml") + if (manifestFile.exists()) { + try { + def manifest = new groovy.xml.XmlSlurper().parse(manifestFile) + def manifestPackage = manifest.@package?.text() + if (manifestPackage) { + androidExt.namespace = manifestPackage + subproject.logger.lifecycle("[namespace-fix] Set namespace for ${subproject.path} to '${manifestPackage}' from AndroidManifest.xml") + } + } catch (Throwable t) { + subproject.logger.warn("[namespace-fix] Failed to read manifest for ${subproject.path}: ${t.message}") + } + } + } + } + } +} + tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/android/build/reports/problems/problems-report.html b/android/build/reports/problems/problems-report.html index 4921e6e4..ce07f9e3 100644 --- a/android/build/reports/problems/problems-report.html +++ b/android/build/reports/problems/problems-report.html @@ -650,12 +650,12 @@ code + .copy-button { diff --git a/android/settings.gradle b/android/settings.gradle index e173b2c9..76846d0d 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -12,6 +12,7 @@ pluginManagement { repositories { google() mavenCentral() + maven { url 'https://developer.huawei.com/repo/' } gradlePluginPortal() } } diff --git a/lib/core/firebase_api.dart b/lib/core/firebase_api.dart index 2ba89290..2d89bd43 100644 --- a/lib/core/firebase_api.dart +++ b/lib/core/firebase_api.dart @@ -1,28 +1,27 @@ import 'dart:developer'; import 'dart:io'; +import 'package:eatvio_app/core/utils/toasts.dart'; +import 'package:flutter/services.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:dio/dio.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/token_storage.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; -import 'package:huawei_push/huawei_push.dart'; +import 'package:flutter/services.dart' show MethodChannel, PlatformException; class FirebaseApi { final _firebaseMessaging = FirebaseMessaging.instance; Future getToken() async { - await _firebaseMessaging.requestPermission(); + String? token; - String? token = await _firebaseMessaging.getToken(); - - if (token == null && Platform.isAndroid) { - final isHuawei = await _isHuaweiDevice(); - if (isHuawei) { - token = await _getHuaweiPushToken(); - } + if (await _isHuaweiDevice()) { + token = await _getHuaweiPushTokenNative(); + } else { + await _firebaseMessaging.requestPermission(); + token = await _firebaseMessaging.getToken(); } - return token; } @@ -30,10 +29,11 @@ class FirebaseApi { if (await TokenStorage.haveTokenInStorage()) { try { final deviceInfo = DeviceInfoPlugin(); + final isHuawei = Platform.isAndroid ? await _isHuaweiDevice() : false; await sl().post( '/api/profile/notification/token/register', data: { - 'platform': Platform.isIOS ? 'ios' : 'android', + 'platform': Platform.isIOS ? 'ios' : (isHuawei ? 'huawei' : 'android'), 'hw_id': Platform.isIOS ? (await deviceInfo.iosInfo).identifierForVendor : (await deviceInfo.androidInfo).id, 'token': await getToken(), }, @@ -47,33 +47,27 @@ class FirebaseApi { Future _isHuaweiDevice() async { if (!Platform.isAndroid) return false; try { - final android = await DeviceInfoPlugin().androidInfo; - final brand = android.brand.toLowerCase(); - final manufacturer = android.manufacturer.toLowerCase(); - return brand.contains('huawei') || manufacturer.contains('huawei') || brand.contains('honor') || manufacturer.contains('honor'); + final info = await DeviceInfoPlugin().androidInfo; + final brand = info.brand.toLowerCase(); + final manufacturer = info.manufacturer.toLowerCase(); + return brand.contains('huawei') || brand.contains('honor') || manufacturer.contains('huawei') || manufacturer.contains('honor'); } catch (_) { return false; } } - Future _getHuaweiPushToken() async { + Future _getHuaweiPushTokenNative() async { try { - await Push.turnOnPush(); - - String? resolvedToken; - final sub = Push.getTokenStream.listen((t) => resolvedToken ??= t, onError: (_) {}); - - Push.getToken(''); - - final started = DateTime.now(); - while (resolvedToken == null && DateTime.now().difference(started).inSeconds < 5) { - await Future.delayed(const Duration(milliseconds: 100)); - } - - await sub.cancel(); - return resolvedToken; + const channel = MethodChannel('com.eatvioapp/hms_push'); + final String? token = await channel.invokeMethod('getHuaweiToken'); + return token; + } on PlatformException catch (e, st) { + showErrorToast('$e $st'); + log(e.toString(), stackTrace: st); + return null; } catch (e, st) { - log('Huawei Push token error: $e', stackTrace: st); + showErrorToast('$e $st'); + log(e.toString(), stackTrace: st); return null; } } diff --git a/pubspec.yaml b/pubspec.yaml index a62a13af..98a4429f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -74,7 +74,6 @@ dependencies: flutter_dotenv: ^5.2.1 uuid: ^4.5.1 appmetrica_plugin: ^3.3.0 - huawei_push: ^6.11.0+300 vk_id: ^1.0.3 flutter_login_yandex: ^0.0.5 From 46d0e31f5f974b09f957eaef545b39ead9ae6bc5 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Mon, 29 Sep 2025 13:29:39 +0300 Subject: [PATCH 28/28] fix(huawei-push): Issue-660 --- .gitignore | 4 +++- .../sessions/kotlin-compiler-10039299475187077818.salive | 0 android/app/build.gradle | 5 +++-- android/build.gradle | 4 ++-- 4 files changed, 8 insertions(+), 5 deletions(-) create mode 100644 android/.kotlin/sessions/kotlin-compiler-10039299475187077818.salive diff --git a/.gitignore b/.gitignore index 06e89a5e..afeb23f8 100644 --- a/.gitignore +++ b/.gitignore @@ -17,4 +17,6 @@ android/build/ ios/XCBuildData/ env/.env -.env \ No newline at end of file +.env +android/key.properties +app/upload-keystore.jks \ No newline at end of file diff --git a/android/.kotlin/sessions/kotlin-compiler-10039299475187077818.salive b/android/.kotlin/sessions/kotlin-compiler-10039299475187077818.salive new file mode 100644 index 00000000..e69de29b diff --git a/android/app/build.gradle b/android/app/build.gradle index 15095cb5..8fc22d6d 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -68,9 +68,10 @@ android { } dependencies { - implementation 'com.huawei.hms:push:6.13.0.300' + implementation 'com.huawei.agconnect:agconnect-core:1.9.1.301' + implementation 'com.huawei.hms:push:6.12.0.300' // Huawei Analytics dependency to satisfy com.huawei.hianalytics.* references during R8 - implementation 'com.huawei.hms:hianalytics:6.13.0.300' + implementation 'com.huawei.hms:hianalytics:6.12.0.300' // BouncyCastle provider required by Huawei secure utils used transitively by HMS implementation 'org.bouncycastle:bcprov-jdk15to18:1.78.1' } diff --git a/android/build.gradle b/android/build.gradle index 03774aff..af06c34c 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -2,7 +2,7 @@ buildscript { repositories { google() mavenCentral() - maven { url 'https://developer.huawei.com/repo/' } + maven {url 'https://developer.huawei.com/repo/'} gradlePluginPortal() } dependencies { @@ -15,7 +15,7 @@ allprojects { repositories { google() mavenCentral() - maven { url 'https://developer.huawei.com/repo/' } + maven {url 'https://developer.huawei.com/repo/'} } }