From c021cdf2c11a811fce08cd066df48541a196dfe2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B0=D1=82=D0=BE=D0=BB=D0=B8=D0=B9=20=D0=9F?= =?UTF-8?q?=D0=BE=D0=BB=D0=BE=D0=B7?= Date: Thu, 13 Mar 2025 14:13:19 +0000 Subject: [PATCH 01/78] Update .gitea/workflows/docker-run-tests.yml --- .gitea/workflows/docker-run-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitea/workflows/docker-run-tests.yml b/.gitea/workflows/docker-run-tests.yml index cf14247c..13609cf5 100644 --- a/.gitea/workflows/docker-run-tests.yml +++ b/.gitea/workflows/docker-run-tests.yml @@ -41,7 +41,7 @@ jobs: # - name: Build apk # run: flutter build apk - # some test + # some test 2 # - name: Rename and move apk # run: | From a9ad6aedd0f3a6812acda1a76e4dca5118e8d196 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B0=D1=82=D0=BE=D0=BB=D0=B8=D0=B9=20=D0=9F?= =?UTF-8?q?=D0=BE=D0=BB=D0=BE=D0=B7?= Date: Wed, 20 Aug 2025 11:41:21 +0000 Subject: [PATCH 02/78] Update README.md --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d7368d78..0e10b1e7 100644 --- a/README.md +++ b/README.md @@ -1 +1,4 @@ -# eatvio_app \ No newline at end of file +# eatvio_app + +ubuntu switch to hwe [kernel](https://canonical-kernel-docs.readthedocs-hosted.com/latest/reference/hwe-kernels/#installing-a-hwe-kernel) +ubuntu instal intel iRIS gpu [drivers](https://dgpu-docs.intel.com/driver/client/overview.html#ubuntu-latest) \ No newline at end of file From f94fd6f358e66ea8b653c887fb021c3883d35ff2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B0=D1=82=D0=BE=D0=BB=D0=B8=D0=B9=20=D0=9F?= =?UTF-8?q?=D0=BE=D0=BB=D0=BE=D0=B7?= Date: Wed, 20 Aug 2025 11:41:55 +0000 Subject: [PATCH 03/78] Update README.md --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 0e10b1e7..62c737fd 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,7 @@ # eatvio_app +--- ubuntu switch to hwe [kernel](https://canonical-kernel-docs.readthedocs-hosted.com/latest/reference/hwe-kernels/#installing-a-hwe-kernel) + +--- ubuntu instal intel iRIS gpu [drivers](https://dgpu-docs.intel.com/driver/client/overview.html#ubuntu-latest) \ No newline at end of file From 7b8a5d14d7b7d10c81f3d65ff3df2935e0ebce84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B0=D1=82=D0=BE=D0=BB=D0=B8=D0=B9=20=D0=9F?= =?UTF-8?q?=D0=BE=D0=BB=D0=BE=D0=B7?= Date: Wed, 20 Aug 2025 11:44:28 +0000 Subject: [PATCH 04/78] Update README.md --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 62c737fd..e1211a46 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ # eatvio_app + +--- +`apt install linux-oem-24.04` + --- ubuntu switch to hwe [kernel](https://canonical-kernel-docs.readthedocs-hosted.com/latest/reference/hwe-kernels/#installing-a-hwe-kernel) From 1b93f5c28723087de88a8ed299a5c861c81a2fe0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B0=D1=82=D0=BE=D0=BB=D0=B8=D0=B9=20=D0=9F?= =?UTF-8?q?=D0=BE=D0=BB=D0=BE=D0=B7?= Date: Wed, 20 Aug 2025 12:55:06 +0000 Subject: [PATCH 05/78] Update README.md --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e1211a46..cfe0a4e0 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,10 @@ --- -`apt install linux-oem-24.04` +```bash +# apt install linux-oem-24.04 +# sudo apt-get install --install-recommends linux-lowlatency-hwe-24.04 +``` --- ubuntu switch to hwe [kernel](https://canonical-kernel-docs.readthedocs-hosted.com/latest/reference/hwe-kernels/#installing-a-hwe-kernel) From bf6e23e96a6a4002bb9b4b5a285ace32d11a05a0 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Wed, 3 Sep 2025 11:34:41 +0300 Subject: [PATCH 06/78] 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 07/78] =?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 08/78] 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 09/78] 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 10/78] 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 11/78] 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 12/78] 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 13/78] 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 14/78] 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 15/78] 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 16/78] 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 17/78] 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 18/78] 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 19/78] 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 20/78] 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 21/78] 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 22/78] 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 23/78] 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 24/78] 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 25/78] 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 26/78] 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 27/78] 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 8d3ae151125ad3fcf1a307ca31bfdf96ea783e84 Mon Sep 17 00:00:00 2001 From: misha Date: Thu, 18 Sep 2025 18:47:07 +0300 Subject: [PATCH 28/78] feat(diary): added v2 methods for diary --- ios/Runner.xcodeproj/project.pbxproj | 3 + lib/core/utils/date_time_utils.dart | 11 +++ .../diary/entities/diary_day_entity.dart | 22 +++++ .../diary_future_diet_day_entity.dart | 18 ++++ .../diary/models/create_day_model.dart | 11 +++ .../diary/models/diary_day_model.dart | 55 +++++++---- lib/features/diary/models/get_day_model.dart | 11 +++ .../diary/presentation/bloc/diary_cubit.dart | 93 ++++++++++++------- .../diary/presentation/bloc/diary_state.dart | 15 ++- .../presentation/pages/diary_screen.dart | 80 +++++----------- .../widgets/finish_day_button_widget.dart | 4 +- .../widgets/photo_gallery_widget.dart | 4 +- .../diary/repositories/diary_repository.dart | 26 ++++-- .../diary/services/diary_service.dart | 9 +- .../presentation/pages/profile_screen.dart | 1 + lib/generated/l10n.dart | 7 +- 16 files changed, 237 insertions(+), 133 deletions(-) create mode 100644 lib/features/diary/entities/diary_day_entity.dart create mode 100644 lib/features/diary/entities/diary_future_diet_day_entity.dart create mode 100644 lib/features/diary/models/create_day_model.dart create mode 100644 lib/features/diary/models/get_day_model.dart 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/utils/date_time_utils.dart b/lib/core/utils/date_time_utils.dart index 701fa80e..9b3085c1 100644 --- a/lib/core/utils/date_time_utils.dart +++ b/lib/core/utils/date_time_utils.dart @@ -46,6 +46,17 @@ extension DateTimeExtension on DateTime { final now = DateTime.now(); return now.year == localDate.year && now.month == localDate.month && now.day == localDate.day; } + + String toISO8601WithTimeZoneOffset() { + final formatter = DateFormat("yyyy-MM-ddTHH:mm:ss"); + final formatted = formatter.format(this); + final offset = timeZoneOffset; + final hours = offset.inHours.abs().toString().padLeft(2, '0'); + final minutes = (offset.inMinutes.abs() % 60).toString().padLeft(2, '0'); + final sign = offset.isNegative ? '-' : '+'; + + return "$formatted$sign$hours:$minutes"; + } } String calculateDifference(DateTime dateTime, [String? format]) { diff --git a/lib/features/diary/entities/diary_day_entity.dart b/lib/features/diary/entities/diary_day_entity.dart new file mode 100644 index 00000000..65f45570 --- /dev/null +++ b/lib/features/diary/entities/diary_day_entity.dart @@ -0,0 +1,22 @@ +import 'package:eatvio_app/features/diary/models/diary_day_model.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'diary_day_entity.freezed.dart'; + +@freezed +abstract class DiaryDayEntity with _$DiaryDayEntity { + factory DiaryDayEntity({ + @JsonKey(name: "id") required int id, + @JsonKey(name: "date") required String date, + @JsonKey(name: "wakeup") String? wakeup, + @JsonKey(name: "retire") String? retire, + @JsonKey(name: "closed") required int closed, + @JsonKey(name: "comment") required String comment, + @JsonKey(name: "state") required TypeOfDiaryModel typeOfDiary, + @JsonKey(name: "meals") required List meals, + @JsonKey(name: "chosen_plan_id") required int? chosenPlanId, + @JsonKey(name: "day_of_chosen_diet") required int? dayOfChosenDiet, + @JsonKey(name: "chosen_diet", fromJson: parseDiet) required DiaryDietModel? diet, + }) = _DiaryDayEntity; + +} diff --git a/lib/features/diary/entities/diary_future_diet_day_entity.dart b/lib/features/diary/entities/diary_future_diet_day_entity.dart new file mode 100644 index 00000000..4247cecf --- /dev/null +++ b/lib/features/diary/entities/diary_future_diet_day_entity.dart @@ -0,0 +1,18 @@ +import 'package:eatvio_app/features/diary/models/diary_day_model.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'diary_future_diet_day_entity.freezed.dart'; + + + +@freezed +abstract class DiaryFutureDietDayEntity with _$DiaryFutureDietDayEntity { + const factory DiaryFutureDietDayEntity({ + required String date, + required TypeOfDiaryModel typeOfDiary, + required List meals, + required DiaryDietModel? diet, + required int? dayOfChosenDiet, + }) = _DiaryFutureDietDayEntity; + +} diff --git a/lib/features/diary/models/create_day_model.dart b/lib/features/diary/models/create_day_model.dart new file mode 100644 index 00000000..9d3eabbd --- /dev/null +++ b/lib/features/diary/models/create_day_model.dart @@ -0,0 +1,11 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'create_day_model.freezed.dart'; +part 'create_day_model.g.dart'; + +@freezed +abstract class CreateDayModel with _$CreateDayModel { + factory CreateDayModel({required String day}) = _CreateDayModel; + + factory CreateDayModel.fromJson(Map json) => _$CreateDayModelFromJson(json); +} diff --git a/lib/features/diary/models/diary_day_model.dart b/lib/features/diary/models/diary_day_model.dart index e1ad4572..19099ded 100644 --- a/lib/features/diary/models/diary_day_model.dart +++ b/lib/features/diary/models/diary_day_model.dart @@ -1,4 +1,6 @@ import 'package:eatvio_app/features/consultations/models/consultation_model.dart'; +import 'package:eatvio_app/features/diary/entities/diary_day_entity.dart'; +import 'package:eatvio_app/features/diary/entities/diary_future_diet_day_entity.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; part 'diary_day_model.freezed.dart'; @@ -30,7 +32,7 @@ abstract class DiaryDayWrapperModel with _$DiaryDayWrapperModel { @freezed abstract class DiaryDayModel with _$DiaryDayModel { const factory DiaryDayModel({ - @JsonKey(name: "id") required int id, + @JsonKey(name: "id") required int? id, @JsonKey(name: "date") required String date, @JsonKey(name: "wakeup") String? wakeup, @JsonKey(name: "retire") String? retire, @@ -40,26 +42,43 @@ abstract class DiaryDayModel with _$DiaryDayModel { @JsonKey(name: "meals") required List meals, @JsonKey(name: "chosen_plan_id") required int? chosenPlanId, @JsonKey(name: "day_of_chosen_diet") required int? dayOfChosenDiet, - @JsonKey(name: "chosen_diet", fromJson: _parseDiet) required DiaryDietModel? diet, + @JsonKey(name: "chosen_diet", fromJson: parseDiet) required DiaryDietModel? diet, }) = _DiaryDayModel; - + const DiaryDayModel._(); factory DiaryDayModel.fromJson(Map json) => _$DiaryDayModelFromJson(json); + + DiaryDayEntity? toEntity() { + if (id == null) { + return null; + } + return DiaryDayEntity( + id: id!, + date: date, + closed: closed, + comment: comment, + typeOfDiary: typeOfDiary, + meals: meals, + chosenPlanId: chosenPlanId, + dayOfChosenDiet: dayOfChosenDiet, + diet: diet, + ); + } + + DiaryFutureDietDayEntity toDiaryFutureDayEntity() { + + return DiaryFutureDietDayEntity( + date: date, + typeOfDiary: typeOfDiary, + meals: meals, + dayOfChosenDiet: dayOfChosenDiet, + diet: diet, + ); + } } -@freezed -abstract class DiaryFutureDietDayModel with _$DiaryFutureDietDayModel { - const factory DiaryFutureDietDayModel({ - @JsonKey(name: "date") required String date, - @JsonKey(name: "state", fromJson: _parseDayTypeForFutureDay) required TypeOfDiaryModel typeOfDiary, - @JsonKey(name: "meals") required List meals, - @JsonKey(name: "chosen_diet", fromJson: _parseDiet) required DiaryDietModel? diet, - @JsonKey(name: "day_of_chosen_diet") required int? dayOfChosenDiet, - }) = _DiaryFutureDietDayModel; - factory DiaryFutureDietDayModel.fromJson(Map json) => _$DiaryFutureDietDayModelFromJson(json); -} -DiaryDietModel? _parseDiet(dynamic json) { +DiaryDietModel? parseDiet(dynamic json) { if (json == null) return null; return DiaryDietModel.fromJson(json['diet']); } @@ -71,10 +90,6 @@ abstract class DiaryDietModel with _$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); -} - @freezed abstract class MealModel with _$MealModel { const factory MealModel({ @@ -141,7 +156,7 @@ abstract class TypeOfDiaryModel with _$TypeOfDiaryModel { const factory TypeOfDiaryModel({ @JsonKey(name: "has_chosen_diet") required bool hasChosenDiet, @JsonKey(name: "has_chosen_plan") required bool hasChosenPlan, - @JsonKey(name: "last_state") required DiaryStateEnum diaryState, + @JsonKey(name: "last_state") required DiaryStateEnum? diaryState, }) = _TypeOfDiaryModel; factory TypeOfDiaryModel.fromJson(Map json) => _$TypeOfDiaryModelFromJson(json); diff --git a/lib/features/diary/models/get_day_model.dart b/lib/features/diary/models/get_day_model.dart new file mode 100644 index 00000000..bf9d285d --- /dev/null +++ b/lib/features/diary/models/get_day_model.dart @@ -0,0 +1,11 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'get_day_model.freezed.dart'; +part 'get_day_model.g.dart'; + +@freezed +abstract class GetDayModel with _$GetDayModel { + factory GetDayModel({required String day}) = _GetDayModel; + + factory GetDayModel.fromJson(Map json) => _$GetDayModelFromJson(json); +} diff --git a/lib/features/diary/presentation/bloc/diary_cubit.dart b/lib/features/diary/presentation/bloc/diary_cubit.dart index 6e6f8891..644837ac 100644 --- a/lib/features/diary/presentation/bloc/diary_cubit.dart +++ b/lib/features/diary/presentation/bloc/diary_cubit.dart @@ -1,10 +1,13 @@ import 'dart:developer'; import 'dart:io'; +import 'package:dio/dio.dart'; import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart'; import 'package:eatvio_app/core/utils/date_time_utils.dart'; import 'package:eatvio_app/features/chat/repositories/chat_repository.dart'; import 'package:eatvio_app/features/consultations/models/consultation_model.dart'; +import 'package:eatvio_app/features/diary/entities/diary_day_entity.dart'; +import 'package:eatvio_app/features/diary/entities/diary_future_diet_day_entity.dart'; import 'package:eatvio_app/features/diary/models/consultant_review_model.dart'; import 'package:eatvio_app/features/diary/models/diary_day_model.dart'; import 'package:eatvio_app/features/diary/models/nutrition_model.dart'; @@ -39,20 +42,50 @@ class DiaryCubit extends Cubit { today = DateTime.now(); selectedDay = today; diaryRepository.forceFetchStream.listen((_) { - fetchDiary(true, diaryId); + fetchDiaryV2(needLoading: true, diaryId: diaryId); }); } String comment = ''; - Future setSelectedDay(DateTime day, {bool? forDietOnly}) async { + Future setSelectedDay(DateTime day, {bool? forDietOnly}) async { selectedDay = day; if (forDietOnly ?? false) { - final result = await fetchDiaryForDietOnly(); + final result = await fetchDiaryV2(); return result; } - await fetchDiary(true); - return null; + await fetchDiaryV2(needLoading: true); + return true; + } + + Future fetchDiaryV2({bool needLoading = false, int? diaryId, int? currentDay}) async { + final (diaryDay, error) = await qwe(); + if (error != null) { + if (error == DiaryFetchingErrors.canNotCreateFutureDays) { + return false; + } + emit(DiaryState.error(errorMessage: error.toString())); + } + + if (diaryDay != null) { + final diaryEntity = diaryDay.toEntity(); + if (diaryEntity != null) { + setDiary(diaryDay: diaryEntity); + return true; + } + setDiaryForDietOnly(diaryDay.toDiaryFutureDayEntity()); + } + return true; + } + + Future<(DiaryDayModel?, DiaryFetchingErrors?)> qwe() async { + try { + final diaryDay = await diaryRepository.getDiaryDayV2(day: selectedDay.toISO8601WithTimeZoneOffset()); + } on DioException catch (e) { + + } + + return (null, null); } void setSelectedMeal(int meal) { @@ -65,9 +98,7 @@ class DiaryCubit extends Cubit { } } - Future fetchDiaryForDietOnly() async { - final stringDate = selectedDay.format('yyyy-MM-dd'); - final diaryDay = await diaryRepository.getDiaryByDateForDietOnly(stringDate); + Future setDiaryForDietOnly(DiaryFutureDietDayEntity diaryDay) async { if (!diaryDay.typeOfDiary.hasChosenDiet) { return false; } @@ -146,13 +177,9 @@ class DiaryCubit extends Cubit { return false; } - Future fetchDiary([bool needLoading = false, int? diaryId, int? currentDay]) async { + Future setDiary({int? diaryId, int? currentDay, required DiaryDayEntity diaryDay}) async { this.diaryId ??= diaryId; final isConsultantView = this.diaryId != null; - if (needLoading) { - emit(const DiaryState.loading()); - selectedMeal = 1; - } try { final DiaryDayModel diaryDay; bool isMoodEnabled = false; @@ -174,14 +201,14 @@ class DiaryCubit extends Cubit { final chosenPlanModel = await chatRepository.getChosenPlan(id: diaryDay.chosenPlanId!); if (!isConsultantView && (diaryDay.typeOfDiary.diaryState == DiaryStateEnum.lastDayCommented || diaryDay.typeOfDiary.diaryState == DiaryStateEnum.commented)) { - await diaryRepository.viewCommentOfDiaryDayByConsultant(diaryId: diaryDay.id); + await diaryRepository.viewCommentOfDiaryDayByConsultant(diaryId: diaryDay.id!); } - consultantReview = await diaryRepository.getDiaryComments(planId: diaryDay.chosenPlanId!, diaryId: diaryDay.id); + consultantReview = await diaryRepository.getDiaryComments(planId: diaryDay.chosenPlanId!, diaryId: diaryDay.id!); planDuration = chosenPlanModel.plan.duration; planName = chosenPlanModel.plan.name; currentPlanDay = consultantReview.currentPlanDay; } - final nutrition = await getNutrition(diaryDay.id); + final nutrition = await getNutrition(diaryDay.id!); final isDiaryFinished = diaryDay.typeOfDiary.diaryState == DiaryStateEnum.finished || diaryDay.typeOfDiary.diaryState == DiaryStateEnum.lastDayFinished; @@ -191,7 +218,7 @@ class DiaryCubit extends Cubit { diaryDay.typeOfDiary.diaryState == DiaryStateEnum.reviewedByUser; emit( DiaryState.loaded( - diary: diaryDay, + diary: diaryDay.toEntity()!, nutrition: nutrition, selectedDay: selectedDay, selectedMeal: selectedMeal, @@ -214,7 +241,7 @@ class DiaryCubit extends Cubit { Future finishDiaryDay({required int diaryId, required String wakeUp, required String retire}) async { await diaryRepository.finishDiaryDay(diaryId: diaryId, wakeUp: wakeUp, retire: retire, comment: comment.isEmpty ? null : comment); - await fetchDiary(); + await fetchDiaryV2(); } Future getNutrition(int index) async { @@ -238,7 +265,7 @@ class DiaryCubit extends Cubit { quantity: quantity, productNutritionalValueId: productNutritionalValueId, ); - await fetchDiary(); + await fetchDiaryV2(); } Future updateProductInMeal({ @@ -272,27 +299,27 @@ class DiaryCubit extends Cubit { portionState: portionState, ); } - await fetchDiary(); + await fetchDiaryV2(); } Future markEaten({required int diaryId, required int mealId, required int portionId}) async { await diaryRepository.markEaten(diaryId: diaryId, mealId: mealId, portionId: portionId); - await fetchDiary(); + await fetchDiaryV2(); } Future unmarkEaten({required int diaryId, required int mealId, required int portionId}) async { await diaryRepository.unmarkEaten(diaryId: diaryId, mealId: mealId, portionId: portionId); - await fetchDiary(); + await fetchDiaryV2(); } Future restore({required int diaryId, required int mealId, required int portionId}) async { await diaryRepository.restore(diaryId: diaryId, mealId: mealId, portionId: portionId); - await fetchDiary(); + await fetchDiaryV2(); } Future deleteProductFromMeal({required int diaryId, required int mealId, required int portionId}) async { await diaryRepository.deleteProductFromMeal(diaryId: diaryId, mealId: mealId, portionId: portionId); - await fetchDiary(); + await fetchDiaryV2(); } Future> getGlossaryListByQuery(String query) async { @@ -301,12 +328,12 @@ class DiaryCubit extends Cubit { Future uploadPhotos({required List files, required int diaryId}) async { await diaryRepository.uploadPhotos(files: await ImageCompressUtility.compressFilesList(files), diaryId: diaryId, mealId: selectedMeal); - await fetchDiary(); + await fetchDiaryV2(); } Future deletePhoto({required DiaryPhotoModel photo, required int diaryId}) async { await diaryRepository.deletePhoto(photo: photo, diaryId: diaryId, mealId: selectedMeal); - await fetchDiary(); + await fetchDiaryV2(); } Future sendProductToModeration({ @@ -326,30 +353,34 @@ class DiaryCubit extends Cubit { Future fillTime({required int diaryId, required String time}) async { await diaryRepository.fillTime(diaryId: diaryId, mealId: selectedMeal, time: time); - await fetchDiary(); + await fetchDiaryV2(); } Future fillWakeup({required int diaryId, required String time}) async { await diaryRepository.fillWakeup(diaryId: diaryId, time: time); - await fetchDiary(); + await fetchDiaryV2(); } Future fillRetire({required int diaryId, required String time}) async { await diaryRepository.fillRetire(diaryId: diaryId, time: time); - await fetchDiary(); + await fetchDiaryV2(); } Future fillSatiety({required int diaryId, required int satiety}) async { await diaryRepository.fillSatiety(diaryId: diaryId, mealId: selectedMeal, satiety: satiety); - await fetchDiary(); + await fetchDiaryV2(); } Future fillMood({required int diaryId, required int mood}) async { await diaryRepository.fillMood(diaryId: diaryId, mealId: selectedMeal, mood: mood); - await fetchDiary(); + await fetchDiaryV2(); } void fillComment(String comment) { this.comment = comment; } + + } + +enum DiaryFetchingErrors { canNotCreateFutureDays, dayAlreadyExists } diff --git a/lib/features/diary/presentation/bloc/diary_state.dart b/lib/features/diary/presentation/bloc/diary_state.dart index 4779e2ea..771ebb31 100644 --- a/lib/features/diary/presentation/bloc/diary_state.dart +++ b/lib/features/diary/presentation/bloc/diary_state.dart @@ -1,3 +1,5 @@ +import 'package:eatvio_app/features/diary/entities/diary_day_entity.dart'; +import 'package:eatvio_app/features/diary/entities/diary_future_diet_day_entity.dart'; import 'package:eatvio_app/features/diary/models/consultant_review_model.dart'; import 'package:eatvio_app/features/diary/models/diary_day_model.dart'; import 'package:eatvio_app/features/diary/models/nutrition_model.dart'; @@ -11,12 +13,10 @@ abstract class DiaryState with _$DiaryState { const factory DiaryState.loading() = DiaryStateLoading; - const factory DiaryState.error({ - required String errorMessage, - }) = DiaryStateError; + const factory DiaryState.error({required String errorMessage}) = DiaryStateError; const factory DiaryState.loaded({ - required DiaryDayModel diary, + required DiaryDayEntity diary, required NutritionDataModel nutrition, required DateTime selectedDay, required int selectedMeal, @@ -32,9 +32,6 @@ abstract class DiaryState with _$DiaryState { @Default(false) bool isLoadingAddingProductInMeal, }) = DiaryStateLoaded; - const factory DiaryState.loadedTomorrowWithDiet({ - required DiaryFutureDietDayModel diary, - required DateTime selectedDay, - required int selectedMeal, - }) = DiaryStateLoadedTomorrowWithDiet; + const factory DiaryState.loadedTomorrowWithDiet({required DiaryFutureDietDayEntity diary, required DateTime selectedDay, required int selectedMeal}) = + DiaryStateLoadedTomorrowWithDiet; } diff --git a/lib/features/diary/presentation/pages/diary_screen.dart b/lib/features/diary/presentation/pages/diary_screen.dart index fb1987ef..4ec74e16 100644 --- a/lib/features/diary/presentation/pages/diary_screen.dart +++ b/lib/features/diary/presentation/pages/diary_screen.dart @@ -35,19 +35,12 @@ class DiaryScreen extends StatelessWidget { class UserDiaryScreen extends StatelessWidget { static const path = '/user-diary-screen'; static const name = 'userDiaryScreen'; - const UserDiaryScreen({ - super.key, - required this.diaryId, - required this.currentPlanDay, - }); + const UserDiaryScreen({super.key, required this.diaryId, required this.currentPlanDay}); final int diaryId; final int currentPlanDay; @override Widget build(BuildContext context) { - return DiaryScreenContent( - diaryId: diaryId, - currentPlanDay: currentPlanDay, - ); + return DiaryScreenContent(diaryId: diaryId, currentPlanDay: currentPlanDay); } } @@ -60,12 +53,8 @@ class DiaryScreenContent extends StatelessWidget { final s = S.of(context); return MultiBlocProvider( providers: [ - BlocProvider( - create: (BuildContext context) => sl()..fetchDiary(true, diaryId, currentPlanDay), - ), - BlocProvider( - create: (context) => sl(), - ), + BlocProvider(create: (BuildContext context) => sl()..fetchDiaryV2(needLoading: true, diaryId: diaryId, currentDay: currentPlanDay)), + BlocProvider(create: (context) => sl()), ], child: BlocBuilder( builder: (BuildContext context, DiaryState state) { @@ -73,17 +62,13 @@ class DiaryScreenContent extends StatelessWidget { log(state.diary.id.toString()); } if (state is DiaryStateError) { - return Scaffold( - body: DataErrorWidget(onReload: () => context.read().fetchDiary(false, diaryId)), - ); + return Scaffold(body: DataErrorWidget(onReload: () => context.read().fetchDiaryV2(needLoading: false, diaryId: diaryId))); } return Scaffold( backgroundColor: Colors.grey.shade200, appBar: diaryId == null - ? AppBar( - toolbarHeight: 0, - ) + ? AppBar(toolbarHeight: 0) : AppBar( title: Text( state is DiaryStateLoaded ? DateTime.parse(state.diary.date).toLocal().format('d MMMM') : '', @@ -97,12 +82,7 @@ class DiaryScreenContent extends StatelessWidget { child: Column( children: [ if (diaryId == null) const CalendarWidget(), - if (state is DiaryStateLoading) ...[ - const SizedBox( - height: 60, - ), - const Loader() - ], + if (state is DiaryStateLoading) ...[const SizedBox(height: 60), const Loader()], if (state is DiaryStateLoaded) WakeupAndRetireWidget( isEnabled: state.diary.typeOfDiary.hasChosenPlan || state.isConsultantView || state.isWakeUpAndRetireEnabled, @@ -114,40 +94,31 @@ class DiaryScreenContent extends StatelessWidget { showMood: state.diary.typeOfDiary.hasChosenPlan || state.isConsultantView || state.isMoodEnabled, showAddButton: !state.isConsultantView && !state.isDiaryFinished && !state.isDiaryCommented, ), - if (state is DiaryStateLoadedTomorrowWithDiet) - const MealWidget( - showMood: false, - showAddButton: false, - ), + if (state is DiaryStateLoadedTomorrowWithDiet) const MealWidget(showMood: false, showAddButton: false), if (state is DiaryStateLoaded) const CaloriesChartWidget(), const SizedBox(height: 12), - Builder(builder: (context) { - if (state is DiaryStateLoaded && state.diary.typeOfDiary.hasChosenPlan) { - if (state.diary.comment.isNotEmpty) { - return CommentForConsultantWidget( - comment: state.diary.comment, - ); + Builder( + builder: (context) { + if (state is DiaryStateLoaded && state.diary.typeOfDiary.hasChosenPlan) { + if (state.diary.comment.isNotEmpty) { + return CommentForConsultantWidget(comment: state.diary.comment); + } + if (state.isDiaryCommented || state.isDiaryFinished) { + return const SizedBox.shrink(); + } + return const CommentForConsultantWidget(); } - if (state.isDiaryCommented || state.isDiaryFinished) { - return const SizedBox.shrink(); - } - return const CommentForConsultantWidget(); - } - return const SizedBox.shrink(); - }), + return const SizedBox.shrink(); + }, + ), if (state is DiaryStateLoaded && !state.isConsultantView && !state.isDiaryFinished && !state.isDiaryCommented) Padding( padding: const EdgeInsets.only(top: 12), - child: FinishDayButtonWidget( - diaryDay: state.diary, - ), + child: FinishDayButtonWidget(diaryDay: state.diary), ), if (state is DiaryStateLoaded && state.isConsultantView && !state.isDiaryCommented) ...[ const SizedBox(height: 16), - const MainBackgroundBouble( - padding: EdgeInsets.all(16), - child: DayCommentWidget(), - ) + const MainBackgroundBouble(padding: EdgeInsets.all(16), child: DayCommentWidget()), ], if (state is DiaryStateLoaded && state.consultantReview?.consultantComments.dayComment != null) ...[ const SizedBox(height: 12), @@ -155,10 +126,7 @@ class DiaryScreenContent extends StatelessWidget { padding: const EdgeInsets.all(16), child: Column( children: [ - Text( - s.dayComment, - style: TextStyles.textLSemibold.copyWith(color: appTheme.black), - ), + Text(s.dayComment, style: TextStyles.textLSemibold.copyWith(color: appTheme.black)), const SizedBox(height: 12), CommentContainer(comment: state.consultantReview?.consultantComments.dayComment ?? ''), ], diff --git a/lib/features/diary/presentation/widgets/finish_day_button_widget.dart b/lib/features/diary/presentation/widgets/finish_day_button_widget.dart index 042c418c..b3f170ec 100644 --- a/lib/features/diary/presentation/widgets/finish_day_button_widget.dart +++ b/lib/features/diary/presentation/widgets/finish_day_button_widget.dart @@ -1,11 +1,11 @@ -import 'package:eatvio_app/features/diary/models/diary_day_model.dart'; +import 'package:eatvio_app/features/diary/entities/diary_day_entity.dart'; import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class FinishDayButtonWidget extends StatefulWidget { - final DiaryDayModel diaryDay; + final DiaryDayEntity diaryDay; const FinishDayButtonWidget({super.key, required this.diaryDay}); diff --git a/lib/features/diary/presentation/widgets/photo_gallery_widget.dart b/lib/features/diary/presentation/widgets/photo_gallery_widget.dart index 352b6519..a058ba61 100644 --- a/lib/features/diary/presentation/widgets/photo_gallery_widget.dart +++ b/lib/features/diary/presentation/widgets/photo_gallery_widget.dart @@ -5,7 +5,7 @@ import 'package:eatvio_app/core/app_router.dart'; import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/permission_handler/permission_handler.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/entities/diary_day_entity.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/generated/l10n.dart'; @@ -34,7 +34,7 @@ class _PhotoGalleryWidgetState extends State { diaryCubit = context.read(); } - void showImagePicker(DiaryDayModel diaryDay) { + void showImagePicker(DiaryDayEntity diaryDay) { showCupertinoModalPopup( context: context, builder: (context) { diff --git a/lib/features/diary/repositories/diary_repository.dart b/lib/features/diary/repositories/diary_repository.dart index 0165ad7f..d385ed75 100644 --- a/lib/features/diary/repositories/diary_repository.dart +++ b/lib/features/diary/repositories/diary_repository.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:io'; import 'package:eatvio_app/features/diary/models/consultant_comment_model.dart'; import 'package:eatvio_app/features/diary/models/consultant_review_model.dart'; +import 'package:eatvio_app/features/diary/models/create_day_model.dart'; import 'package:eatvio_app/features/diary/models/diary_day_model.dart'; import 'package:eatvio_app/features/diary/models/finish_diary_day_model.dart'; import 'package:eatvio_app/features/diary/models/nutrition_model.dart'; @@ -69,14 +70,6 @@ class DiaryRepository { } } - Future getDiaryByDateForDietOnly(String date) async { - try { - return (await diaryService.getDiaryIndexByDateForDietOnly(date: date)).last; - } catch (e) { - rethrow; - } - } - Future> getDiaryIndexByDate(String date) async { return (await diaryService.getDiaryIndexByDate(date: date)); } @@ -254,4 +247,21 @@ class DiaryRepository { rethrow; } } + + Future getDiaryDayV2({required String day}) async { + try { + return await diaryService.getDiaryDayV2(day: day); + } catch (e) { + rethrow; + } + } + + Future createDiaryDay({required String day}) async { + try { + await diaryService.createDay(body: CreateDayModel(day: day)); + await getDiaryByDate(day); + } catch (e) { + rethrow; + } + } } diff --git a/lib/features/diary/services/diary_service.dart b/lib/features/diary/services/diary_service.dart index 8cba6f33..985fb885 100644 --- a/lib/features/diary/services/diary_service.dart +++ b/lib/features/diary/services/diary_service.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:dio/dio.dart'; import 'package:eatvio_app/features/diary/models/consultant_comment_model.dart'; import 'package:eatvio_app/features/diary/models/consultant_review_model.dart'; +import 'package:eatvio_app/features/diary/models/create_day_model.dart'; import 'package:eatvio_app/features/diary/models/diary_day_model.dart'; import 'package:eatvio_app/features/diary/models/finish_diary_day_model.dart'; import 'package:eatvio_app/features/diary/models/nutrition_model.dart'; @@ -17,8 +18,6 @@ abstract class DiaryService { @GET('/api/diary/index/{date}') Future> getDiaryIndexByDate({@Path('date') required String date}); - @GET('/api/diary/index/{date}') - Future> getDiaryIndexByDateForDietOnly({@Path('date') required String date}); @GET('/api/diary/{diary}/show') Future getDiaryDay({@Path('diary') required int diary}); @@ -103,4 +102,10 @@ abstract class DiaryService { @GET('/api/glossaries/index') Future getGlossariesIndex({@Queries() Map? filters}); + + @POST('/api/v2/diary/create/day') + Future createDay({@Body() required CreateDayModel body}); + + @GET('/api/v2/diary/index/{day?}') + Future getDiaryDayV2({@Query('day') required String day}); } diff --git a/lib/features/profile/presentation/pages/profile_screen.dart b/lib/features/profile/presentation/pages/profile_screen.dart index cc8f7245..0167fce9 100644 --- a/lib/features/profile/presentation/pages/profile_screen.dart +++ b/lib/features/profile/presentation/pages/profile_screen.dart @@ -110,6 +110,7 @@ class _ProfileScreenState extends State { Text( 'Пользователь ограничил доступ к своей странице', textAlign: TextAlign.center, + style: TextStyle(fontSize: 18, fontWeight: FontWeight.w500, color: Colors.grey[600]), ), ], diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index 85f74918..6144d39c 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -28,9 +28,10 @@ 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; From e6df89c98ad0587f26a2a21c869cb7a0cc1b8449 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Fri, 19 Sep 2025 15:55:07 +0300 Subject: [PATCH 29/78] 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 a8994eecf4a2aa8f90e77a1d6e71b291c74df8aa Mon Sep 17 00:00:00 2001 From: misha Date: Mon, 22 Sep 2025 10:52:29 +0300 Subject: [PATCH 30/78] feat(diary): fixed bugs fod diary with v2 methods --- .../diary/models/diary_day_model.dart | 13 +- .../diary/presentation/bloc/diary_cubit.dart | 120 ++++++++++-------- .../diary/presentation/bloc/diary_state.dart | 1 - .../presentation/pages/diary_screen.dart | 2 +- .../presentation/widgets/calendar_widget.dart | 75 +++-------- .../presentation/pages/profile_screen.dart | 1 - 6 files changed, 92 insertions(+), 120 deletions(-) diff --git a/lib/features/diary/models/diary_day_model.dart b/lib/features/diary/models/diary_day_model.dart index 19099ded..c602111f 100644 --- a/lib/features/diary/models/diary_day_model.dart +++ b/lib/features/diary/models/diary_day_model.dart @@ -60,24 +60,17 @@ abstract class DiaryDayModel with _$DiaryDayModel { meals: meals, chosenPlanId: chosenPlanId, dayOfChosenDiet: dayOfChosenDiet, + wakeup: wakeup, + retire: retire, diet: diet, ); } DiaryFutureDietDayEntity toDiaryFutureDayEntity() { - - return DiaryFutureDietDayEntity( - date: date, - typeOfDiary: typeOfDiary, - meals: meals, - dayOfChosenDiet: dayOfChosenDiet, - diet: diet, - ); + return DiaryFutureDietDayEntity(date: date, typeOfDiary: typeOfDiary, meals: meals, dayOfChosenDiet: dayOfChosenDiet, diet: diet); } } - - DiaryDietModel? parseDiet(dynamic json) { if (json == null) return null; return DiaryDietModel.fromJson(json['diet']); diff --git a/lib/features/diary/presentation/bloc/diary_cubit.dart b/lib/features/diary/presentation/bloc/diary_cubit.dart index 644837ac..d23cb8d4 100644 --- a/lib/features/diary/presentation/bloc/diary_cubit.dart +++ b/lib/features/diary/presentation/bloc/diary_cubit.dart @@ -23,7 +23,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; class DiaryCubit extends Cubit { late final DateTime today; late DateTime selectedDay; - late int selectedMeal; + int selectedMeal = 1; final DiaryRepository diaryRepository; final GlossaryRepository glossaryRepository; final ProfileRepository profileRepository; @@ -48,44 +48,70 @@ class DiaryCubit extends Cubit { String comment = ''; - Future setSelectedDay(DateTime day, {bool? forDietOnly}) async { - selectedDay = day; - if (forDietOnly ?? false) { - final result = await fetchDiaryV2(); - return result; + Future setSelectedDay(DateTime day) async { + final canSelect = await fetchDiaryV2(needLoading: true); + if (canSelect) { + selectedDay = day; } - await fetchDiaryV2(needLoading: true); - return true; + return canSelect; } - Future fetchDiaryV2({bool needLoading = false, int? diaryId, int? currentDay}) async { - final (diaryDay, error) = await qwe(); - if (error != null) { - if (error == DiaryFetchingErrors.canNotCreateFutureDays) { - return false; - } - emit(DiaryState.error(errorMessage: error.toString())); + Future fetchDiaryV2({bool needLoading = false, int? diaryId}) async { + final currentState = state; + if (needLoading) { + emit(const DiaryState.loading()); } - - if (diaryDay != null) { - final diaryEntity = diaryDay.toEntity(); + if (diaryId != null) { + final diaryDayById = await diaryRepository.getDiaryDay(diaryId); + final diaryEntity = diaryDayById.toEntity(); if (diaryEntity != null) { - setDiary(diaryDay: diaryEntity); + setDiary(diaryDay: diaryEntity, diaryId: diaryId); return true; } - setDiaryForDietOnly(diaryDay.toDiaryFutureDayEntity()); } - return true; + final diaryDay = await getDiary(); + if (diaryDay == null) { + emit(const DiaryState.error(errorMessage: 'Diary not found')); + return true; + } + + final diaryEntity = diaryDay.toEntity(); + if (diaryEntity != null) { + setDiary(diaryDay: diaryEntity, diaryId: diaryId); + return true; + } + if (diaryDay.typeOfDiary.hasChosenDiet == true && diaryDay.typeOfDiary.diaryState == null) { + setDiaryForDietOnly(diaryDay.toDiaryFutureDayEntity()); + return true; + } + emit(currentState); + return false; } - Future<(DiaryDayModel?, DiaryFetchingErrors?)> qwe() async { + Future updateCurrentDiary() async { try { final diaryDay = await diaryRepository.getDiaryDayV2(day: selectedDay.toISO8601WithTimeZoneOffset()); - } on DioException catch (e) { - + final diaryEntity = diaryDay.toEntity(); + if (diaryEntity != null) { + setDiary(diaryDay: diaryEntity, diaryId: diaryId); + } + } catch (e) { + log(e.toString()); } + } - return (null, null); + Future getDiary() async { + try { + await diaryRepository.createDiaryDay(day: selectedDay.toISO8601WithTimeZoneOffset()); + final diaryDay = await diaryRepository.getDiaryDayV2(day: selectedDay.toISO8601WithTimeZoneOffset()); + return diaryDay; + } on DioException catch (e) { + if (e.response?.statusCode == 422) { + final diaryDay = await diaryRepository.getDiaryDayV2(day: selectedDay.toISO8601WithTimeZoneOffset()); + return diaryDay; + } + } + return null; } void setSelectedMeal(int meal) { @@ -177,18 +203,14 @@ class DiaryCubit extends Cubit { return false; } - Future setDiary({int? diaryId, int? currentDay, required DiaryDayEntity diaryDay}) async { + Future setDiary({int? diaryId, required DiaryDayEntity diaryDay}) async { this.diaryId ??= diaryId; final isConsultantView = this.diaryId != null; try { - final DiaryDayModel diaryDay; bool isMoodEnabled = false; bool isWakeUpAndRetireTimeEnabled = false; if (diaryId != null) { - diaryDay = await diaryRepository.getDiaryDay(diaryId); } else { - final stringDate = selectedDay.format('yyyy-MM-dd'); - diaryDay = await diaryRepository.getDiaryByDate(stringDate); final profileSettings = await settingsRepository.getProfileSettings(); isMoodEnabled = profileSettings.isMoodFillable; isWakeUpAndRetireTimeEnabled = profileSettings.isBedtimeFillable; @@ -201,14 +223,14 @@ class DiaryCubit extends Cubit { final chosenPlanModel = await chatRepository.getChosenPlan(id: diaryDay.chosenPlanId!); if (!isConsultantView && (diaryDay.typeOfDiary.diaryState == DiaryStateEnum.lastDayCommented || diaryDay.typeOfDiary.diaryState == DiaryStateEnum.commented)) { - await diaryRepository.viewCommentOfDiaryDayByConsultant(diaryId: diaryDay.id!); + await diaryRepository.viewCommentOfDiaryDayByConsultant(diaryId: diaryDay.id); } - consultantReview = await diaryRepository.getDiaryComments(planId: diaryDay.chosenPlanId!, diaryId: diaryDay.id!); + consultantReview = await diaryRepository.getDiaryComments(planId: diaryDay.chosenPlanId!, diaryId: diaryDay.id); planDuration = chosenPlanModel.plan.duration; planName = chosenPlanModel.plan.name; currentPlanDay = consultantReview.currentPlanDay; } - final nutrition = await getNutrition(diaryDay.id!); + final nutrition = await getNutrition(diaryDay.id); final isDiaryFinished = diaryDay.typeOfDiary.diaryState == DiaryStateEnum.finished || diaryDay.typeOfDiary.diaryState == DiaryStateEnum.lastDayFinished; @@ -218,7 +240,7 @@ class DiaryCubit extends Cubit { diaryDay.typeOfDiary.diaryState == DiaryStateEnum.reviewedByUser; emit( DiaryState.loaded( - diary: diaryDay.toEntity()!, + diary: diaryDay, nutrition: nutrition, selectedDay: selectedDay, selectedMeal: selectedMeal, @@ -241,7 +263,7 @@ class DiaryCubit extends Cubit { Future finishDiaryDay({required int diaryId, required String wakeUp, required String retire}) async { await diaryRepository.finishDiaryDay(diaryId: diaryId, wakeUp: wakeUp, retire: retire, comment: comment.isEmpty ? null : comment); - await fetchDiaryV2(); + await updateCurrentDiary(); } Future getNutrition(int index) async { @@ -265,7 +287,7 @@ class DiaryCubit extends Cubit { quantity: quantity, productNutritionalValueId: productNutritionalValueId, ); - await fetchDiaryV2(); + await updateCurrentDiary(); } Future updateProductInMeal({ @@ -299,27 +321,27 @@ class DiaryCubit extends Cubit { portionState: portionState, ); } - await fetchDiaryV2(); + await updateCurrentDiary(); } Future markEaten({required int diaryId, required int mealId, required int portionId}) async { await diaryRepository.markEaten(diaryId: diaryId, mealId: mealId, portionId: portionId); - await fetchDiaryV2(); + await updateCurrentDiary(); } Future unmarkEaten({required int diaryId, required int mealId, required int portionId}) async { await diaryRepository.unmarkEaten(diaryId: diaryId, mealId: mealId, portionId: portionId); - await fetchDiaryV2(); + await updateCurrentDiary(); } Future restore({required int diaryId, required int mealId, required int portionId}) async { await diaryRepository.restore(diaryId: diaryId, mealId: mealId, portionId: portionId); - await fetchDiaryV2(); + await updateCurrentDiary(); } Future deleteProductFromMeal({required int diaryId, required int mealId, required int portionId}) async { await diaryRepository.deleteProductFromMeal(diaryId: diaryId, mealId: mealId, portionId: portionId); - await fetchDiaryV2(); + await updateCurrentDiary(); } Future> getGlossaryListByQuery(String query) async { @@ -328,12 +350,12 @@ class DiaryCubit extends Cubit { Future uploadPhotos({required List files, required int diaryId}) async { await diaryRepository.uploadPhotos(files: await ImageCompressUtility.compressFilesList(files), diaryId: diaryId, mealId: selectedMeal); - await fetchDiaryV2(); + await updateCurrentDiary(); } Future deletePhoto({required DiaryPhotoModel photo, required int diaryId}) async { await diaryRepository.deletePhoto(photo: photo, diaryId: diaryId, mealId: selectedMeal); - await fetchDiaryV2(); + await updateCurrentDiary(); } Future sendProductToModeration({ @@ -353,34 +375,32 @@ class DiaryCubit extends Cubit { Future fillTime({required int diaryId, required String time}) async { await diaryRepository.fillTime(diaryId: diaryId, mealId: selectedMeal, time: time); - await fetchDiaryV2(); + await updateCurrentDiary(); } Future fillWakeup({required int diaryId, required String time}) async { await diaryRepository.fillWakeup(diaryId: diaryId, time: time); - await fetchDiaryV2(); + await updateCurrentDiary(); } Future fillRetire({required int diaryId, required String time}) async { await diaryRepository.fillRetire(diaryId: diaryId, time: time); - await fetchDiaryV2(); + await updateCurrentDiary(); } Future fillSatiety({required int diaryId, required int satiety}) async { await diaryRepository.fillSatiety(diaryId: diaryId, mealId: selectedMeal, satiety: satiety); - await fetchDiaryV2(); + await updateCurrentDiary(); } Future fillMood({required int diaryId, required int mood}) async { await diaryRepository.fillMood(diaryId: diaryId, mealId: selectedMeal, mood: mood); - await fetchDiaryV2(); + await updateCurrentDiary(); } void fillComment(String comment) { this.comment = comment; } - - } enum DiaryFetchingErrors { canNotCreateFutureDays, dayAlreadyExists } diff --git a/lib/features/diary/presentation/bloc/diary_state.dart b/lib/features/diary/presentation/bloc/diary_state.dart index 771ebb31..02209371 100644 --- a/lib/features/diary/presentation/bloc/diary_state.dart +++ b/lib/features/diary/presentation/bloc/diary_state.dart @@ -1,7 +1,6 @@ import 'package:eatvio_app/features/diary/entities/diary_day_entity.dart'; import 'package:eatvio_app/features/diary/entities/diary_future_diet_day_entity.dart'; import 'package:eatvio_app/features/diary/models/consultant_review_model.dart'; -import 'package:eatvio_app/features/diary/models/diary_day_model.dart'; import 'package:eatvio_app/features/diary/models/nutrition_model.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; diff --git a/lib/features/diary/presentation/pages/diary_screen.dart b/lib/features/diary/presentation/pages/diary_screen.dart index 4ec74e16..6b636857 100644 --- a/lib/features/diary/presentation/pages/diary_screen.dart +++ b/lib/features/diary/presentation/pages/diary_screen.dart @@ -53,7 +53,7 @@ class DiaryScreenContent extends StatelessWidget { final s = S.of(context); return MultiBlocProvider( providers: [ - BlocProvider(create: (BuildContext context) => sl()..fetchDiaryV2(needLoading: true, diaryId: diaryId, currentDay: currentPlanDay)), + BlocProvider(create: (BuildContext context) => sl()..fetchDiaryV2(needLoading: true, diaryId: diaryId)), BlocProvider(create: (context) => sl()), ], child: BlocBuilder( diff --git a/lib/features/diary/presentation/widgets/calendar_widget.dart b/lib/features/diary/presentation/widgets/calendar_widget.dart index b0e5c442..cce80a55 100644 --- a/lib/features/diary/presentation/widgets/calendar_widget.dart +++ b/lib/features/diary/presentation/widgets/calendar_widget.dart @@ -43,46 +43,24 @@ class _CalendarWidgetState extends State { locale: 'ru_RU', startingDayOfWeek: StartingDayOfWeek.monday, calendarFormat: _calendarFormat, - headerStyle: const HeaderStyle( - titleCentered: true, - formatButtonVisible: false, - leftChevronVisible: false, - rightChevronVisible: false, - ), + headerStyle: const HeaderStyle(titleCentered: true, formatButtonVisible: false, leftChevronVisible: false, rightChevronVisible: false), onDaySelected: (selectedDay, focusedDay) async { if (!isSameDay(_selectedDay, selectedDay)) { - final today = DateTime.now(); - if (selectedDay.isAfter(today)) { - if ((state is DiaryStateLoaded)) { - final result = await context.read().setSelectedDay(_selectedDay, forDietOnly: true); - if (result == false && context.mounted) { - showErrorDialog(context, title: s.want_to_see_tomorrow, description: s.can_not_see_next_day); - return; - } - setState(() { - _selectedDay = selectedDay; - _focusedDay = focusedDay; - }); - return; - } - if (state is DiaryStateLoadedTomorrowWithDiet) { - final result = await context.read().setSelectedDay(_selectedDay, forDietOnly: true); - if (result == false && context.mounted) { - showErrorDialog(context, title: s.want_to_see_tomorrow, description: s.can_not_see_next_day); - return; - } - setState(() { - _selectedDay = selectedDay; - _focusedDay = focusedDay; - }); - return; - } - } + final previousSelectedDay = _selectedDay; + final previousFocusedDay = _selectedDay; setState(() { _selectedDay = selectedDay; _focusedDay = focusedDay; }); - context.read().setSelectedDay(_selectedDay); + final canOpenNextDay = await context.read().setSelectedDay(selectedDay); + if (!canOpenNextDay && context.mounted) { + showErrorDialog(context, title: s.want_to_see_tomorrow, description: s.can_not_see_next_day); + setState(() { + _selectedDay = previousSelectedDay; + _focusedDay = previousFocusedDay; + }); + return; + } } }, selectedDayPredicate: (day) { @@ -101,26 +79,12 @@ class _CalendarWidgetState extends State { } }, calendarStyle: CalendarStyle( - weekendDecoration: const BoxDecoration( - color: Colors.white, - shape: BoxShape.circle, - ), - defaultDecoration: const BoxDecoration( - color: Colors.white, - shape: BoxShape.circle, - ), - selectedDecoration: BoxDecoration( - color: appTheme.blueA400, - shape: BoxShape.circle, - ), - todayTextStyle: const TextStyle( - color: Colors.black, - ), + weekendDecoration: const BoxDecoration(color: Colors.white, shape: BoxShape.circle), + defaultDecoration: const BoxDecoration(color: Colors.white, shape: BoxShape.circle), + selectedDecoration: BoxDecoration(color: appTheme.blueA400, shape: BoxShape.circle), + todayTextStyle: const TextStyle(color: Colors.black), todayDecoration: BoxDecoration( - border: Border.all( - color: appTheme.blueA400, - width: 2, - ), + border: Border.all(color: appTheme.blueA400, width: 2), color: Colors.white, shape: BoxShape.circle, ), @@ -152,10 +116,7 @@ class _CalendarWidgetState extends State { child: Container( width: double.infinity, padding: const EdgeInsets.only(top: 8, bottom: 16), - child: Icon( - _calendarFormat == CalendarFormat.month ? Icons.keyboard_arrow_up_rounded : Icons.keyboard_arrow_down_rounded, - color: Colors.grey.shade600, - ), + child: Icon(_calendarFormat == CalendarFormat.month ? Icons.keyboard_arrow_up_rounded : Icons.keyboard_arrow_down_rounded, color: Colors.grey.shade600), ), ); } diff --git a/lib/features/profile/presentation/pages/profile_screen.dart b/lib/features/profile/presentation/pages/profile_screen.dart index 0167fce9..1ea45b7c 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'; From ad777c695bd38fd3a308b095c853bf4f8474e62a Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Mon, 22 Sep 2025 17:08:41 +0300 Subject: [PATCH 31/78] 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 32/78] 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 517e493b5e442efb2e6cc37d92178df6c2e87d6d Mon Sep 17 00:00:00 2001 From: misha Date: Wed, 24 Sep 2025 15:23:01 +0300 Subject: [PATCH 33/78] feat(pushNotifications): added anonymous token for push notifications --- lib/core/app_router.dart | 21 ++++++--- lib/core/firebase_api.dart | 16 +++++++ lib/core/locator_service.dart | 8 +++- lib/core/prefs.dart | 43 +++++++++++++++++++ lib/core/prefs_keys.dart | 1 + .../presentation/pages/welcome_screen.dart | 8 ++-- 6 files changed, 87 insertions(+), 10 deletions(-) create mode 100644 lib/core/prefs.dart create mode 100644 lib/core/prefs_keys.dart diff --git a/lib/core/app_router.dart b/lib/core/app_router.dart index cc028255..9b94b818 100644 --- a/lib/core/app_router.dart +++ b/lib/core/app_router.dart @@ -1,6 +1,8 @@ import 'package:dio/dio.dart'; import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/firebase_api.dart'; +import 'package:eatvio_app/core/prefs.dart'; +import 'package:eatvio_app/core/prefs_keys.dart'; import 'package:eatvio_app/core/token_storage.dart'; import 'package:eatvio_app/core/websockets/ws_manager.dart'; import 'package:eatvio_app/core/locator_service.dart'; @@ -72,7 +74,6 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:go_router/go_router.dart'; import 'package:package_info_plus/package_info_plus.dart'; -import 'package:shared_preferences/shared_preferences.dart'; import 'package:version/version.dart'; import 'dart:developer'; @@ -111,6 +112,10 @@ class AppRouter { path: '/auth-screen', name: 'authScreen', builder: (context, state) => const AuthScreen(), + redirect: (context, state) async { + await sl().registerAnonymousPushToken(); + return null; + }, routes: [ GoRoute(path: 'registration-screen', name: 'registrationScreen', builder: (context, state) => const RegistrationScreen()), GoRoute( @@ -196,14 +201,20 @@ class AppRouter { builder: (context, state) => const WelcomeScreen(), redirect: (context, state) async { try { - final sharedPreferences = await SharedPreferences.getInstance(); - final welcomeScreenWasShowed = sharedPreferences.getBool('welcomeScreenWasShowed') ?? false; - await sl().registerPushToken(); - if (welcomeScreenWasShowed == false) { + final prefs = sl(); + final firebaseApi = sl(); + final welcomeScreenWasShowed = prefs.getBool(PrefsKeys.welcomeScreenWasShowed) ?? false; + await firebaseApi.registerPushToken(); + if (!welcomeScreenWasShowed) { return null; } else if (await TokenStorage.haveTokenInStorage()) { return '/feed-screen'; } else { + final hasAnonymousTokenBeenSent = prefs.getBool(PrefsKeys.hasAnonymousTokenBeenSent) ?? false; + if (!hasAnonymousTokenBeenSent) { + await firebaseApi.registerAnonymousPushToken(); + prefs.setBool(PrefsKeys.hasAnonymousTokenBeenSent, true); + } return '/auth-screen'; } } catch (e) { diff --git a/lib/core/firebase_api.dart b/lib/core/firebase_api.dart index 2ba89290..cd0c4d12 100644 --- a/lib/core/firebase_api.dart +++ b/lib/core/firebase_api.dart @@ -26,6 +26,22 @@ class FirebaseApi { return token; } + Future registerAnonymousPushToken() async { + try { + final deviceInfo = DeviceInfoPlugin(); + await sl().post( + '/api/anonymous_token', + data: { + 'platform': Platform.isIOS ? 'ios' : 'android', + 'hw_id': Platform.isIOS ? (await deviceInfo.iosInfo).identifierForVendor : (await deviceInfo.androidInfo).id, + 'token': await getToken(), + }, + ); + } catch (e, st) { + log(e.toString(), stackTrace: st); + } + } + Future registerPushToken() async { if (await TokenStorage.haveTokenInStorage()) { try { diff --git a/lib/core/locator_service.dart b/lib/core/locator_service.dart index 37713f29..4ac7e003 100644 --- a/lib/core/locator_service.dart +++ b/lib/core/locator_service.dart @@ -5,6 +5,7 @@ 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/prefs.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'; @@ -118,10 +119,14 @@ import 'package:eatvio_app/features/settings/services/settings_service.dart'; import 'package:eatvio_app/features/user_details/presentation/cubit/user_details_cubit.dart'; import 'package:get_it/get_it.dart'; import 'package:eatvio_app/features/profile/presentation/bloc/plans_cubit/plans_cubit.dart' as profile_plans; +import 'package:shared_preferences/shared_preferences.dart'; final sl = GetIt.instance; Future init() async { + // Prefs + sl.registerLazySingletonAsync(() async=> Prefs(await SharedPreferences.getInstance())); + // Cubit singletons sl.registerLazySingleton(() => WsManager(wsService: sl())); sl.registerLazySingleton(() => NewMessagesCubit()); @@ -216,8 +221,7 @@ Future init() async { sl.registerLazySingleton(() => CommentsRepository(service: sl())); sl.registerLazySingleton(() => NotificationSettingsRepository(sl())); sl.registerFactory(() => ConsultantRatingRepository(sl())); - sl.registerFactory(() => ViewsRepository(viewsService: sl())); - + sl.registerFactory(() => ViewsRepository(viewsService: sl())); // Services sl.registerLazySingleton(() => ChatService(sl())); sl.registerLazySingleton(() => FeedService(sl())); diff --git a/lib/core/prefs.dart b/lib/core/prefs.dart new file mode 100644 index 00000000..14ed7812 --- /dev/null +++ b/lib/core/prefs.dart @@ -0,0 +1,43 @@ +import 'package:eatvio_app/core/prefs_keys.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + + +class Prefs { + final SharedPreferences _prefs; + + Prefs(this._prefs); + + String _key(PrefsKeys key) => key.name; + + Future setString(PrefsKeys key, String value) async { + await _prefs.setString(_key(key), value); + } + + String? getString(PrefsKeys key) { + return _prefs.getString(_key(key)); + } + + Future setInt(PrefsKeys key, int value) async { + await _prefs.setInt(_key(key), value); + } + + int? getInt(PrefsKeys key) { + return _prefs.getInt(_key(key)); + } + + Future setBool(PrefsKeys key, bool value) async { + await _prefs.setBool(_key(key), value); + } + + bool? getBool(PrefsKeys key) { + return _prefs.getBool(_key(key)); + } + + Future remove(PrefsKeys key) async { + await _prefs.remove(_key(key)); + } + + Future clear() async { + await _prefs.clear(); + } +} diff --git a/lib/core/prefs_keys.dart b/lib/core/prefs_keys.dart new file mode 100644 index 00000000..0ba552d5 --- /dev/null +++ b/lib/core/prefs_keys.dart @@ -0,0 +1 @@ +enum PrefsKeys { hasAnonymousTokenBeenSent, welcomeScreenWasShowed } diff --git a/lib/features/welcome/presentation/pages/welcome_screen.dart b/lib/features/welcome/presentation/pages/welcome_screen.dart index 31b4bf0f..887e2d9e 100644 --- a/lib/features/welcome/presentation/pages/welcome_screen.dart +++ b/lib/features/welcome/presentation/pages/welcome_screen.dart @@ -1,9 +1,11 @@ +import 'package:eatvio_app/core/locator_service.dart'; +import 'package:eatvio_app/core/prefs.dart'; +import 'package:eatvio_app/core/prefs_keys.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/token_storage.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:go_router/go_router.dart'; -import 'package:shared_preferences/shared_preferences.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart'; class WelcomeScreen extends StatefulWidget { @@ -80,8 +82,8 @@ class _WelcomeScreenState extends State { const SizedBox(height: 24), InkWell( onTap: () async { - final sharedPreferences = await SharedPreferences.getInstance(); - await sharedPreferences.setBool('welcomeScreenWasShowed', true); + final sharedPreferences = sl(); + await sharedPreferences.setBool(PrefsKeys.welcomeScreenWasShowed, true); if (await TokenStorage.haveTokenInStorage()) { goRouter.go('/feed-screen'); } else { From fc09fa36b8ae48c78e1d88c7e39240040b797036 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Wed, 24 Sep 2025 23:47:11 +0300 Subject: [PATCH 34/78] 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 35/78] 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 c88b010cc53758af343f19106e4704b8566a2d9a Mon Sep 17 00:00:00 2001 From: misha Date: Mon, 29 Sep 2025 09:13:45 +0300 Subject: [PATCH 36/78] feat(glossary): added pass key for glossary --- lib/core/app_router.dart | 2 +- .../dio/phone_verify_error_interceptor.dart | 4 +- lib/core/locator_service.dart | 2 + .../presentation/pages/glossary_screen.dart | 10 +- .../bloc/menu_cubit/menu_cubit.dart | 20 +++ .../bloc/menu_cubit/menu_state.dart | 1 + .../menu/presentation/pages/menu_screen.dart | 140 ++++++++++-------- .../profile/models/pass_key_params.dart | 15 ++ .../repositories/profile_repository.dart | 9 +- .../profile/services/profile_service.dart | 3 +- lib/generated/l10n.dart | 7 +- 11 files changed, 137 insertions(+), 76 deletions(-) create mode 100644 lib/features/menu/presentation/bloc/menu_cubit/menu_cubit.dart create mode 100644 lib/features/menu/presentation/bloc/menu_cubit/menu_state.dart create mode 100644 lib/features/profile/models/pass_key_params.dart diff --git a/lib/core/app_router.dart b/lib/core/app_router.dart index cc028255..dda4395a 100644 --- a/lib/core/app_router.dart +++ b/lib/core/app_router.dart @@ -257,7 +257,7 @@ class AppRouter { return AboutPlanScreen(slug: data['slug'] as String, index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool); }, ), - GoRoute(path: '/glossary-screen', name: 'glossaryScreen', builder: (context, state) => const GlossaryScreen()), + GoRoute(path: '/glossary-screen', name: 'glossaryScreen', builder: (context, state) => GlossaryScreen(passKey: state.extra as String,)), GoRoute( path: '/about-recipes-screen', name: 'aboutRecipesScreen', diff --git a/lib/core/dio/phone_verify_error_interceptor.dart b/lib/core/dio/phone_verify_error_interceptor.dart index ab6188bf..4073cb85 100644 --- a/lib/core/dio/phone_verify_error_interceptor.dart +++ b/lib/core/dio/phone_verify_error_interceptor.dart @@ -12,8 +12,8 @@ class PhoneVerifyErrorInterceptor extends Interceptor { final data = err.response?.data; if (data is Map && data['type'] == 'unverificated_phone') { - final String phone = await sl().getMyPhone(); - final String passKey = await sl().getPassKey(phone); + final String phone = await sl().getMyPhone(); + final String passKey = await sl().getPassKey(routeName: 'user_detailz.change-phone-verify', routeParams: {"phone": phone}); await showErrorDialog(AppRouter.navigatorKey.currentContext!, title: 'Привяжите номер телефона', description: 'Для того, что бы выполнить это действие необходимо привязать номер телефона'); AppRouter.navigatorKey.currentContext?.pushNamed('verifyScreen', extra: passKey); diff --git a/lib/core/locator_service.dart b/lib/core/locator_service.dart index 37713f29..917c9dd1 100644 --- a/lib/core/locator_service.dart +++ b/lib/core/locator_service.dart @@ -45,6 +45,7 @@ import 'package:eatvio_app/features/blog_post_create/services/blog_post_service. import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/new_messages_cubit/new_messages_cubit.dart'; import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/consultant_finances/consultant_finances_cubit.dart'; import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/user_purchases/user_purchases_cubit.dart'; +import 'package:eatvio_app/features/menu/presentation/bloc/menu_cubit/menu_cubit.dart'; import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_cubit.dart'; import 'package:eatvio_app/features/menu/presentation/bloc/notification_settings_cubit/notification_settings_cubit.dart'; import 'package:eatvio_app/features/menu/presentation/repositories/blocked_users_repository.dart'; @@ -183,6 +184,7 @@ Future init() async { sl.registerFactory(() => NotificationSettingsCubit(notificationSettingsRepository: sl())); sl.registerFactory(() => ConsultantRatingCubit(sl())); sl.registerFactory(() => ViewsCubit(viewsRepository: sl())); + sl.registerFactory(() => MenuCubit(sl())); // Repository sl.registerLazySingleton(() => ChatRepository(chatService: sl())); diff --git a/lib/features/glossary/presentation/pages/glossary_screen.dart b/lib/features/glossary/presentation/pages/glossary_screen.dart index 7205b3d3..1c4dd4cd 100644 --- a/lib/features/glossary/presentation/pages/glossary_screen.dart +++ b/lib/features/glossary/presentation/pages/glossary_screen.dart @@ -3,8 +3,8 @@ import 'package:flutter/material.dart'; import 'package:webview_flutter/webview_flutter.dart'; class GlossaryScreen extends StatefulWidget { - const GlossaryScreen({super.key}); - + const GlossaryScreen({super.key, required this.passKey}); + final String passKey; @override State createState() => _GlossaryScreenState(); } @@ -18,15 +18,13 @@ class _GlossaryScreenState extends State { _controller = WebViewController() ..setJavaScriptMode(JavaScriptMode.unrestricted) - ..loadRequest(Uri.parse('$baseUrl/product_nutritional_value')); + ..loadRequest(Uri.parse('$baseUrl/login_with_pass_key?id=${widget.passKey}')); } @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - title: const Text('Глоссарий'), - ), + appBar: AppBar(title: const Text('Глоссарий')), body: WebViewWidget(controller: _controller), ); } diff --git a/lib/features/menu/presentation/bloc/menu_cubit/menu_cubit.dart b/lib/features/menu/presentation/bloc/menu_cubit/menu_cubit.dart new file mode 100644 index 00000000..bb60d139 --- /dev/null +++ b/lib/features/menu/presentation/bloc/menu_cubit/menu_cubit.dart @@ -0,0 +1,20 @@ +import 'dart:developer'; + +import 'package:eatvio_app/features/menu/presentation/bloc/menu_cubit/menu_state.dart'; +import 'package:eatvio_app/features/profile/repositories/profile_repository.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class MenuCubit extends Cubit { + final ProfileRepository profileRepository; + MenuCubit(this.profileRepository) : super(MenuState()); + + Future makePassKey() async { + try { + final passKey = await profileRepository.getPassKey(routeName: 'product_nutritional_value.index', routeParams: {}); + return passKey; + } catch (e) { + log(e.toString()); + } + return null; + } +} diff --git a/lib/features/menu/presentation/bloc/menu_cubit/menu_state.dart b/lib/features/menu/presentation/bloc/menu_cubit/menu_state.dart new file mode 100644 index 00000000..70f37bb8 --- /dev/null +++ b/lib/features/menu/presentation/bloc/menu_cubit/menu_state.dart @@ -0,0 +1 @@ +class MenuState {} \ No newline at end of file diff --git a/lib/features/menu/presentation/pages/menu_screen.dart b/lib/features/menu/presentation/pages/menu_screen.dart index 5c350d3c..77bbbd20 100644 --- a/lib/features/menu/presentation/pages/menu_screen.dart +++ b/lib/features/menu/presentation/pages/menu_screen.dart @@ -9,12 +9,14 @@ import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/features/consultant_rating/presentation/consultant_rating_screen.dart'; import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/consultant_finances_screen.dart'; import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/user_purchases_screen.dart'; +import 'package:eatvio_app/features/menu/presentation/bloc/menu_cubit/menu_cubit.dart'; import 'package:eatvio_app/features/my_diets/presentation/my_diets_screen.dart'; import 'package:eatvio_app/features/my_plans/presentation/my_plans_screen.dart'; import 'package:eatvio_app/features/profile/models/profile_model.dart'; import 'package:eatvio_app/features/recipe/presentation/pages/my_recipes_screen.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'; import 'package:go_router/go_router.dart'; import 'package:package_info_plus/package_info_plus.dart'; @@ -26,66 +28,86 @@ class MenuScreen extends StatelessWidget { @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(centerTitle: false, title: const Text('Меню')), - body: SingleChildScrollView( - child: Column( - children: [ - _tile(title: 'Настройки профиля', leading: ImageConstant.icnProfileSettings, onTap: () => GoRouter.of(context).push('/settings-screen')), - if (profileModel.roles.contains('consultant')) _divider(context), - if (profileModel.roles.contains('consultant')) - _tile(leading: ImageConstant.icnRecipes, title: 'Мои рецепты', onTap: () => GoRouter.of(context).push(MyRecipesScreen.path)), - if (profileModel.roles.contains('consultant')) _divider(context), - if (profileModel.roles.contains('consultant')) - _tile(leading: 'assets/icons/diets.svg', title: 'Мои диеты', onTap: () => GoRouter.of(context).push(MyDietsScreen.path)), - if (profileModel.roles.contains('consultant')) _divider(context), - if (profileModel.roles.contains('consultant')) - _tile(leading: ImageConstant.icnPlans, title: 'Мои планы', onTap: () => GoRouter.of(context).push(MyPlansScreen.path)), - if (!hidePlans) ...[ - _divider(context), - _tile( - leading: 'assets/icons/finance.svg', - title: 'Мои финансы', - onTap: () { - if (profileModel.roles.contains('consultant')) { - GoRouter.of(context).push(ConsultantFinancesScreen.path); - return; - } - GoRouter.of(context).push(UserPurchasesScreen.path); - }, + return BlocProvider( + create: (context) => sl(), + child: Builder( + builder: (context) { + return Scaffold( + appBar: AppBar(centerTitle: false, title: const Text('Меню')), + body: SingleChildScrollView( + child: Column( + children: [ + _tile(title: 'Настройки профиля', leading: ImageConstant.icnProfileSettings, onTap: () => GoRouter.of(context).push('/settings-screen')), + if (profileModel.roles.contains('consultant')) _divider(context), + if (profileModel.roles.contains('consultant')) + _tile(leading: ImageConstant.icnRecipes, title: 'Мои рецепты', onTap: () => GoRouter.of(context).push(MyRecipesScreen.path)), + if (profileModel.roles.contains('consultant')) _divider(context), + if (profileModel.roles.contains('consultant')) + _tile(leading: 'assets/icons/diets.svg', title: 'Мои диеты', onTap: () => GoRouter.of(context).push(MyDietsScreen.path)), + if (profileModel.roles.contains('consultant')) _divider(context), + if (profileModel.roles.contains('consultant')) + _tile(leading: ImageConstant.icnPlans, title: 'Мои планы', onTap: () => GoRouter.of(context).push(MyPlansScreen.path)), + if (!hidePlans) ...[ + _divider(context), + _tile( + leading: 'assets/icons/finance.svg', + title: 'Мои финансы', + onTap: () { + if (profileModel.roles.contains('consultant')) { + GoRouter.of(context).push(ConsultantFinancesScreen.path); + return; + } + GoRouter.of(context).push(UserPurchasesScreen.path); + }, + ), + ], + if (profileModel.roles.contains('consultant')) _divider(context), + if (profileModel.roles.contains('consultant')) + _tile( + leading: 'assets/icons/finance.svg', + title: 'Мой рейтинг', + onTap: () { + GoRouter.of(context).push(ConsultantRatingScreen.path); + }, + ), + _divider(context), + _tile( + leading: ImageConstant.icnGlossary, + title: 'Глоссарий', + onTap: () async { + final passKey = await context.read().makePassKey(); + if (passKey != null && context.mounted) { + GoRouter.of(context).push('/glossary-screen', extra: passKey); + } + }, + ), + _divider(context), + _tile( + leading: 'assets/icons/notifications.svg', + title: 'Уведомления', + onTap: () => GoRouter.of(context).pushNamed('notificationsSettingsScreen'), + ), + _divider(context), + _tile(leading: ImageConstant.icnSupport, title: 'Поддержка', onTap: () => GoRouter.of(context).push('/support-screen')), + _divider(context), + _tile(leading: 'assets/icons/blocked_users.svg', title: 'Чёрный список', onTap: () => GoRouter.of(context).pushNamed('blockedUsersScreen')), + // _divider(context), + // _tile( + // leading: ImageConstant.icnDevTools, + // title: 'Для разработчиков', + // onTap: () { + // GoRouter.of(context).pushNamed(DevToolsScreen.name); + // }, + // ), + _divider(context), + _tile(leading: ImageConstant.icnExit, title: 'Выход', onTap: _onTapLogout(context)), + const SizedBox(height: 10), + const AppInfo(), + ], ), - ], - if (profileModel.roles.contains('consultant')) _divider(context), - if (profileModel.roles.contains('consultant')) - _tile( - leading: 'assets/icons/finance.svg', - title: 'Мой рейтинг', - onTap: () { - GoRouter.of(context).push(ConsultantRatingScreen.path); - }, - ), - _divider(context), - _tile(leading: ImageConstant.icnGlossary, title: 'Глоссарий', onTap: () => GoRouter.of(context).push('/glossary-screen')), - _divider(context), - _tile(leading: 'assets/icons/notifications.svg', title: 'Уведомления', onTap: () => GoRouter.of(context).pushNamed('notificationsSettingsScreen')), - _divider(context), - _tile(leading: ImageConstant.icnSupport, title: 'Поддержка', onTap: () => GoRouter.of(context).push('/support-screen')), - _divider(context), - _tile(leading: 'assets/icons/blocked_users.svg', title: 'Чёрный список', onTap: () => GoRouter.of(context).pushNamed('blockedUsersScreen')), - // _divider(context), - // _tile( - // leading: ImageConstant.icnDevTools, - // title: 'Для разработчиков', - // onTap: () { - // GoRouter.of(context).pushNamed(DevToolsScreen.name); - // }, - // ), - _divider(context), - _tile(leading: ImageConstant.icnExit, title: 'Выход', onTap: _onTapLogout(context)), - const SizedBox(height: 10), - const AppInfo(), - ], - ), + ), + ); + }, ), ); } diff --git a/lib/features/profile/models/pass_key_params.dart b/lib/features/profile/models/pass_key_params.dart new file mode 100644 index 00000000..972f0f9f --- /dev/null +++ b/lib/features/profile/models/pass_key_params.dart @@ -0,0 +1,15 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'pass_key_params.freezed.dart'; +part 'pass_key_params.g.dart'; + +@freezed +abstract class PassKeyParams with _$PassKeyParams { + + factory PassKeyParams({ + @JsonKey(name: 'route_name') required String routeName, + @JsonKey(name: 'route_params') Map? routeParams, + }) = _PassKeyParams; + + factory PassKeyParams.fromJson(Map json) => _$PassKeyParamsFromJson(json); +} \ No newline at end of file diff --git a/lib/features/profile/repositories/profile_repository.dart b/lib/features/profile/repositories/profile_repository.dart index 4867d85a..0f2e903c 100644 --- a/lib/features/profile/repositories/profile_repository.dart +++ b/lib/features/profile/repositories/profile_repository.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart'; import 'package:eatvio_app/features/profile/models/chosen_diets_pagination_model.dart'; import 'package:eatvio_app/features/profile/models/circumference_model.dart'; +import 'package:eatvio_app/features/profile/models/pass_key_params.dart'; import 'package:eatvio_app/features/profile/models/profile_model.dart'; import 'package:eatvio_app/features/profile/models/sub_model.dart'; import 'package:eatvio_app/features/profile/models/weight_model.dart'; @@ -154,10 +155,10 @@ class ProfileRepository { } } - Future getPassKey(String phone) async { - return (jsonDecode( - (await profileService.getPassKey(json: '{"route_name": "user_detailz.change-phone-verify", "route_params": {"phone": "$phone"}}')), - ))['key_is']!; + Future getPassKey({required String routeName, Map? routeParams}) async { + return (await profileService.getPassKey( + json: PassKeyParams(routeName: routeName, routeParams: routeParams), + )).data['key_is']!; } Future getMyPhone() async { diff --git a/lib/features/profile/services/profile_service.dart b/lib/features/profile/services/profile_service.dart index b313f27f..95117d36 100644 --- a/lib/features/profile/services/profile_service.dart +++ b/lib/features/profile/services/profile_service.dart @@ -4,6 +4,7 @@ import 'package:dio/dio.dart'; import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart'; import 'package:eatvio_app/features/profile/models/chosen_diets_pagination_model.dart'; import 'package:eatvio_app/features/profile/models/circumference_model.dart'; +import 'package:eatvio_app/features/profile/models/pass_key_params.dart'; import 'package:eatvio_app/features/profile/models/profile_model.dart'; import 'package:eatvio_app/features/profile/models/sub_model.dart'; import 'package:eatvio_app/features/profile/models/weight_model.dart'; @@ -75,7 +76,7 @@ abstract class ProfileService { Future getChosenDiets(); @POST('/api/profile/make_pass_key') - Future getPassKey({@Body() required String json}); + Future getPassKey({@Body() required PassKeyParams json}); @GET('/api/profile/my_phone') Future getMyPhone(); diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index 85f74918..6144d39c 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -28,9 +28,10 @@ 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; From 782f75d26b9398fb2649833986a5c1a285cf52c1 Mon Sep 17 00:00:00 2001 From: misha Date: Mon, 29 Sep 2025 09:49:59 +0300 Subject: [PATCH 37/78] feat(diary): fixed cpfc for v2 diary --- .../diary/repositories/diary_repository.dart | 50 ++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/lib/features/diary/repositories/diary_repository.dart b/lib/features/diary/repositories/diary_repository.dart index ddc9c74a..642a8121 100644 --- a/lib/features/diary/repositories/diary_repository.dart +++ b/lib/features/diary/repositories/diary_repository.dart @@ -296,7 +296,8 @@ class DiaryRepository { Future getDiaryDayV2({required String day}) async { try { - return await diaryService.getDiaryDayV2(day: day); + final diaryDay = await diaryService.getDiaryDayV2(day: day); + return setCPFCToDiary(diaryDay); } catch (e) { rethrow; } @@ -310,4 +311,51 @@ class DiaryRepository { rethrow; } } + + Future setCPFCToDiary(DiaryDayModel diaryDay) async { + 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; + } } From 46d0e31f5f974b09f957eaef545b39ead9ae6bc5 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Mon, 29 Sep 2025 13:29:39 +0300 Subject: [PATCH 38/78] 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/'} } } From 24fe984e6bd63f2468fae0c8c73bb540dc351c02 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Mon, 29 Sep 2025 23:45:18 +0300 Subject: [PATCH 39/78] feat(auth): Issue-691 --- ...otlin-compiler-10039299475187077818.salive | 0 assets/icons/mail_logo.svg | 11 +++ assets/icons/ok_logo.svg | 5 + assets/icons/vk_logo.svg | 16 ++++ assets/icons/yandex_logo.svg | 16 ++++ .../auth/presentation/pages/auth_screen.dart | 92 +++++++++---------- 6 files changed, 91 insertions(+), 49 deletions(-) delete mode 100644 android/.kotlin/sessions/kotlin-compiler-10039299475187077818.salive create mode 100644 assets/icons/mail_logo.svg create mode 100644 assets/icons/ok_logo.svg create mode 100644 assets/icons/vk_logo.svg create mode 100644 assets/icons/yandex_logo.svg diff --git a/android/.kotlin/sessions/kotlin-compiler-10039299475187077818.salive b/android/.kotlin/sessions/kotlin-compiler-10039299475187077818.salive deleted file mode 100644 index e69de29b..00000000 diff --git a/assets/icons/mail_logo.svg b/assets/icons/mail_logo.svg new file mode 100644 index 00000000..5891b116 --- /dev/null +++ b/assets/icons/mail_logo.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/assets/icons/ok_logo.svg b/assets/icons/ok_logo.svg new file mode 100644 index 00000000..07a62fbd --- /dev/null +++ b/assets/icons/ok_logo.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/vk_logo.svg b/assets/icons/vk_logo.svg new file mode 100644 index 00000000..274b3486 --- /dev/null +++ b/assets/icons/vk_logo.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/assets/icons/yandex_logo.svg b/assets/icons/yandex_logo.svg new file mode 100644 index 00000000..51b51eee --- /dev/null +++ b/assets/icons/yandex_logo.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/lib/features/auth/presentation/pages/auth_screen.dart b/lib/features/auth/presentation/pages/auth_screen.dart index f9eeddf5..970ea8d3 100644 --- a/lib/features/auth/presentation/pages/auth_screen.dart +++ b/lib/features/auth/presentation/pages/auth_screen.dart @@ -9,6 +9,7 @@ import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/utils/image_constant.dart'; import 'package:eatvio_app/core/utils/toasts.dart'; import 'package:eatvio_app/core/websockets/ws_manager.dart'; +import 'package:eatvio_app/core/widgets/custom_divider.dart'; import 'package:eatvio_app/core/widgets/custom_text_form_field.dart'; import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart'; import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_state.dart'; @@ -119,6 +120,42 @@ class _AuthScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + // Social auth buttons at the top (outlined) + const Text('Авторизация через:', style: TextStyle(fontWeight: FontWeight.w600)), + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _SocialButton( + onTap: () => _onVkPressed(context), + backgroundColor: const Color(0xFF2787F5), + tooltip: 'VK ID', + child: SvgPicture.asset('assets/icons/vk_logo.svg', height: 24, width: 24), + ), + _SocialButton( + onTap: _onOkPressed, + backgroundColor: const Color(0xFFF58220), + tooltip: 'Одноклассники', + child: SvgPicture.asset('assets/icons/ok_logo.svg', height: 24, width: 24), + ), + _SocialButton( + onTap: _onMailRuPressed, + backgroundColor: const Color(0xFF005FF9), + tooltip: 'Mail.ru', + child: SvgPicture.asset('assets/icons/mail_logo.svg', height: 24, width: 24), + ), + _SocialButton( + onTap: _onYandexPressed, + backgroundColor: const Color(0xFFFFCC00), + tooltip: 'Яндекс ID', + child: SvgPicture.asset('assets/icons/yandex_logo.svg', height: 24, width: 24), + ), + ], + ), + const SizedBox(height: 16), + const CustomDivider(), + const SizedBox(height: 8), + const SizedBox(height: 16), Text(s.email), const SizedBox(height: 4), CustomTextFormField( @@ -168,52 +205,6 @@ class _AuthScreenState extends State { ), ], ), - // const SizedBox(height: 16), - 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( @@ -367,7 +358,6 @@ 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}); @@ -381,12 +371,16 @@ class _SocialButton extends StatelessWidget { return Tooltip( message: tooltip, child: InkWell( - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(20), 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)), + decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular(20), + border: Border.all(color: const Color(0xFFDBDBDB), width: 1), + ), child: Center(child: child), ), ), From 27376ca54d244b13d425b5c6c0b31fddc01a1231 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Tue, 30 Sep 2025 00:39:21 +0300 Subject: [PATCH 40/78] feat(auth): Issue-691 --- ENV_SETUP.md | 9 + lib/core/env_config.dart | 8 + .../auth/presentation/pages/auth_screen.dart | 67 +++++--- lib/features/auth/sdk/social_auth_facade.dart | 158 +++++++++++++----- lib/main.dart | 2 + 5 files changed, 184 insertions(+), 60 deletions(-) diff --git a/ENV_SETUP.md b/ENV_SETUP.md index 0873fb1c..092a3bd9 100644 --- a/ENV_SETUP.md +++ b/ENV_SETUP.md @@ -54,6 +54,15 @@ cp env.example .env - `GOOGLE_CHECK_URL` - URL для проверки соединения (Google) - `YANDEX_CHECK_URL` - URL для проверки соединения (Yandex) +### VK ID Configuration +- `VK_CLIENT_ID` - ID приложения VK (числовой) +- **Примечание**: VK_CLIENT_ID - это числовой "ID приложения" из консоли VK ID, НЕ "Защищённый ключ" или "Сервисный ключ доступа" + +### Yandex ID Configuration +- `YANDEX_CLIENT_ID` - ID приложения Yandex OAuth +- `YANDEX_CLIENT_SECRET` - секретный ключ приложения Yandex OAuth +- `YANDEX_REDIRECT_URI` - URI для перенаправления (должен совпадать с настройками в Yandex OAuth приложении) + ## Безопасность ⚠️ **ВАЖНО**: Файл `.env` добавлен в `.gitignore` и не должен попадать в репозиторий. Все секретные данные должны храниться только в этом файле. diff --git a/lib/core/env_config.dart b/lib/core/env_config.dart index a3404aeb..446bc65b 100644 --- a/lib/core/env_config.dart +++ b/lib/core/env_config.dart @@ -43,4 +43,12 @@ class EnvConfig { // Connection Check URLs static String get googleCheckUrl => dotenv.env['GOOGLE_CHECK_URL'] ?? ''; static String get yandexCheckUrl => dotenv.env['YANDEX_CHECK_URL'] ?? ''; + + // VK ID Configuration + static String get vkClientId => dotenv.env['VK_CLIENT_ID'] ?? ''; + + // Yandex ID Configuration + static String get yandexClientId => dotenv.env['YANDEX_CLIENT_ID'] ?? ''; + static String get yandexClientSecret => dotenv.env['YANDEX_CLIENT_SECRET'] ?? ''; + static String get yandexRedirectUri => dotenv.env['YANDEX_REDIRECT_URI'] ?? ''; } diff --git a/lib/features/auth/presentation/pages/auth_screen.dart b/lib/features/auth/presentation/pages/auth_screen.dart index 970ea8d3..5f9496ac 100644 --- a/lib/features/auth/presentation/pages/auth_screen.dart +++ b/lib/features/auth/presentation/pages/auth_screen.dart @@ -388,35 +388,64 @@ class _SocialButton extends StatelessWidget { } } -// ignore: unused_element 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) { - if (AppRouter.navigatorKey.currentContext != null) { - // Reuse success flow - // ignore: use_build_context_synchronously - await _reconnectAfterAuth(); + print('[VK] VK button pressed'); + try { + final accessToken = await SocialAuthFacade.signInWithVk(context: context); + print('[VK] Got access token: ${accessToken != null ? 'success' : 'null'}'); + if (accessToken == null) { + print('[VK] No access token received'); + return; } + final isAuthenticated = await sl().getTokenBySocial(provider: 'vk', accessToken: accessToken); + print('[VK] Authentication result: $isAuthenticated'); + if (isAuthenticated) { + if (AppRouter.navigatorKey.currentContext != null) { + // Reuse success flow + // ignore: use_build_context_synchronously + await _reconnectAfterAuth(); + } + } + } catch (e) { + print('[VK] Error during VK auth: $e'); } } 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(); + print('[OK] OK button pressed'); + try { + final accessToken = await SocialAuthFacade.signInWithOk(); + print('[OK] Got access token: ${accessToken != null ? 'success' : 'null'}'); + if (accessToken == null) { + print('[OK] No access token received'); + return; + } + final isAuthenticated = await sl().getTokenBySocial(provider: 'ok', accessToken: accessToken); + print('[OK] Authentication result: $isAuthenticated'); + if (isAuthenticated) { + await _reconnectAfterAuth(); + } + } catch (e) { + print('[OK] Error during OK auth: $e'); } } 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(); + print('[MailRu] MailRu button pressed'); + try { + final accessToken = await SocialAuthFacade.signInWithMailRu(); + print('[MailRu] Got access token: ${accessToken != null ? 'success' : 'null'}'); + if (accessToken == null) { + print('[MailRu] No access token received'); + return; + } + final isAuthenticated = await sl().getTokenBySocial(provider: 'mailru', accessToken: accessToken); + print('[MailRu] Authentication result: $isAuthenticated'); + if (isAuthenticated) { + await _reconnectAfterAuth(); + } + } catch (e) { + print('[MailRu] Error during MailRu auth: $e'); } } diff --git a/lib/features/auth/sdk/social_auth_facade.dart b/lib/features/auth/sdk/social_auth_facade.dart index e5761498..90d07e95 100644 --- a/lib/features/auth/sdk/social_auth_facade.dart +++ b/lib/features/auth/sdk/social_auth_facade.dart @@ -1,10 +1,9 @@ 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:eatvio_app/core/env_config.dart'; import 'package:vk_id/vk_id.dart'; import 'package:webview_flutter/webview_flutter.dart'; @@ -29,26 +28,52 @@ class SocialAuthFacade { } 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) { + final clientIdStr = EnvConfig.vkClientId.trim(); + print('[VK] VK_CLIENT_ID from EnvConfig: $clientIdStr'); + if (clientIdStr.isEmpty) { + print('[VK] VK_CLIENT_ID is not set. Configure it in .env'); return null; } try { + print('[VK] Creating VkIDController with clientId: $clientIdStr'); dynamic controller = VkIDController(clID: int.parse(clientIdStr)); final authUri = controller.generateAuthorizeLinkWithCodeVerifier(); + print('[VK] Generated auth URI: $authUri'); + + // Extract URL from MapEntry + String authUrl; + if (authUri is MapEntry) { + authUrl = authUri.value.toString(); + } else { + authUrl = authUri.toString(); + } + print('[VK] Extracted auth URL: $authUrl'); + final redirectScheme = 'vk$clientIdStr://'; - final redirectParams = await _presentOAuthWebView(context: context, initialUrl: authUri.toString(), redirectSchemePrefix: redirectScheme); - if (redirectParams == null) return null; + print('[VK] Redirect scheme: $redirectScheme'); + + final redirectParams = await _presentOAuthWebView(context: context, initialUrl: authUrl, redirectSchemePrefix: redirectScheme); + print('[VK] WebView returned params: $redirectParams'); + if (redirectParams == null) { + print('[VK] No redirect params received'); + return null; + } + final code = redirectParams['code']; final deviceId = redirectParams['device_id']; - if (code == null || deviceId == null) return null; + print('[VK] Code: $code, DeviceId: $deviceId'); + if (code == null || deviceId == null) { + print('[VK] Missing code or deviceId'); + 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 (_) { + // Instead of exchanging the code on-device (the sdk method is not available), + // return the authorization code so the backend can exchange it securely. + print('[VK] Returning authorization code to backend for exchange'); + return VkAuthResult(accessToken: code); + } catch (e) { + print('[VK] Error in signInWithVkAndFetchProfile: $e'); return null; } } @@ -66,10 +91,14 @@ class SocialAuthFacade { } static Future signInWithYandex() async { - 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']; + final clientId = EnvConfig.yandexClientId.trim(); + final redirectUri = EnvConfig.yandexRedirectUri.trim(); - if (clientId == null || clientId.isEmpty || redirectUri == null || redirectUri.isEmpty) { + print('[Yandex] Client ID: $clientId'); + print('[Yandex] Redirect URI: $redirectUri'); + + if (clientId.isEmpty || redirectUri.isEmpty) { + print('[Yandex] Missing YANDEX_CLIENT_ID or YANDEX_REDIRECT_URI in .env'); return null; } try { @@ -83,73 +112,120 @@ class SocialAuthFacade { } 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) { + final clientIdStr = EnvConfig.vkClientId.trim(); + if (clientIdStr.isEmpty) { return null; } try { + print('[VK] Provider flow start: $provider, clientId: $clientIdStr'); dynamic controller = VkIDController(clID: int.parse(clientIdStr)); - final authUri = controller.generateAuthorizeLinkWithCodeVerifier(provider: provider); + + // Try different approaches for provider parameter + dynamic authUri; + try { + // First try with provider as string + authUri = controller.generateAuthorizeLinkWithCodeVerifier(provider: provider); + print('[VK] [$provider] Generated auth URI with string provider: $authUri'); + } catch (e) { + print('[VK] [$provider] Failed with string provider, trying null: $e'); + try { + // Try with null provider + authUri = controller.generateAuthorizeLinkWithCodeVerifier(provider: null); + print('[VK] [$provider] Generated auth URI with null provider: $authUri'); + } catch (e2) { + print('[VK] [$provider] Failed with null provider, trying without provider: $e2'); + // Try without provider parameter + authUri = controller.generateAuthorizeLinkWithCodeVerifier(); + print('[VK] [$provider] Generated auth URI without provider: $authUri'); + } + } + + // Extract URL from MapEntry + String authUrl; + if (authUri is MapEntry) { + authUrl = authUri.value.toString(); + } else { + authUrl = authUri.toString(); + } + print('[VK] [$provider] Extracted auth URL: $authUrl'); + final redirectScheme = 'vk$clientIdStr://'; - final redirectParams = await _presentOAuthWebView(context: context, initialUrl: authUri.toString(), redirectSchemePrefix: redirectScheme); + print('[VK] [$provider] Redirect scheme: $redirectScheme'); + final redirectParams = await _presentOAuthWebView(context: context, initialUrl: authUrl, redirectSchemePrefix: redirectScheme); + print('[VK] [$provider] WebView returned params: $redirectParams'); if (redirectParams == null) return null; final code = redirectParams['code']; final deviceId = redirectParams['device_id']; + print('[VK] [$provider] Code: $code, DeviceId: $deviceId'); 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 the authorization code to be exchanged by the backend for provider flow + return code; + } catch (e) { + print('[VK] [$provider] Error in provider flow: $e'); 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; - } + // Token extraction is no longer needed because we return the authorization code + // to the backend for exchange. static Future?> _presentOAuthWebView({ required BuildContext context, required String initialUrl, required String redirectSchemePrefix, }) async { + print('[VK] WebView: Loading URL: $initialUrl'); + print('[VK] WebView: Redirect scheme: $redirectSchemePrefix'); + final controller = WebViewController() ..setJavaScriptMode(JavaScriptMode.unrestricted) ..setNavigationDelegate( NavigationDelegate( onNavigationRequest: (request) { final url = request.url; + print('[VK] WebView: Navigation to: $url'); if (url.startsWith(redirectSchemePrefix)) { + print('[VK] WebView: Redirect detected, parsing params'); final uri = Uri.parse(url); final params = {}..addAll(uri.queryParameters); + print('[VK] WebView: Parsed params: $params'); Navigator.of(context, rootNavigator: true).pop(params); return NavigationDecision.prevent; } return NavigationDecision.navigate; }, + onPageStarted: (url) { + print('[VK] WebView: Page started loading: $url'); + }, + onPageFinished: (url) { + print('[VK] WebView: Page finished loading: $url'); + }, + onWebResourceError: (error) { + print('[VK] WebView: Error: ${error.description}'); + }, ), - ) - ..loadRequest(Uri.parse(initialUrl)); + ); + + try { + await controller.loadRequest(Uri.parse(initialUrl)); + } catch (e) { + print('[VK] WebView: Failed to load URL: $e'); + return null; + } return showDialog>( context: context, barrierDismissible: false, builder: (_) { + const borderRadius = BorderRadius.all(Radius.circular(16)); return Dialog( insetPadding: const EdgeInsets.all(16), - child: SizedBox(height: 560, child: WebViewWidget(controller: controller)), + shape: const RoundedRectangleBorder(borderRadius: borderRadius), + elevation: 0, + child: ClipRRect( + borderRadius: borderRadius, + child: SizedBox(height: 560, child: WebViewWidget(controller: controller)), + ), ); }, ); diff --git a/lib/main.dart b/lib/main.dart index d716f3fa..e0492219 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -31,6 +31,8 @@ Future main() async { FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); await dotenv.load(fileName: ".env"); + print('[ENV] Loaded .env file'); + print('[ENV] VK_CLIENT_ID: ${dotenv.maybeGet('VK_CLIENT_ID')}'); await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); await FirebaseMessaging.instance.requestPermission(); From b65fca8116a90fde2241ae69f209df84e292ca14 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Tue, 30 Sep 2025 16:11:30 +0300 Subject: [PATCH 41/78] feat(auth): Issue-691 --- .../auth/presentation/pages/auth_screen.dart | 17 --- lib/features/auth/sdk/social_auth_facade.dart | 102 ++++++++---------- lib/main.dart | 2 - 3 files changed, 47 insertions(+), 74 deletions(-) diff --git a/lib/features/auth/presentation/pages/auth_screen.dart b/lib/features/auth/presentation/pages/auth_screen.dart index 5f9496ac..30ef29cf 100644 --- a/lib/features/auth/presentation/pages/auth_screen.dart +++ b/lib/features/auth/presentation/pages/auth_screen.dart @@ -389,63 +389,46 @@ class _SocialButton extends StatelessWidget { } void _onVkPressed(BuildContext context) async { - print('[VK] VK button pressed'); try { final accessToken = await SocialAuthFacade.signInWithVk(context: context); - print('[VK] Got access token: ${accessToken != null ? 'success' : 'null'}'); if (accessToken == null) { - print('[VK] No access token received'); return; } final isAuthenticated = await sl().getTokenBySocial(provider: 'vk', accessToken: accessToken); - print('[VK] Authentication result: $isAuthenticated'); if (isAuthenticated) { if (AppRouter.navigatorKey.currentContext != null) { - // Reuse success flow - // ignore: use_build_context_synchronously await _reconnectAfterAuth(); } } } catch (e) { - print('[VK] Error during VK auth: $e'); } } void _onOkPressed() async { - print('[OK] OK button pressed'); try { final accessToken = await SocialAuthFacade.signInWithOk(); - print('[OK] Got access token: ${accessToken != null ? 'success' : 'null'}'); if (accessToken == null) { - print('[OK] No access token received'); return; } final isAuthenticated = await sl().getTokenBySocial(provider: 'ok', accessToken: accessToken); - print('[OK] Authentication result: $isAuthenticated'); if (isAuthenticated) { await _reconnectAfterAuth(); } } catch (e) { - print('[OK] Error during OK auth: $e'); } } void _onMailRuPressed() async { - print('[MailRu] MailRu button pressed'); try { final accessToken = await SocialAuthFacade.signInWithMailRu(); - print('[MailRu] Got access token: ${accessToken != null ? 'success' : 'null'}'); if (accessToken == null) { - print('[MailRu] No access token received'); return; } final isAuthenticated = await sl().getTokenBySocial(provider: 'mailru', accessToken: accessToken); - print('[MailRu] Authentication result: $isAuthenticated'); if (isAuthenticated) { await _reconnectAfterAuth(); } } catch (e) { - print('[MailRu] Error during MailRu auth: $e'); } } diff --git a/lib/features/auth/sdk/social_auth_facade.dart b/lib/features/auth/sdk/social_auth_facade.dart index 90d07e95..74959498 100644 --- a/lib/features/auth/sdk/social_auth_facade.dart +++ b/lib/features/auth/sdk/social_auth_facade.dart @@ -29,51 +29,35 @@ class SocialAuthFacade { static Future signInWithVkAndFetchProfile({required BuildContext context}) async { final clientIdStr = EnvConfig.vkClientId.trim(); - print('[VK] VK_CLIENT_ID from EnvConfig: $clientIdStr'); if (clientIdStr.isEmpty) { - print('[VK] VK_CLIENT_ID is not set. Configure it in .env'); return null; } try { - print('[VK] Creating VkIDController with clientId: $clientIdStr'); dynamic controller = VkIDController(clID: int.parse(clientIdStr)); final authUri = controller.generateAuthorizeLinkWithCodeVerifier(); - print('[VK] Generated auth URI: $authUri'); - // Extract URL from MapEntry String authUrl; if (authUri is MapEntry) { authUrl = authUri.value.toString(); } else { authUrl = authUri.toString(); } - print('[VK] Extracted auth URL: $authUrl'); final redirectScheme = 'vk$clientIdStr://'; - print('[VK] Redirect scheme: $redirectScheme'); final redirectParams = await _presentOAuthWebView(context: context, initialUrl: authUrl, redirectSchemePrefix: redirectScheme); - print('[VK] WebView returned params: $redirectParams'); if (redirectParams == null) { - print('[VK] No redirect params received'); return null; } final code = redirectParams['code']; final deviceId = redirectParams['device_id']; - print('[VK] Code: $code, DeviceId: $deviceId'); if (code == null || deviceId == null) { - print('[VK] Missing code or deviceId'); return null; } - - // Instead of exchanging the code on-device (the sdk method is not available), - // return the authorization code so the backend can exchange it securely. - print('[VK] Returning authorization code to backend for exchange'); return VkAuthResult(accessToken: code); } catch (e) { - print('[VK] Error in signInWithVkAndFetchProfile: $e'); return null; } } @@ -94,20 +78,47 @@ class SocialAuthFacade { final clientId = EnvConfig.yandexClientId.trim(); final redirectUri = EnvConfig.yandexRedirectUri.trim(); - print('[Yandex] Client ID: $clientId'); - print('[Yandex] Redirect URI: $redirectUri'); - if (clientId.isEmpty || redirectUri.isEmpty) { - print('[Yandex] Missing YANDEX_CLIENT_ID or YANDEX_REDIRECT_URI in .env'); 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 (_) { + final context = AppRouter.navigatorKey.currentContext; + if (context == null) return null; + + final authorizeUrl = Uri.https('oauth.yandex.ru', '/authorize', { + 'response_type': 'token', + 'client_id': clientId, + 'redirect_uri': redirectUri, + 'force_confirm': 'yes', + }).toString(); + + final redirectSchemePrefix = redirectUri; + + final params = await _presentOAuthWebView(context: context, initialUrl: authorizeUrl, redirectSchemePrefix: redirectSchemePrefix); + + if (params == null) return null; + + final token = params['access_token']; + if (token != null && token.isNotEmpty) { + return token; + } + + final code = params['code']; + if (code != null && code.isNotEmpty) { + return code; + } + return null; + } catch (e) { + 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; + } } } @@ -117,26 +128,16 @@ class SocialAuthFacade { return null; } try { - print('[VK] Provider flow start: $provider, clientId: $clientIdStr'); dynamic controller = VkIDController(clID: int.parse(clientIdStr)); - // Try different approaches for provider parameter dynamic authUri; try { - // First try with provider as string authUri = controller.generateAuthorizeLinkWithCodeVerifier(provider: provider); - print('[VK] [$provider] Generated auth URI with string provider: $authUri'); } catch (e) { - print('[VK] [$provider] Failed with string provider, trying null: $e'); try { - // Try with null provider authUri = controller.generateAuthorizeLinkWithCodeVerifier(provider: null); - print('[VK] [$provider] Generated auth URI with null provider: $authUri'); } catch (e2) { - print('[VK] [$provider] Failed with null provider, trying without provider: $e2'); - // Try without provider parameter authUri = controller.generateAuthorizeLinkWithCodeVerifier(); - print('[VK] [$provider] Generated auth URI without provider: $authUri'); } } @@ -147,61 +148,53 @@ class SocialAuthFacade { } else { authUrl = authUri.toString(); } - print('[VK] [$provider] Extracted auth URL: $authUrl'); - final redirectScheme = 'vk$clientIdStr://'; - print('[VK] [$provider] Redirect scheme: $redirectScheme'); final redirectParams = await _presentOAuthWebView(context: context, initialUrl: authUrl, redirectSchemePrefix: redirectScheme); - print('[VK] [$provider] WebView returned params: $redirectParams'); if (redirectParams == null) return null; final code = redirectParams['code']; final deviceId = redirectParams['device_id']; - print('[VK] [$provider] Code: $code, DeviceId: $deviceId'); if (code == null || deviceId == null) return null; - // Return the authorization code to be exchanged by the backend for provider flow return code; } catch (e) { - print('[VK] [$provider] Error in provider flow: $e'); return null; } } - // Token extraction is no longer needed because we return the authorization code - // to the backend for exchange. - static Future?> _presentOAuthWebView({ required BuildContext context, required String initialUrl, required String redirectSchemePrefix, }) async { - print('[VK] WebView: Loading URL: $initialUrl'); - print('[VK] WebView: Redirect scheme: $redirectSchemePrefix'); - final controller = WebViewController() ..setJavaScriptMode(JavaScriptMode.unrestricted) ..setNavigationDelegate( NavigationDelegate( onNavigationRequest: (request) { final url = request.url; - print('[VK] WebView: Navigation to: $url'); if (url.startsWith(redirectSchemePrefix)) { - print('[VK] WebView: Redirect detected, parsing params'); final uri = Uri.parse(url); final params = {}..addAll(uri.queryParameters); - print('[VK] WebView: Parsed params: $params'); + if (uri.fragment.isNotEmpty) { + final fragPairs = uri.fragment.split('&'); + for (final pair in fragPairs) { + final eq = pair.indexOf('='); + if (eq > 0 && eq < pair.length - 1) { + final key = Uri.decodeComponent(pair.substring(0, eq)); + final value = Uri.decodeComponent(pair.substring(eq + 1)); + params[key] = value; + } + } + } Navigator.of(context, rootNavigator: true).pop(params); return NavigationDecision.prevent; } return NavigationDecision.navigate; }, onPageStarted: (url) { - print('[VK] WebView: Page started loading: $url'); }, onPageFinished: (url) { - print('[VK] WebView: Page finished loading: $url'); }, onWebResourceError: (error) { - print('[VK] WebView: Error: ${error.description}'); }, ), ); @@ -209,7 +202,6 @@ class SocialAuthFacade { try { await controller.loadRequest(Uri.parse(initialUrl)); } catch (e) { - print('[VK] WebView: Failed to load URL: $e'); return null; } diff --git a/lib/main.dart b/lib/main.dart index e0492219..d716f3fa 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -31,8 +31,6 @@ Future main() async { FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); await dotenv.load(fileName: ".env"); - print('[ENV] Loaded .env file'); - print('[ENV] VK_CLIENT_ID: ${dotenv.maybeGet('VK_CLIENT_ID')}'); await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); await FirebaseMessaging.instance.requestPermission(); From f1945202fe7b476e21932deef5114b377c65c798 Mon Sep 17 00:00:00 2001 From: misha Date: Tue, 30 Sep 2025 17:33:30 +0300 Subject: [PATCH 42/78] feat(internetConnection): changed logic for internet connection checking and displaying state --- .../internet_connection_cubit.dart | 73 +++++++++++++++++++ .../internet_connection_state.dart | 13 ++++ lib/core/locator_service.dart | 3 + lib/core/utils/toasts.dart | 14 +++- lib/main.dart | 8 +- 5 files changed, 105 insertions(+), 6 deletions(-) create mode 100644 lib/core/internet_connection_cubit/internet_connection_cubit.dart create mode 100644 lib/core/internet_connection_cubit/internet_connection_state.dart diff --git a/lib/core/internet_connection_cubit/internet_connection_cubit.dart b/lib/core/internet_connection_cubit/internet_connection_cubit.dart new file mode 100644 index 00000000..fa9a3527 --- /dev/null +++ b/lib/core/internet_connection_cubit/internet_connection_cubit.dart @@ -0,0 +1,73 @@ +import 'dart:async'; +import 'dart:developer'; + +import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:eatvio_app/core/env_config.dart'; +import 'package:eatvio_app/core/internet_connection_cubit/internet_connection_state.dart'; +import 'package:eatvio_app/core/utils/toasts.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:http/http.dart' as http; + +class InternetConnectionCubit extends Cubit { + InternetConnectionCubit() : super(InternetConnectionState()) { + checkConnection(); + _connectivitySubscription = _connectivity.onConnectivityChanged.listen(_updateAccessStatus); + _listener = AppLifecycleListener( + onResume: () async { + _connectivitySubscription.resume(); + await checkConnection(); + }, + onHide: _connectivitySubscription.pause, + onPause: _connectivitySubscription.pause, + ); + } + + final Connectivity _connectivity = Connectivity(); + late StreamSubscription> _connectivitySubscription; + late final AppLifecycleListener _listener; + + Future checkConnection() async { + late List result; + + try { + result = await _connectivity.checkConnectivity(); + } on PlatformException catch (e) { + log('Couldn\'t check connectivity status', error: e); + return; + } + + return _updateAccessStatus(result); + } + + Future _updateAccessStatus(List result) async { + final hasInternet = await _checkInternetAccess(result); + showInternetConnectionToast(); + emit(state.copyWith(isInternetAvailable: hasInternet)); + } + + Future _checkInternetAccess(List result) async { + if (result.contains(ConnectivityResult.none)) { + return false; + } + + final uris = [Uri.parse(EnvConfig.googleCheckUrl), Uri.parse(EnvConfig.yandexCheckUrl), Uri.parse(EnvConfig.devBaseUrl), Uri.parse(EnvConfig.baseUrl)]; + for (final uri in uris) { + try { + final response = await http.get(uri).timeout(const Duration(seconds: 3)); + return response.statusCode >= 100 && response.statusCode < 600; + } catch (e) { + continue; + } + } + return false; + } + + @override + Future close() { + _connectivitySubscription.cancel(); + _listener.dispose(); + return super.close(); + } +} diff --git a/lib/core/internet_connection_cubit/internet_connection_state.dart b/lib/core/internet_connection_cubit/internet_connection_state.dart new file mode 100644 index 00000000..95c831e1 --- /dev/null +++ b/lib/core/internet_connection_cubit/internet_connection_state.dart @@ -0,0 +1,13 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'internet_connection_state.freezed.dart'; + + +@freezed +abstract class InternetConnectionState with _$InternetConnectionState { + + factory InternetConnectionState({ + @Default(true) bool isInternetAvailable, + }) = _InternetConnectionState; + +} \ No newline at end of file diff --git a/lib/core/locator_service.dart b/lib/core/locator_service.dart index 37713f29..ffe747a9 100644 --- a/lib/core/locator_service.dart +++ b/lib/core/locator_service.dart @@ -5,6 +5,7 @@ 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/internet_connection_cubit/internet_connection_cubit.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'; @@ -45,6 +46,7 @@ import 'package:eatvio_app/features/blog_post_create/services/blog_post_service. import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/new_messages_cubit/new_messages_cubit.dart'; import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/consultant_finances/consultant_finances_cubit.dart'; import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/user_purchases/user_purchases_cubit.dart'; +import 'package:eatvio_app/features/internet_connection_checker/internet_connection_checker_screen.dart'; import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_cubit.dart'; import 'package:eatvio_app/features/menu/presentation/bloc/notification_settings_cubit/notification_settings_cubit.dart'; import 'package:eatvio_app/features/menu/presentation/repositories/blocked_users_repository.dart'; @@ -128,6 +130,7 @@ Future init() async { sl.registerLazySingleton(() => NotificationsCubit(wsManager: sl(), notificationsRepository: sl())); sl.registerLazySingleton(() => ProfileAvatarCubit(profileRepository: sl())); sl.registerLazySingleton(() => ChatsCubit(chatRepository: sl(), wsManager: sl(), profileRepository: sl())); + sl.registerLazySingleton(() => InternetConnectionCubit()); // Cubit factories sl.registerFactory(() => FeedCubit(feedRepository: sl())); diff --git a/lib/core/utils/toasts.dart b/lib/core/utils/toasts.dart index 3d4fda0d..4683cb86 100644 --- a/lib/core/utils/toasts.dart +++ b/lib/core/utils/toasts.dart @@ -38,4 +38,16 @@ void showSuccessToast(String message) { textColor: appTheme.whiteA700, fontSize: 16.0, ); -} \ No newline at end of file +} + +void showInternetConnectionToast() { + Fluttertoast.showToast( + msg: 'Проверьте подключение к интернету', + toastLength: Toast.LENGTH_LONG, + gravity: ToastGravity.TOP, + timeInSecForIosWeb: 3, + backgroundColor: appTheme.red, + textColor: Colors.white, + fontSize: 16.0, + ); +} diff --git a/lib/main.dart b/lib/main.dart index d716f3fa..c9e9a3d6 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,6 @@ import 'dart:io' as io; import 'package:eatvio_app/core/app_router.dart'; +import 'package:eatvio_app/core/internet_connection_cubit/internet_connection_cubit.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'; @@ -9,7 +10,6 @@ import 'package:eatvio_app/features/notifications/presentation/notifications_cub import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart'; -import 'package:eatvio_app/features/internet_connection_checker/internet_connection_checker_screen.dart'; import 'package:eatvio_app/firebase_options.dart'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:firebase_core/firebase_core.dart'; @@ -58,20 +58,18 @@ class MyApp extends StatelessWidget { return MultiBlocProvider( providers: [ BlocProvider(create: (context) => sl()), + BlocProvider(create: (context) => sl()), BlocProvider(create: (context) => sl()..getNotifications()), BlocProvider(create: (context) => sl()..loadFeedBlogPostList()), BlocProvider(create: (context) => sl()..getMyProfile()), BlocProvider(create: (context) => sl()..getChatRoomsWithLoading()), + BlocProvider(create: (context) => sl()), ], child: Portal( child: KeyboardDismisser( child: MaterialApp.router( theme: ThemeHelper().themeData(), routerConfig: AppRouter.appRouter, - builder: (context, child) => BlocProvider( - create: (context) => sl(), - child: InternetConnectionCheckerScreen(child: child!), - ), debugShowCheckedModeBanner: false, localizationsDelegates: const [ S.delegate, From d18ad865909c556a7807053e680a1de484141bff Mon Sep 17 00:00:00 2001 From: misha Date: Wed, 1 Oct 2025 17:24:48 +0300 Subject: [PATCH 43/78] feat(internetConnection): fixed yandex sign in compilation --- ...otlin-compiler-10039299475187077818.salive | 0 ios/Runner/AppDelegate.swift | 19 ++++++++++++++++++- ios/Runner/Info.plist | 19 +++++++++++++++++++ ios/Runner/Runner.entitlements | 1 + lib/core/const.dart | 2 +- .../internet_connection_cubit.dart | 2 +- lib/main.dart | 2 +- 7 files changed, 41 insertions(+), 4 deletions(-) delete mode 100644 android/.kotlin/sessions/kotlin-compiler-10039299475187077818.salive diff --git a/android/.kotlin/sessions/kotlin-compiler-10039299475187077818.salive b/android/.kotlin/sessions/kotlin-compiler-10039299475187077818.salive deleted file mode 100644 index e69de29b..00000000 diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index b6363034..fc70b0b0 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -1,5 +1,6 @@ -import UIKit import Flutter +import UIKit +import YandexLoginSDK @main @objc class AppDelegate: FlutterAppDelegate { @@ -10,4 +11,20 @@ import Flutter GeneratedPluginRegistrant.register(with: self) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } + + @available(iOS 8.0, *) + func application( + _ application: UIApplication, continue userActivity: NSUserActivity, + restorationHandler: @escaping ([Any]?) -> Void + ) -> Bool { + YXLSdk.shared.processUserActivity(userActivity) + return true + } + + override public func application( + _ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:] + ) -> Bool { + return YXLSdk.shared.handleOpen( + url, sourceApplication: options[UIApplication.OpenURLOptionsKey.sourceApplication] as? String) + } } diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 7e82c5b2..3275dbdb 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -67,5 +67,24 @@ UIViewControllerBasedStatusBarAppearance + LSApplicationQueriesSchemes + + yandexauth + yandexauth2 + yandexauth4 + + YAClientId + 5399b03636104c6d920b87c1ad329254 + CFBundleURLTypes + + + CFBundleURLName + YandexLoginSDK + CFBundleURLSchemes + + yx5399b03636104c6d920b87c1ad329254 + + + diff --git a/ios/Runner/Runner.entitlements b/ios/Runner/Runner.entitlements index 7b8ca2a8..4ed4ad00 100644 --- a/ios/Runner/Runner.entitlements +++ b/ios/Runner/Runner.entitlements @@ -8,6 +8,7 @@ applinks:eatvio.ru applinks:www.eatvio.ru + applinks:5399b03636104c6d920b87c1ad329254.oauth.yandex.ru 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/core/internet_connection_cubit/internet_connection_cubit.dart b/lib/core/internet_connection_cubit/internet_connection_cubit.dart index fa9a3527..67427c6c 100644 --- a/lib/core/internet_connection_cubit/internet_connection_cubit.dart +++ b/lib/core/internet_connection_cubit/internet_connection_cubit.dart @@ -43,7 +43,7 @@ class InternetConnectionCubit extends Cubit { Future _updateAccessStatus(List result) async { final hasInternet = await _checkInternetAccess(result); - showInternetConnectionToast(); + if (!hasInternet) showInternetConnectionToast(); emit(state.copyWith(isInternetAvailable: hasInternet)); } diff --git a/lib/main.dart b/lib/main.dart index c9e9a3d6..0343afd2 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -63,7 +63,7 @@ class MyApp extends StatelessWidget { BlocProvider(create: (context) => sl()..loadFeedBlogPostList()), BlocProvider(create: (context) => sl()..getMyProfile()), BlocProvider(create: (context) => sl()..getChatRoomsWithLoading()), - BlocProvider(create: (context) => sl()), + BlocProvider(create: (context) => sl(), lazy: false), ], child: Portal( child: KeyboardDismisser( From 28658521240bcef909570070b18019bbb6568d29 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Wed, 1 Oct 2025 23:59:19 +0300 Subject: [PATCH 44/78] fix: refactor --- lib/core/app_config.dart | 53 +++++ lib/core/app_initializer.dart | 68 ++++++ lib/core/di/cubit_registrations.dart | 162 +++++++++++++ lib/core/di/external_registrations.dart | 33 +++ lib/core/di/repository_registrations.dart | 88 +++++++ lib/core/di/service_registrations.dart | 88 +++++++ lib/core/locator_service.dart | 276 ++-------------------- lib/main.dart | 68 +----- 8 files changed, 520 insertions(+), 316 deletions(-) create mode 100644 lib/core/app_config.dart create mode 100644 lib/core/app_initializer.dart create mode 100644 lib/core/di/cubit_registrations.dart create mode 100644 lib/core/di/external_registrations.dart create mode 100644 lib/core/di/repository_registrations.dart create mode 100644 lib/core/di/service_registrations.dart diff --git a/lib/core/app_config.dart b/lib/core/app_config.dart new file mode 100644 index 00000000..554a326f --- /dev/null +++ b/lib/core/app_config.dart @@ -0,0 +1,53 @@ +import 'package:eatvio_app/core/locator_service.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'; +import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart'; +import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart'; +import 'package:eatvio_app/features/internet_connection_checker/internet_connection_checker_screen.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; + +/// Конфигурация приложения +class AppConfig { + /// Список глобальных BlocProvider'ов + static List get globalBlocProviders => [ + BlocProvider(create: (context) => sl()), + BlocProvider( + create: (context) => sl()..getNotifications(), + ), + BlocProvider( + create: (context) => sl()..loadFeedBlogPostList(), + ), + BlocProvider( + create: (context) => sl()..getMyProfile(), + ), + BlocProvider( + create: (context) => sl()..getChatRoomsWithLoading(), + ), + ]; + + /// Делегаты локализации + static const List localizationDelegates = [ + S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ]; + + /// Поддерживаемые локали + static const List supportedLocales = [ + Locale.fromSubtags(languageCode: 'ru'), + ]; + + /// Создание обертки для проверки интернет-соединения + static Widget createInternetConnectionWrapper(Widget child) { + return BlocProvider( + create: (context) => sl(), + child: InternetConnectionCheckerScreen(child: child), + ); + } +} diff --git a/lib/core/app_initializer.dart b/lib/core/app_initializer.dart new file mode 100644 index 00000000..a3798a34 --- /dev/null +++ b/lib/core/app_initializer.dart @@ -0,0 +1,68 @@ +import 'dart:io' as io; +import 'package:eatvio_app/core/env_config.dart'; +import 'package:eatvio_app/core/locator_service.dart' as di; +import 'package:eatvio_app/firebase_options.dart'; +import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_native_splash/flutter_native_splash.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:appmetrica_plugin/appmetrica_plugin.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; + +/// Класс для инициализации приложения +class AppInitializer { + /// Инициализация всех сервисов приложения + static Future initialize() async { + await _initializeSentry(); + await _initializeEnvironment(); + await _initializeFirebase(); + await _initializeDependencyInjection(); + await _initializeDebugMode(); + await _initializeAnalytics(); + } + + /// Инициализация Sentry + static Future _initializeSentry() async { + final widgetsBinding = SentryWidgetsFlutterBinding.ensureInitialized(); + FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); + } + + /// Загрузка переменных окружения + static Future _initializeEnvironment() async { + await dotenv.load(fileName: ".env"); + } + + /// Инициализация Firebase + static Future _initializeFirebase() async { + await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); + await FirebaseMessaging.instance.requestPermission(); + } + + /// Инициализация системы внедрения зависимостей + static Future _initializeDependencyInjection() async { + await di.init(); + } + + /// Настройка режима отладки + static Future _initializeDebugMode() async { + if (kDebugMode) { + io.HttpClient.enableTimelineLogging = true; + } + } + + /// Инициализация аналитики + static Future _initializeAnalytics() async { + AppMetrica.activate(AppMetricaConfig(EnvConfig.appMetricaApiKey)); + AppMetrica.reportEvent('Application started'); + } + + /// Запуск приложения с Sentry + static Future runAppWithSentry(VoidCallback appRunner) async { + await SentryFlutter.init((options) { + options.dsn = EnvConfig.sentryDsn; + options.tracesSampleRate = 1.0; + options.debug = false; + }, appRunner: appRunner); + } +} diff --git a/lib/core/di/cubit_registrations.dart b/lib/core/di/cubit_registrations.dart new file mode 100644 index 00000000..efd0dbd2 --- /dev/null +++ b/lib/core/di/cubit_registrations.dart @@ -0,0 +1,162 @@ +import 'package:eatvio_app/core/views/cubit/views_cubit.dart'; +import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart'; +import 'package:eatvio_app/features/auth/presentation/bloc/reset_password_cubit/reset_password_cubit.dart'; +import 'package:eatvio_app/features/blog_post/presentation/cubit/blog_post_cubit.dart'; +import 'package:eatvio_app/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_cubit.dart'; +import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/new_messages_cubit/new_messages_cubit.dart'; +import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; +import 'package:eatvio_app/features/calendar/presentation/bloc/calendar_cubit.dart'; +import 'package:eatvio_app/features/cancel_plan_bottom_sheet/cubit/cancel_plan_cubit.dart'; +import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart'; +import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart'; +import 'package:eatvio_app/features/complaining/presentation/cubit/complaining_cubit.dart'; +import 'package:eatvio_app/features/consultant/become_consultant/presentation/bloc/register_consultant_cubit/register_consultant_cubit.dart'; +import 'package:eatvio_app/features/consultant_rating/presentation/cubit/consultant_rating_cubit.dart'; +import 'package:eatvio_app/features/consultations/presentation/cubit/consultations_cubit.dart'; +import 'package:eatvio_app/features/create_diet/presentation/cubit/create_diet_cubit.dart'; +import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_cubit.dart'; +import 'package:eatvio_app/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart'; +import 'package:eatvio_app/features/diary/presentation/bloc/consultant_comments_cubit/consultant_comments_cubit.dart'; +import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart'; +import 'package:eatvio_app/features/diets/presentation/cubit/about_diet_cubit.dart'; +import 'package:eatvio_app/features/feed/presentation/bloc/consultants_cubit/consultants_cubit.dart'; +import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart'; +import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart'; +import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_cubit.dart'; +import 'package:eatvio_app/features/feed/presentation/bloc/profile_cubit/profile_cubit.dart'; +import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes_cubit.dart'; +import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/archive/consultations_archive_cubit.dart'; +import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/consultant_finances/consultant_finances_cubit.dart'; +import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/user_purchases/user_purchases_cubit.dart'; +import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_cubit.dart'; +import 'package:eatvio_app/features/menu/presentation/bloc/notification_settings_cubit/notification_settings_cubit.dart'; +import 'package:eatvio_app/features/my_diets/presentation/cubit/my_diets_cubit.dart'; +import 'package:eatvio_app/features/my_finance/presentation/cubit/user_cards_cubit/user_cards_cubit.dart'; +import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_cubit.dart'; +import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart'; +import 'package:eatvio_app/features/order_info/cubit/order_info_cubit.dart'; +import 'package:eatvio_app/features/plans/presentation/cubit/about_plan_cubit.dart'; +import 'package:eatvio_app/features/profile/presentation/bloc/plans_cubit/plans_cubit.dart' as profile_plans; +import 'package:eatvio_app/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart'; +import 'package:eatvio_app/features/profile/presentation/bloc/sub_cubit/sub_cubit.dart'; +import 'package:eatvio_app/features/recipe/presentation/cubit/about_recipe_cubit.dart'; +import 'package:eatvio_app/features/recipe/presentation/cubit/my_recipes_cubit/my_recieps_cubit.dart'; +import 'package:eatvio_app/features/registration/presentation/bloc/registration_cubit/registration_cubit.dart'; +import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart'; +import 'package:eatvio_app/features/settings/presentation/bloc/settings_cubit/settings_cubit.dart'; +import 'package:eatvio_app/features/user_details/presentation/cubit/user_details_cubit.dart'; +import 'package:eatvio_app/core/comments/comments_cubit/comments_cubit.dart'; +import 'package:get_it/get_it.dart'; + +/// Регистрация всех Cubit'ов в системе внедрения зависимостей +class CubitRegistrations { + static final sl = GetIt.instance; + + /// Регистрация всех Cubit'ов + static void registerAll() { + _registerSingletonCubits(); + _registerFactoryCubits(); + } + + /// Регистрация Cubit'ов как синглтонов + static void _registerSingletonCubits() { + sl.registerLazySingleton(() => NewMessagesCubit()); + sl.registerLazySingleton( + () => NotificationsCubit(wsManager: sl(), notificationsRepository: sl()), + ); + sl.registerLazySingleton( + () => ProfileAvatarCubit(profileRepository: sl()), + ); + sl.registerLazySingleton( + () => ChatsCubit(chatRepository: sl(), wsManager: sl(), profileRepository: sl()), + ); + } + + /// Регистрация Cubit'ов как фабрик + static void _registerFactoryCubits() { + // Auth related cubits + sl.registerFactory(() => AuthCubit(authRepository: sl())); + sl.registerFactory(() => RegistrationCubit(registrationRepository: sl())); + sl.registerFactory(() => ResetPasswordCubit(resetPasswordRepository: sl())); + sl.registerFactory(() => VerifyEmailCubit(profileRepository: sl())); + + // Feed related cubits + sl.registerFactory(() => FeedCubit(feedRepository: sl())); + sl.registerFactory( + () => RecipesCubit(feedRepository: sl(), recipeRepository: sl(), aboutRecipeRepository: sl()), + ); + sl.registerFactory(() => DietsCubit(feedRepository: sl(), dietsRepository: sl())); + sl.registerFactory(() => PlansCubit(feedRepository: sl(), planRepository: sl())); + sl.registerFactory(() => ConsultantsCubit(feedRepository: sl())); + sl.registerFactory(() => CommentsCubit(feedRepository: sl(), commentsRepository: sl())); + + // Chat related cubits + sl.registerFactory( + () => ChatRoomCubit(cardsRepository: sl(), chatRepository: sl(), wsManager: sl()), + ); + + // Profile related cubits + sl.registerFactory( + () => ProfileCubit(profileRepository: sl(), chatRepository: sl(), consultantRepository: sl()), + ); + sl.registerFactory(() => profile_plans.PlansCubit(plansRepository: sl())); + sl.registerFactory(() => SubCubit(profileRepository: sl())); + + // Content creation cubits + sl.registerFactory( + () => CreateDietCubit(profileRepository: sl(), createDietRepository: sl(), glossaryRepository: sl()), + ); + sl.registerFactory(() => CreatePlanCubit(createPlanRepository: sl())); + sl.registerFactory( + () => CreateRecipeCubit(recipeRepository: sl(), glossaryRepository: sl()), + ); + sl.registerFactory(() => BlogPostCreateCubit(blogPostRepository: sl())); + + // Content viewing cubits + sl.registerFactory(() => AboutDietCubit(dietsRepository: sl(), profileRepository: sl())); + sl.registerFactory(() => AboutPlanCubit(planRepository: sl())); + sl.registerFactory(() => AboutRecipeCubit(recipeRepository: sl(), profileRepository: sl())); + sl.registerFactory(() => BlogPostCubit(sl())); + + // User content cubits + sl.registerFactory(() => MyDietsCubit(myDietsRepository: sl(), createDietRepository: sl())); + sl.registerFactory(() => MyPlansCubit(myPlansRepository: sl())); + sl.registerFactory(() => MyRecipesCubit(myRecipesRepository: sl(), aboutRecipeRepository: sl())); + + // Other feature cubits + sl.registerFactory(() => CalendarCubit(calendarRepository: sl())); + sl.registerFactory( + () => DiaryCubit( + chatRepository: sl(), + diaryRepository: sl(), + glossaryRepository: sl(), + profileRepository: sl(), + dietRepository: sl(), + settingsRepository: sl(), + ), + ); + sl.registerFactory( + () => SettingsCubit( + profileRepository: sl(), + settingsRepository: sl(), + consultantRepository: sl(), + consultationsRepository: sl(), + ), + ); + sl.registerFactory(() => RegisterConsultantCubit(consultantRepository: sl())); + sl.registerFactory(() => ComplainingCubit(repository: sl())); + sl.registerFactory(() => UserCardsCubit(cardsRepository: sl())); + sl.registerFactory(() => OrderInfoCubit(chatRepository: sl())); + sl.registerFactory(() => ConsultationsCubit(consultationsRepository: sl())); + sl.registerFactory(() => ConsultantCommentsCubit(diaryRepository: sl())); + sl.registerFactory(() => CancelPlanCubit(cancelPlanRepository: sl())); + sl.registerFactory(() => ConsultationsArchiveCubit(consultationsRepository: sl())); + sl.registerFactory(() => ConsultantFinancesCubit(consultationsRepository: sl())); + sl.registerFactory(() => UserPurchasesCubit(consultationsRepository: sl())); + sl.registerFactory(() => BlockedUsersCubit(blockedUsersRepository: sl())); + sl.registerFactory(() => NotificationSettingsCubit(notificationSettingsRepository: sl())); + sl.registerFactory(() => ConsultantRatingCubit(sl())); + sl.registerFactory(() => ViewsCubit(viewsRepository: sl())); + sl.registerFactory(() => UserDetailsCubit(profileRepository: sl())); + } +} diff --git a/lib/core/di/external_registrations.dart b/lib/core/di/external_registrations.dart new file mode 100644 index 00000000..38b3caa0 --- /dev/null +++ b/lib/core/di/external_registrations.dart @@ -0,0 +1,33 @@ +import 'package:dio/dio.dart'; +import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/dio/phone_verify_error_interceptor.dart'; +import 'package:eatvio_app/core/interceptors/token_interceptor.dart'; +import 'package:get_it/get_it.dart'; + +/// Регистрация внешних зависимостей в системе внедрения зависимостей +class ExternalRegistrations { + static final sl = GetIt.instance; + + /// Регистрация всех внешних зависимостей + static void registerAll() { + _registerDio(); + } + + /// Регистрация Dio HTTP клиента + static void _registerDio() { + sl.registerLazySingleton( + () => Dio( + BaseOptions( + baseUrl: baseUrl, + headers: { + 'accept': 'application/json', + 'content-type': 'application/json', + }, + ), + )..interceptors.addAll([ + TokenInterceptor(), + PhoneVerifyErrorInterceptor(), + ]), + ); + } +} diff --git a/lib/core/di/repository_registrations.dart b/lib/core/di/repository_registrations.dart new file mode 100644 index 00000000..6ce258aa --- /dev/null +++ b/lib/core/di/repository_registrations.dart @@ -0,0 +1,88 @@ +import 'package:eatvio_app/core/comments/repositories/comments_repository.dart'; +import 'package:eatvio_app/core/views/repository/views_repository.dart'; +import 'package:eatvio_app/features/auth/repositories/auth_repository.dart'; +import 'package:eatvio_app/features/auth/repositories/reset_password_repository.dart'; +import 'package:eatvio_app/features/blog_post_create/repositories/blog_post_repository.dart'; +import 'package:eatvio_app/features/calendar/repositories/calendar_repository.dart'; +import 'package:eatvio_app/features/cancel_plan_bottom_sheet/repository/cancel_plan_repository.dart'; +import 'package:eatvio_app/features/chat/repositories/chat_repository.dart'; +import 'package:eatvio_app/features/complaining/repositories/complaining_repository.dart'; +import 'package:eatvio_app/features/consultant/become_consultant/repositories/consultant_repository.dart'; +import 'package:eatvio_app/features/consultant_rating/repositories/consultant_rating_repository.dart'; +import 'package:eatvio_app/features/consultations/repositories/consultations_repository.dart'; +import 'package:eatvio_app/features/create_diet/repositories/create_diet_repository.dart'; +import 'package:eatvio_app/features/create_plan/repositories/create_plan_repository.dart'; +import 'package:eatvio_app/features/create_recipe/reporitories/recipe_repository.dart'; +import 'package:eatvio_app/features/diary/repositories/diary_repository.dart'; +import 'package:eatvio_app/features/diets/repositories/diet_repository.dart'; +import 'package:eatvio_app/features/feed/repositories/feed_repository.dart'; +import 'package:eatvio_app/features/glossary/repositories/glossary_repository.dart'; +import 'package:eatvio_app/features/menu/presentation/repositories/blocked_users_repository.dart'; +import 'package:eatvio_app/features/menu/repositories/notification_settings_repository.dart'; +import 'package:eatvio_app/features/my_diets/repositories/my_diets_repository.dart'; +import 'package:eatvio_app/features/my_finance/repositories/cards_repository.dart'; +import 'package:eatvio_app/features/my_plans/repositories/my_diets_repository.dart'; +import 'package:eatvio_app/features/notifications/repository/notifications_repository.dart'; +import 'package:eatvio_app/features/plans/repositories/plan_repository.dart'; +import 'package:eatvio_app/features/plans/repositories/plans_repository.dart'; +import 'package:eatvio_app/features/profile/repositories/profile_repository.dart'; +import 'package:eatvio_app/features/recipe/repositories/about_recipe_repository.dart'; +import 'package:eatvio_app/features/recipe/repositories/my_recipes_repository.dart'; +import 'package:eatvio_app/features/registration/repositories/registration_repository.dart'; +import 'package:eatvio_app/features/settings/repositories/service_repository.dart'; +import 'package:get_it/get_it.dart'; + +/// Регистрация всех Repository в системе внедрения зависимостей +class RepositoryRegistrations { + static final sl = GetIt.instance; + + /// Регистрация всех Repository + static void registerAll() { + _registerSingletonRepositories(); + _registerFactoryRepositories(); + } + + /// Регистрация Repository как синглтонов + static void _registerSingletonRepositories() { + // Core repositories + sl.registerLazySingleton(() => ChatRepository(chatService: sl())); + sl.registerLazySingleton(() => FeedRepository(feedService: sl())); + sl.registerLazySingleton(() => ProfileRepository(profileService: sl())); + sl.registerLazySingleton(() => CalendarRepository(calendarService: sl())); + sl.registerLazySingleton(() => SettingsRepository(settingsService: sl())); + sl.registerLazySingleton(() => ConsultantRepository(consultantService: sl())); + sl.registerLazySingleton(() => DiaryRepository(diaryService: sl())); + sl.registerLazySingleton(() => GlossaryRepository(glossaryService: sl())); + sl.registerLazySingleton(() => DietRepository(dietsService: sl())); + sl.registerLazySingleton(() => PlansRepository(plansService: sl())); + sl.registerLazySingleton(() => PlanRepository(planService: sl())); + sl.registerLazySingleton(() => AboutRecipeRepository(recipeService: sl())); + sl.registerLazySingleton(() => ComplainingRepository(service: sl())); + sl.registerLazySingleton(() => AuthRepository(authService: sl())); + sl.registerLazySingleton(() => CardsRepository(cardsService: sl())); + sl.registerLazySingleton(() => ConsultationsRepository(consultationService: sl())); + sl.registerLazySingleton(() => CancelPlanRepository(cancelService: sl())); + sl.registerLazySingleton(() => CreatePlanRepository(createPlanService: sl())); + sl.registerLazySingleton(() => NotificationsRepository(notificationsService: sl())); + sl.registerLazySingleton(() => BlockedUsersRepository(blockedUserService: sl())); + sl.registerLazySingleton(() => CommentsRepository(service: sl())); + sl.registerLazySingleton(() => NotificationSettingsRepository(sl())); + + // Content repositories + sl.registerLazySingleton(() => BlogPostRepository(blogPostService: sl())); + sl.registerLazySingleton(() => CreateDietRepository(createDietService: sl())); + sl.registerLazySingleton(() => MyDietsRepository(myDietsService: sl())); + sl.registerLazySingleton(() => MyPlansRepository(myPlansService: sl())); + + // Auth repositories + sl.registerLazySingleton(() => RegistrationRepository(registrationService: sl())); + } + + /// Регистрация Repository как фабрик + static void _registerFactoryRepositories() { + sl.registerFactory(() => MyRecipesRepository(myRecipesService: sl())); + sl.registerFactory(() => ResetPasswordRepository(resetPasswordService: sl())); + sl.registerFactory(() => ConsultantRatingRepository(sl())); + sl.registerFactory(() => ViewsRepository(viewsService: sl())); + } +} diff --git a/lib/core/di/service_registrations.dart b/lib/core/di/service_registrations.dart new file mode 100644 index 00000000..454d9064 --- /dev/null +++ b/lib/core/di/service_registrations.dart @@ -0,0 +1,88 @@ +import 'package:eatvio_app/core/comments/services/comments_service.dart'; +import 'package:eatvio_app/core/firebase_api.dart'; +import 'package:eatvio_app/core/views/service/views_service.dart'; +import 'package:eatvio_app/core/websockets/ws_service.dart'; +import 'package:eatvio_app/features/auth/services/auth_service.dart'; +import 'package:eatvio_app/features/auth/services/reset_password_service.dart'; +import 'package:eatvio_app/features/blog_post_create/services/blog_post_service.dart'; +import 'package:eatvio_app/features/calendar/services/calendar_service.dart'; +import 'package:eatvio_app/features/cancel_plan_bottom_sheet/service/cancel_plan_service.dart'; +import 'package:eatvio_app/features/chat/services/chat_service.dart'; +import 'package:eatvio_app/features/complaining/services/complaining_service.dart'; +import 'package:eatvio_app/features/consultant/become_consultant/services/consultant_service.dart'; +import 'package:eatvio_app/features/consultant_rating/services/consultant_rating_service.dart'; +import 'package:eatvio_app/features/consultations/services/consultation_service.dart'; +import 'package:eatvio_app/features/create_diet/services/create_diet_service.dart'; +import 'package:eatvio_app/features/create_plan/services/create_plan_service.dart'; +import 'package:eatvio_app/features/create_recipe/services/recipe_service.dart'; +import 'package:eatvio_app/features/diary/services/diary_service.dart'; +import 'package:eatvio_app/features/diets/services/diet_service.dart'; +import 'package:eatvio_app/features/feed/services/feed_service.dart'; +import 'package:eatvio_app/features/glossary/services/glossary_service.dart'; +import 'package:eatvio_app/features/menu/services/blocked_users_service.dart'; +import 'package:eatvio_app/features/menu/services/notification_settings_service.dart'; +import 'package:eatvio_app/features/my_diets/services/my_diets_service.dart'; +import 'package:eatvio_app/features/my_finance/services/cards_service.dart'; +import 'package:eatvio_app/features/my_plans/services/my_plans_service.dart'; +import 'package:eatvio_app/features/notifications/service/notifications_service.dart'; +import 'package:eatvio_app/features/plans/services/plan_service.dart'; +import 'package:eatvio_app/features/plans/services/plans_service.dart'; +import 'package:eatvio_app/features/profile/services/profile_service.dart'; +import 'package:eatvio_app/features/recipe/services/about_recipe_service.dart'; +import 'package:eatvio_app/features/recipe/services/my_recipes_service.dart'; +import 'package:eatvio_app/features/registration/services/registration_service.dart'; +import 'package:eatvio_app/features/settings/services/settings_service.dart'; +import 'package:get_it/get_it.dart'; + +/// Регистрация всех Service в системе внедрения зависимостей +class ServiceRegistrations { + static final sl = GetIt.instance; + + /// Регистрация всех Service + static void registerAll() { + _registerSingletonServices(); + } + + /// Регистрация Service как синглтонов + static void _registerSingletonServices() { + // Core services + sl.registerLazySingleton(() => ChatService(sl())); + sl.registerLazySingleton(() => FeedService(sl())); + sl.registerLazySingleton(() => ProfileService(sl())); + sl.registerLazySingleton(() => CalendarService(sl())); + sl.registerLazySingleton(() => SettingsService(sl())); + sl.registerLazySingleton(() => ConsultantService(sl())); + sl.registerLazySingleton(() => DiaryService(sl())); + sl.registerLazySingleton(() => GlossaryService(sl())); + sl.registerLazySingleton(() => DietService(sl())); + sl.registerLazySingleton(() => PlansService(sl())); + sl.registerLazySingleton(() => PlanService(sl())); + sl.registerLazySingleton(() => MyDietsService(sl())); + sl.registerLazySingleton(() => MyPlansService(sl())); + sl.registerLazySingleton(() => WsService(sl())); + sl.registerLazySingleton(() => AboutRecipeService(sl())); + sl.registerLazySingleton(() => ComplainingService(sl())); + sl.registerLazySingleton(() => AuthService(sl())); + sl.registerLazySingleton(() => CardsService(sl())); + sl.registerLazySingleton(() => ConsultationService(sl())); + sl.registerLazySingleton(() => CancelPlanService(sl())); + sl.registerLazySingleton(() => CreatePlanService(sl())); + sl.registerLazySingleton(() => NotificationsService(sl())); + sl.registerLazySingleton(() => FirebaseApi()); + sl.registerLazySingleton(() => ResetPasswordService(sl())); + sl.registerLazySingleton(() => BlockedUserService(sl())); + sl.registerLazySingleton(() => CommentsService(sl())); + sl.registerLazySingleton(() => MyRecipesService(sl())); + sl.registerLazySingleton(() => ConsultantRatingService(sl())); + sl.registerLazySingleton(() => NotificationSettingsService(sl())); + sl.registerLazySingleton(() => ViewsService(sl())); + + // Content services + sl.registerLazySingleton(() => CreateRecipeService(sl())); + sl.registerLazySingleton(() => BlogPostService(sl())); + sl.registerLazySingleton(() => CreateDietService(sl())); + + // Auth services + sl.registerLazySingleton(() => RegistrationService(sl())); + } +} diff --git a/lib/core/locator_service.dart b/lib/core/locator_service.dart index 37713f29..8299fea2 100644 --- a/lib/core/locator_service.dart +++ b/lib/core/locator_service.dart @@ -1,263 +1,27 @@ -import 'package:dio/dio.dart'; -import 'package:eatvio_app/core/comments/repositories/comments_repository.dart'; -import 'package:eatvio_app/core/comments/services/comments_service.dart'; -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/di/cubit_registrations.dart'; +import 'package:eatvio_app/core/di/external_registrations.dart'; +import 'package:eatvio_app/core/di/repository_registrations.dart'; +import 'package:eatvio_app/core/di/service_registrations.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'; -import 'package:eatvio_app/features/auth/presentation/bloc/reset_password_cubit/reset_password_cubit.dart'; -import 'package:eatvio_app/features/auth/repositories/auth_repository.dart'; -import 'package:eatvio_app/features/auth/repositories/reset_password_repository.dart'; -import 'package:eatvio_app/features/auth/services/auth_service.dart'; -import 'package:eatvio_app/features/auth/services/reset_password_service.dart'; -import 'package:eatvio_app/features/blog_post/presentation/cubit/blog_post_cubit.dart'; -import 'package:eatvio_app/features/cancel_plan_bottom_sheet/cubit/cancel_plan_cubit.dart'; -import 'package:eatvio_app/features/cancel_plan_bottom_sheet/repository/cancel_plan_repository.dart'; -import 'package:eatvio_app/features/cancel_plan_bottom_sheet/service/cancel_plan_service.dart'; -import 'package:eatvio_app/features/complaining/presentation/cubit/complaining_cubit.dart'; -import 'package:eatvio_app/features/complaining/repositories/complaining_repository.dart'; -import 'package:eatvio_app/features/complaining/services/complaining_service.dart'; -import 'package:eatvio_app/features/consultant_rating/presentation/cubit/consultant_rating_cubit.dart'; -import 'package:eatvio_app/features/consultant_rating/repositories/consultant_rating_repository.dart'; -import 'package:eatvio_app/features/consultant_rating/services/consultant_rating_service.dart'; -import 'package:eatvio_app/features/consultations/presentation/cubit/consultations_cubit.dart'; -import 'package:eatvio_app/features/consultations/repositories/consultations_repository.dart'; -import 'package:eatvio_app/features/consultations/services/consultation_service.dart'; -import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/archive/consultations_archive_cubit.dart'; -import 'package:eatvio_app/features/create_diet/presentation/cubit/create_diet_cubit.dart'; -import 'package:eatvio_app/features/create_diet/repositories/create_diet_repository.dart'; -import 'package:eatvio_app/features/create_diet/services/create_diet_service.dart'; -import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_cubit.dart'; -import 'package:eatvio_app/features/create_plan/repositories/create_plan_repository.dart'; -import 'package:eatvio_app/features/create_plan/services/create_plan_service.dart'; -import 'package:eatvio_app/features/diary/presentation/bloc/consultant_comments_cubit/consultant_comments_cubit.dart'; -import 'package:eatvio_app/features/diets/repositories/diet_repository.dart'; -import 'package:eatvio_app/features/diets/services/diet_service.dart'; -import 'package:eatvio_app/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_cubit.dart'; -import 'package:eatvio_app/features/blog_post_create/repositories/blog_post_repository.dart'; -import 'package:eatvio_app/features/blog_post_create/services/blog_post_service.dart'; -import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/new_messages_cubit/new_messages_cubit.dart'; -import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/consultant_finances/consultant_finances_cubit.dart'; -import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/user_purchases/user_purchases_cubit.dart'; -import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_cubit.dart'; -import 'package:eatvio_app/features/menu/presentation/bloc/notification_settings_cubit/notification_settings_cubit.dart'; -import 'package:eatvio_app/features/menu/presentation/repositories/blocked_users_repository.dart'; -import 'package:eatvio_app/features/menu/repositories/notification_settings_repository.dart'; -import 'package:eatvio_app/features/menu/services/blocked_users_service.dart'; -import 'package:eatvio_app/features/menu/services/notification_settings_service.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'; -import 'package:eatvio_app/features/calendar/presentation/bloc/calendar_cubit.dart'; -import 'package:eatvio_app/features/calendar/repositories/calendar_repository.dart'; -import 'package:eatvio_app/features/calendar/services/calendar_service.dart'; -import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart'; -import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart'; -import 'package:eatvio_app/features/chat/repositories/chat_repository.dart'; -import 'package:eatvio_app/features/chat/services/chat_service.dart'; -import 'package:eatvio_app/features/consultant/become_consultant/presentation/bloc/register_consultant_cubit/register_consultant_cubit.dart'; -import 'package:eatvio_app/features/consultant/become_consultant/repositories/consultant_repository.dart'; -import 'package:eatvio_app/features/consultant/become_consultant/services/consultant_service.dart'; -import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart'; -import 'package:eatvio_app/features/diary/repositories/diary_repository.dart'; -import 'package:eatvio_app/features/diary/services/diary_service.dart'; -import 'package:eatvio_app/features/diets/presentation/cubit/about_diet_cubit.dart'; -import 'package:eatvio_app/core/comments/comments_cubit/comments_cubit.dart'; -import 'package:eatvio_app/features/feed/presentation/bloc/consultants_cubit/consultants_cubit.dart'; -import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart'; -import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart'; -import 'package:eatvio_app/features/feed/presentation/bloc/profile_cubit/profile_cubit.dart'; -import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes_cubit.dart'; -import 'package:eatvio_app/features/feed/repositories/feed_repository.dart'; -import 'package:eatvio_app/features/feed/services/feed_service.dart'; -import 'package:eatvio_app/features/glossary/repositories/glossary_repository.dart'; -import 'package:eatvio_app/features/glossary/services/glossary_service.dart'; -import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_cubit.dart'; -import 'package:eatvio_app/features/my_diets/presentation/cubit/my_diets_cubit.dart'; -import 'package:eatvio_app/features/my_diets/repositories/my_diets_repository.dart'; -import 'package:eatvio_app/features/my_diets/services/my_diets_service.dart'; -import 'package:eatvio_app/features/my_finance/presentation/cubit/user_cards_cubit/user_cards_cubit.dart'; -import 'package:eatvio_app/features/my_finance/repositories/cards_repository.dart'; -import 'package:eatvio_app/features/my_finance/services/cards_service.dart'; -import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_cubit.dart'; -import 'package:eatvio_app/features/my_plans/repositories/my_diets_repository.dart'; -import 'package:eatvio_app/features/my_plans/services/my_plans_service.dart'; -import 'package:eatvio_app/features/notifications/repository/notifications_repository.dart'; -import 'package:eatvio_app/features/notifications/service/notifications_service.dart'; -import 'package:eatvio_app/features/order_info/cubit/order_info_cubit.dart'; -import 'package:eatvio_app/features/plans/repositories/plans_repository.dart'; -import 'package:eatvio_app/features/plans/services/plans_service.dart'; -import 'package:eatvio_app/features/plans/presentation/cubit/about_plan_cubit.dart'; -import 'package:eatvio_app/features/plans/repositories/plan_repository.dart'; -import 'package:eatvio_app/features/plans/services/plan_service.dart'; -import 'package:eatvio_app/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart'; -import 'package:eatvio_app/features/profile/presentation/bloc/sub_cubit/sub_cubit.dart'; -import 'package:eatvio_app/features/profile/repositories/profile_repository.dart'; -import 'package:eatvio_app/features/profile/services/profile_service.dart'; -import 'package:eatvio_app/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart'; -import 'package:eatvio_app/features/create_recipe/reporitories/recipe_repository.dart'; -import 'package:eatvio_app/features/create_recipe/services/recipe_service.dart'; -import 'package:eatvio_app/features/recipe/presentation/cubit/about_recipe_cubit.dart'; -import 'package:eatvio_app/features/recipe/presentation/cubit/my_recipes_cubit/my_recieps_cubit.dart'; -import 'package:eatvio_app/features/recipe/repositories/about_recipe_repository.dart'; -import 'package:eatvio_app/features/recipe/repositories/my_recipes_repository.dart'; -import 'package:eatvio_app/features/recipe/services/about_recipe_service.dart'; -import 'package:eatvio_app/features/recipe/services/my_recipes_service.dart'; -import 'package:eatvio_app/features/registration/presentation/bloc/registration_cubit/registration_cubit.dart'; -import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart'; -import 'package:eatvio_app/features/registration/repositories/registration_repository.dart'; -import 'package:eatvio_app/features/registration/services/registration_service.dart'; -import 'package:eatvio_app/features/settings/presentation/bloc/settings_cubit/settings_cubit.dart'; -import 'package:eatvio_app/features/settings/repositories/service_repository.dart'; -import 'package:eatvio_app/features/settings/services/settings_service.dart'; -import 'package:eatvio_app/features/user_details/presentation/cubit/user_details_cubit.dart'; import 'package:get_it/get_it.dart'; -import 'package:eatvio_app/features/profile/presentation/bloc/plans_cubit/plans_cubit.dart' as profile_plans; +/// Глобальный экземпляр GetIt для внедрения зависимостей final sl = GetIt.instance; +/// Инициализация системы внедрения зависимостей Future init() async { - // Cubit singletons + // Регистрация внешних зависимостей (Dio, etc.) + ExternalRegistrations.registerAll(); + + // Регистрация сервисов + ServiceRegistrations.registerAll(); + + // Регистрация репозиториев + RepositoryRegistrations.registerAll(); + + // Регистрация Cubit'ов + CubitRegistrations.registerAll(); + + // Регистрация WebSocket менеджера sl.registerLazySingleton(() => WsManager(wsService: sl())); - sl.registerLazySingleton(() => NewMessagesCubit()); - sl.registerLazySingleton(() => NotificationsCubit(wsManager: sl(), notificationsRepository: sl())); - sl.registerLazySingleton(() => ProfileAvatarCubit(profileRepository: sl())); - sl.registerLazySingleton(() => ChatsCubit(chatRepository: sl(), wsManager: sl(), profileRepository: sl())); - - // Cubit factories - sl.registerFactory(() => FeedCubit(feedRepository: sl())); - sl.registerFactory(() => RegistrationCubit(registrationRepository: sl())); - sl.registerFactory(() => ChatRoomCubit(cardsRepository: sl(), chatRepository: sl(), wsManager: sl())); - sl.registerFactory(() => RecipesCubit(feedRepository: sl(), recipeRepository: sl(), aboutRecipeRepository: sl())); - sl.registerFactory(() => CalendarCubit(calendarRepository: sl())); - sl.registerFactory(() => ProfileCubit(profileRepository: sl(), chatRepository: sl(), consultantRepository: sl())); - sl.registerFactory(() => profile_plans.PlansCubit(plansRepository: sl())); - 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(), consultationsRepository: sl())); - sl.registerFactory(() => RegisterConsultantCubit(consultantRepository: sl())); - sl.registerFactory( - () => DiaryCubit( - chatRepository: sl(), - diaryRepository: sl(), - glossaryRepository: sl(), - profileRepository: sl(), - dietRepository: sl(), - settingsRepository: sl(), - ), - ); - sl.registerFactory(() => AboutDietCubit(dietsRepository: sl(), profileRepository: sl())); - sl.registerFactory(() => BlogPostCreateCubit(blogPostRepository: sl())); - sl.registerFactory(() => SubCubit(profileRepository: sl())); - sl.registerFactory(() => CreateDietCubit(profileRepository: sl(), createDietRepository: sl(), glossaryRepository: sl())); - sl.registerFactory(() => CreatePlanCubit(createPlanRepository: sl())); - sl.registerFactory(() => FeedProfileCubit(profileRepository: sl())); - sl.registerFactory(() => AboutPlanCubit(planRepository: sl())); - sl.registerFactory(() => CreateRecipeCubit(recipeRepository: sl(), glossaryRepository: sl())); - sl.registerFactory(() => MyDietsCubit(myDietsRepository: sl(), createDietRepository: sl())); - sl.registerFactory(() => MyPlansCubit(myPlansRepository: sl())); - sl.registerFactory(() => UserDetailsCubit(profileRepository: sl())); - sl.registerFactory(() => ConsultantsCubit(feedRepository: sl())); - sl.registerFactory(() => AboutRecipeCubit(recipeRepository: sl(), profileRepository: sl())); - sl.registerFactory(() => AuthCubit(authRepository: sl())); - sl.registerFactory(() => ComplainingCubit(repository: sl())); - sl.registerFactory(() => UserCardsCubit(cardsRepository: sl())); - sl.registerFactory(() => OrderInfoCubit(chatRepository: sl())); - sl.registerFactory(() => ConsultationsCubit(consultationsRepository: sl())); - sl.registerFactory(() => ConsultantCommentsCubit(diaryRepository: sl())); - sl.registerFactory(() => CancelPlanCubit(cancelPlanRepository: sl())); - sl.registerFactory(() => VerifyEmailCubit(profileRepository: sl())); - sl.registerFactory(() => MyRecipesCubit(myRecipesRepository: sl(), aboutRecipeRepository: sl())); - sl.registerFactory(() => ConsultationsArchiveCubit(consultationsRepository: sl())); - sl.registerFactory(() => ConsultantFinancesCubit(consultationsRepository: sl())); - sl.registerFactory(() => UserPurchasesCubit(consultationsRepository: sl())); - sl.registerFactory(() => ResetPasswordCubit(resetPasswordRepository: sl())); - sl.registerFactory(() => BlogPostCubit(sl())); - 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())); - sl.registerLazySingleton(() => FeedRepository(feedService: sl())); - sl.registerLazySingleton(() => RegistrationRepository(registrationService: sl())); - sl.registerLazySingleton(() => CalendarRepository(calendarService: sl())); - sl.registerLazySingleton(() => ProfileRepository(profileService: sl())); - sl.registerLazySingleton(() => RecipeRepository(recipeService: sl())); - sl.registerLazySingleton(() => SettingsRepository(settingsService: sl())); - sl.registerLazySingleton(() => ConsultantRepository(consultantService: sl())); - sl.registerLazySingleton(() => DiaryRepository(diaryService: sl())); - sl.registerLazySingleton(() => GlossaryRepository(glossaryService: sl())); - sl.registerLazySingleton(() => DietRepository(dietsService: sl())); - sl.registerLazySingleton(() => BlogPostRepository(blogPostService: sl())); - sl.registerLazySingleton(() => CreateDietRepository(createDietService: sl())); - sl.registerLazySingleton(() => PlansRepository(plansService: sl())); - sl.registerLazySingleton(() => PlanRepository(planService: sl())); - sl.registerLazySingleton(() => MyDietsRepository(myDietsService: sl())); - sl.registerLazySingleton(() => MyPlansRepository(myPlansService: sl())); - sl.registerLazySingleton(() => AboutRecipeRepository(recipeService: sl())); - sl.registerLazySingleton(() => ComplainingRepository(service: sl())); - sl.registerLazySingleton(() => AuthRepository(authService: sl())); - sl.registerLazySingleton(() => CardsRepository(cardsService: sl())); - sl.registerLazySingleton(() => ConsultationsRepository(consultationService: sl())); - sl.registerLazySingleton(() => CancelPlanRepository(cancelService: sl())); - sl.registerLazySingleton(() => CreatePlanRepository(createPlanService: sl())); - sl.registerLazySingleton(() => NotificationsRepository(notificationsService: sl())); - sl.registerFactory(() => MyRecipesRepository(myRecipesService: sl())); - sl.registerFactory(() => ResetPasswordRepository(resetPasswordService: sl())); - sl.registerLazySingleton(() => BlockedUsersRepository(blockedUserService: sl())); - sl.registerLazySingleton(() => CommentsRepository(service: sl())); - sl.registerLazySingleton(() => NotificationSettingsRepository(sl())); - sl.registerFactory(() => ConsultantRatingRepository(sl())); - sl.registerFactory(() => ViewsRepository(viewsService: sl())); - - // Services - sl.registerLazySingleton(() => ChatService(sl())); - sl.registerLazySingleton(() => FeedService(sl())); - sl.registerLazySingleton(() => RegistrationService(sl())); - sl.registerLazySingleton(() => CalendarService(sl())); - sl.registerLazySingleton(() => ProfileService(sl())); - sl.registerLazySingleton(() => CreateRecipeService(sl())); - sl.registerLazySingleton(() => SettingsService(sl())); - sl.registerLazySingleton(() => ConsultantService(sl())); - sl.registerLazySingleton(() => DiaryService(sl())); - sl.registerLazySingleton(() => GlossaryService(sl())); - sl.registerLazySingleton(() => DietService(sl())); - sl.registerLazySingleton(() => BlogPostService(sl())); - sl.registerLazySingleton(() => CreateDietService(sl())); - sl.registerLazySingleton(() => PlansService(sl())); - sl.registerLazySingleton(() => PlanService(sl())); - sl.registerLazySingleton(() => MyDietsService(sl())); - sl.registerLazySingleton(() => MyPlansService(sl())); - sl.registerLazySingleton(() => WsService(sl())); - sl.registerLazySingleton(() => AboutRecipeService(sl())); - sl.registerLazySingleton(() => ComplainingService(sl())); - sl.registerLazySingleton(() => AuthService(sl())); - sl.registerLazySingleton(() => CardsService(sl())); - sl.registerLazySingleton(() => ConsultationService(sl())); - sl.registerLazySingleton(() => CancelPlanService(sl())); - sl.registerLazySingleton(() => CreatePlanService(sl())); - sl.registerLazySingleton(() => NotificationsService(sl())); - sl.registerLazySingleton(() => FirebaseApi()); - sl.registerLazySingleton(() => ResetPasswordService(sl())); - sl.registerLazySingleton(() => BlockedUserService(sl())); - sl.registerLazySingleton(() => CommentsService(sl())); - sl.registerLazySingleton(() => MyRecipesService(sl())); - sl.registerLazySingleton(() => ConsultantRatingService(sl())); - sl.registerLazySingleton(() => NotificationSettingsService(sl())); - sl.registerLazySingleton(() => ViewsService(sl())); - - // External - sl.registerLazySingleton( - () => - Dio(BaseOptions(baseUrl: baseUrl, headers: {'accept': 'application/json', 'content-type': 'application/json'})) - ..interceptors.addAll([TokenInterceptor(), PhoneVerifyErrorInterceptor()]), - ); -} +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index d716f3fa..c6501d49 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,53 +1,15 @@ -import 'dart:io' as io; +import 'package:eatvio_app/core/app_config.dart'; +import 'package:eatvio_app/core/app_initializer.dart'; 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'; -import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart'; -import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart'; -import 'package:eatvio_app/features/internet_connection_checker/internet_connection_checker_screen.dart'; -import 'package:eatvio_app/firebase_options.dart'; -import 'package:eatvio_app/generated/l10n.dart'; -import 'package:firebase_core/firebase_core.dart'; -import 'package:firebase_messaging/firebase_messaging.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_localizations/flutter_localizations.dart'; -import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:flutter_portal/flutter_portal.dart'; import 'package:keyboard_dismisser/keyboard_dismisser.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; -import 'package:flutter_dotenv/flutter_dotenv.dart'; -import 'package:eatvio_app/core/env_config.dart'; -import 'package:appmetrica_plugin/appmetrica_plugin.dart'; Future main() async { - WidgetsBinding widgetsBinding = SentryWidgetsFlutterBinding.ensureInitialized(); - FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); - - await dotenv.load(fileName: ".env"); - - await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); - await FirebaseMessaging.instance.requestPermission(); - await di.init(); - - if (kDebugMode) { - io.HttpClient.enableTimelineLogging = true; - } - - AppMetrica.activate(AppMetricaConfig(EnvConfig.appMetricaApiKey)); - AppMetrica.reportEvent('Application started'); - - await SentryFlutter.init((options) { - options.dsn = EnvConfig.sentryDsn; - options.tracesSampleRate = 1.0; - options.debug = false; - }, appRunner: () => runApp(const MyApp())); + await AppInitializer.initialize(); + await AppInitializer.runAppWithSentry(() => runApp(const MyApp())); } class MyApp extends StatelessWidget { @@ -56,30 +18,16 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MultiBlocProvider( - providers: [ - BlocProvider(create: (context) => sl()), - BlocProvider(create: (context) => sl()..getNotifications()), - BlocProvider(create: (context) => sl()..loadFeedBlogPostList()), - BlocProvider(create: (context) => sl()..getMyProfile()), - BlocProvider(create: (context) => sl()..getChatRoomsWithLoading()), - ], + providers: AppConfig.globalBlocProviders, child: Portal( child: KeyboardDismisser( child: MaterialApp.router( theme: ThemeHelper().themeData(), routerConfig: AppRouter.appRouter, - builder: (context, child) => BlocProvider( - create: (context) => sl(), - child: InternetConnectionCheckerScreen(child: child!), - ), + builder: (context, child) => AppConfig.createInternetConnectionWrapper(child!), debugShowCheckedModeBanner: false, - localizationsDelegates: const [ - S.delegate, - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - ], - supportedLocales: const [Locale.fromSubtags(languageCode: 'ru')], + localizationsDelegates: AppConfig.localizationDelegates, + supportedLocales: AppConfig.supportedLocales, ), ), ), From 7861cdce3cdb74b2471e61c5ab024ffc73c3eb2d Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Wed, 1 Oct 2025 23:59:34 +0300 Subject: [PATCH 45/78] fix: refactor --- lib/core/di/cubit_registrations.dart | 1 - lib/core/di/repository_registrations.dart | 1 - 2 files changed, 2 deletions(-) diff --git a/lib/core/di/cubit_registrations.dart b/lib/core/di/cubit_registrations.dart index efd0dbd2..156e43b1 100644 --- a/lib/core/di/cubit_registrations.dart +++ b/lib/core/di/cubit_registrations.dart @@ -23,7 +23,6 @@ import 'package:eatvio_app/features/feed/presentation/bloc/consultants_cubit/con import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_cubit.dart'; -import 'package:eatvio_app/features/feed/presentation/bloc/profile_cubit/profile_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes_cubit.dart'; import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/archive/consultations_archive_cubit.dart'; import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/consultant_finances/consultant_finances_cubit.dart'; diff --git a/lib/core/di/repository_registrations.dart b/lib/core/di/repository_registrations.dart index 6ce258aa..71d1523d 100644 --- a/lib/core/di/repository_registrations.dart +++ b/lib/core/di/repository_registrations.dart @@ -12,7 +12,6 @@ import 'package:eatvio_app/features/consultant_rating/repositories/consultant_ra import 'package:eatvio_app/features/consultations/repositories/consultations_repository.dart'; import 'package:eatvio_app/features/create_diet/repositories/create_diet_repository.dart'; import 'package:eatvio_app/features/create_plan/repositories/create_plan_repository.dart'; -import 'package:eatvio_app/features/create_recipe/reporitories/recipe_repository.dart'; import 'package:eatvio_app/features/diary/repositories/diary_repository.dart'; import 'package:eatvio_app/features/diets/repositories/diet_repository.dart'; import 'package:eatvio_app/features/feed/repositories/feed_repository.dart'; From e7ce9d8f909aab9841aaa3424804df3432345174 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Thu, 2 Oct 2025 00:05:25 +0300 Subject: [PATCH 46/78] fix: refactor --- lib/core/app_router.dart | 482 ++---------------- lib/core/constants/app_constants.dart | 72 +++ lib/core/cubit/base_cubit.dart | 42 ++ lib/core/cubit/state_utils.dart | 28 + lib/core/routing/auth_routes.dart | 37 ++ lib/core/routing/content_routes.dart | 73 +++ lib/core/routing/main_navigation_routes.dart | 94 ++++ lib/core/routing/other_routes.dart | 229 +++++++++ lib/core/routing/route_constants.dart | 84 +++ lib/core/routing/settings_routes.dart | 62 +++ lib/core/utils/validation_utils.dart | 87 ++++ .../blog_post_create_cubit.dart | 55 +- 12 files changed, 862 insertions(+), 483 deletions(-) create mode 100644 lib/core/constants/app_constants.dart create mode 100644 lib/core/cubit/base_cubit.dart create mode 100644 lib/core/cubit/state_utils.dart create mode 100644 lib/core/routing/auth_routes.dart create mode 100644 lib/core/routing/content_routes.dart create mode 100644 lib/core/routing/main_navigation_routes.dart create mode 100644 lib/core/routing/other_routes.dart create mode 100644 lib/core/routing/route_constants.dart create mode 100644 lib/core/routing/settings_routes.dart create mode 100644 lib/core/utils/validation_utils.dart diff --git a/lib/core/app_router.dart b/lib/core/app_router.dart index cc028255..f3715464 100644 --- a/lib/core/app_router.dart +++ b/lib/core/app_router.dart @@ -1,455 +1,53 @@ -import 'package:dio/dio.dart'; -import 'package:eatvio_app/core/const.dart'; -import 'package:eatvio_app/core/firebase_api.dart'; -import 'package:eatvio_app/core/token_storage.dart'; -import 'package:eatvio_app/core/websockets/ws_manager.dart'; -import 'package:eatvio_app/core/locator_service.dart'; -import 'package:eatvio_app/features/auth/presentation/pages/auth_screen.dart'; -import 'package:eatvio_app/features/auth/presentation/pages/forget_password_screen.dart'; -import 'package:eatvio_app/features/auth/presentation/pages/scan_qr_code_screen.dart'; -import 'package:eatvio_app/features/blog_post/presentation/pages/blog_post_screen.dart'; -import 'package:eatvio_app/features/blog_post_create/presentation/pages/blog_post_create_screen.dart'; -import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; -import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_state.dart'; -import 'package:eatvio_app/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart'; -import 'package:eatvio_app/features/calendar/presentation/pages/calendar_screen.dart'; -import 'package:eatvio_app/features/chat/presentation/pages/chat_screen.dart'; -import 'package:eatvio_app/features/chat/presentation/pages/chats_screen.dart'; -import 'package:eatvio_app/features/chat/presentation/pages/empty_chat_screen.dart'; -import 'package:eatvio_app/features/chat/presentation/pages/payment_screen.dart'; -import 'package:eatvio_app/features/chat/presentation/pages/photo_view_screen.dart'; -import 'package:eatvio_app/features/consultant/become_consultant/presentation/pages/become_consultant_screen.dart'; -import 'package:eatvio_app/features/consultant/become_consultant/presentation/pages/stepper_become_consultant_screen.dart'; -import 'package:eatvio_app/features/consultations/presentation/pages/consultations_screen.dart'; -import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/consultant_finances_screen.dart'; -import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/user_purchases_screen.dart'; -import 'package:eatvio_app/features/menu/presentation/pages/blocked_users_screen.dart'; -import 'package:eatvio_app/features/menu/presentation/pages/notifications_settings_screen.dart'; -import 'package:eatvio_app/features/order_info/presentation/info_by_order_screen.dart'; -import 'package:eatvio_app/features/create_diet/presentation/create_diet_screen.dart'; -import 'package:eatvio_app/features/create_diet/presentation/diet_preview_screen.dart'; -import 'package:eatvio_app/features/create_plan/presentation/create_plan_screen.dart'; -import 'package:eatvio_app/features/consultant_rating/presentation/consultant_rating_screen.dart'; -import 'package:eatvio_app/features/dev_tools/presentation/dev_tools_screen.dart'; -import 'package:eatvio_app/features/diary/presentation/pages/diary_screen.dart'; -import 'package:eatvio_app/features/diets/presentation/pages/about_diet_screen.dart'; -import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; -import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart'; -import 'package:eatvio_app/features/feed/models/recipes_model.dart'; -import 'package:eatvio_app/features/feed/presentation/pages/feed_screen.dart'; -import 'package:eatvio_app/features/glossary/presentation/pages/glossary_screen.dart'; -import 'package:eatvio_app/features/menu/presentation/pages/menu_screen.dart'; -import 'package:eatvio_app/features/my_diets/presentation/my_diets_screen.dart'; -import 'package:eatvio_app/features/my_finance/presentation/pages/card_settings.dart'; -import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/consultations_archive_screen.dart'; -import 'package:eatvio_app/features/my_finance/presentation/pages/user_finance_screen.dart'; -import 'package:eatvio_app/features/my_plans/presentation/my_plans_screen.dart'; -import 'package:eatvio_app/features/notifications/presentation/notifications_screen.dart'; -import 'package:eatvio_app/features/plans/presentation/pages/about_plan_screen.dart'; -import 'package:eatvio_app/features/profile/presentation/pages/profile_screen.dart'; -import 'package:eatvio_app/features/profile/presentation/pages/subs_screen.dart'; -import 'package:eatvio_app/features/recipe/presentation/pages/about_recipe_screen.dart'; -import 'package:eatvio_app/features/create_recipe/presentation/pages/recipe_create_screen.dart'; -import 'package:eatvio_app/features/recipe/presentation/pages/my_recipes_screen.dart'; -import 'package:eatvio_app/features/registration/presentation/pages/registration_screen.dart'; -import 'package:eatvio_app/features/registration/presentation/pages/verify_email_screen.dart'; -import 'package:eatvio_app/features/registration/presentation/pages/verify_screen.dart'; -import 'package:eatvio_app/features/settings/entities/settings_entity.dart'; -import 'package:eatvio_app/features/settings/presentation/pages/change_email_screen.dart'; -import 'package:eatvio_app/features/settings/presentation/pages/change_password_screen.dart'; -import 'package:eatvio_app/features/settings/presentation/pages/general_info_screen.dart'; -import 'package:eatvio_app/features/settings/presentation/pages/other_info_screen.dart'; -import 'package:eatvio_app/features/settings/presentation/pages/physical_activity_screen.dart'; -import 'package:eatvio_app/features/settings/presentation/pages/physical_form_screen.dart'; -import 'package:eatvio_app/features/settings/presentation/pages/pressure_screen.dart'; -import 'package:eatvio_app/features/settings/presentation/pages/settings_screen.dart'; -import 'package:eatvio_app/features/support/presentation/pages/support_screen.dart'; -import 'package:eatvio_app/features/update_screen/presentation/update_screen.dart'; -import 'package:eatvio_app/features/user_details/presentation/user_details_screen.dart'; -import 'package:eatvio_app/features/welcome/presentation/pages/welcome_screen.dart'; +import 'package:eatvio_app/core/routing/auth_routes.dart'; +import 'package:eatvio_app/core/routing/content_routes.dart'; +import 'package:eatvio_app/core/routing/main_navigation_routes.dart'; +import 'package:eatvio_app/core/routing/other_routes.dart'; +import 'package:eatvio_app/core/routing/route_constants.dart'; +import 'package:eatvio_app/core/routing/settings_routes.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:go_router/go_router.dart'; -import 'package:package_info_plus/package_info_plus.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import 'package:version/version.dart'; -import 'dart:developer'; +/// Главный роутер приложения class AppRouter { static GlobalKey navigatorKey = GlobalKey(); static GoRouter appRouter = GoRouter( - initialLocation: '/update-screen', + initialLocation: RouteConstants.updateScreen, navigatorKey: navigatorKey, - redirect: (context, state) { - final segments = state.uri.pathSegments; - if (segments.length >= 3) { - final type = segments[1]; - final contentIdentifier = segments[2]; - - switch (type) { - case 'recipe': - FlutterNativeSplash.remove(); - return '/about-recipes-screen?slug=$contentIdentifier'; - case 'blog_post': - FlutterNativeSplash.remove(); - return '/blog-post-screen?id=$contentIdentifier'; - case 'plan': - FlutterNativeSplash.remove(); - return '/about-plan-screen?slug=$contentIdentifier'; - case 'diet': - FlutterNativeSplash.remove(); - return '/about-diet-screen?slug=$contentIdentifier'; - } - } - - return null; - }, + redirect: _handleRedirect, routes: [ - GoRoute( - path: '/auth-screen', - name: 'authScreen', - builder: (context, state) => const AuthScreen(), - routes: [ - GoRoute(path: 'registration-screen', name: 'registrationScreen', builder: (context, state) => const RegistrationScreen()), - GoRoute( - path: 'forget-password-screen', - name: 'forgetPasswordScreen', - builder: (context, state) => ForgetPasswordScreen(isAuth: state.extra as bool), - ), - GoRoute(path: 'scan-qr-code-screen', name: 'scanQRCodeScreen', builder: (context, state) => const ScanQRCodeScreen()), - ], - ), - ShellRoute( - builder: (context, state, child) { - return BlocBuilder( - builder: (BuildContext context, ProfileAvatarState state) { - if (state is ProfileAvatarStateLoaded) { - return BottomNavigationBarWidget(profileModel: state.profile, child: child); - } - return const SizedBox.shrink(); - }, - ); - }, - routes: [ - GoRoute( - path: '/my-profile-screen', - name: 'myProfileScreen', - pageBuilder: (context, state) { - return const NoTransitionPage(child: ProfileScreen()); - }, - ), - GoRoute( - path: '/feed-screen', - name: 'feedScreen', - pageBuilder: (context, state) => const NoTransitionPage(child: FeedScreen()), - redirect: (context, state) async { - try { - await sl().initialize(); - } catch (e, st) { - log(e.toString(), stackTrace: st); - } - return null; - }, - ), - GoRoute( - path: '/diary-screen', - name: 'diaryScreen', - pageBuilder: (context, state) => const NoTransitionPage(child: DiaryScreen()), - ), - GoRoute( - path: ConsultationsScreen.path, - name: ConsultationsScreen.name, - pageBuilder: (context, state) => const NoTransitionPage(child: ConsultationsScreen()), - ), - GoRoute( - path: '/chat-select-screen', - name: 'chatSelectScreen', - pageBuilder: (context, state) => const NoTransitionPage(child: ChatsScreen()), - ), - GoRoute( - path: '/notifications-screen', - name: 'notificationsScreen', - pageBuilder: (context, state) => const NoTransitionPage(child: NotificationsScreen()), - ), - GoRoute( - path: '/menu-screen', - name: 'menuScreen', - pageBuilder: (context, state) => NoTransitionPage( - child: BlocBuilder( - builder: (BuildContext context, ProfileAvatarState state) { - if (state is ProfileAvatarStateLoaded) { - return MenuScreen(profileModel: state.profile); - } - return const SizedBox.shrink(); - }, - ), - ), - ), - ], - ), - GoRoute(name: 'blockedUsersScreen', path: '/blocked-users-screen', builder: (context, state) => const BlockedUsersScreen()), - GoRoute( - path: '/welcome-screen', - name: 'welcomeScreen', - builder: (context, state) => const WelcomeScreen(), - redirect: (context, state) async { - try { - final sharedPreferences = await SharedPreferences.getInstance(); - final welcomeScreenWasShowed = sharedPreferences.getBool('welcomeScreenWasShowed') ?? false; - await sl().registerPushToken(); - if (welcomeScreenWasShowed == false) { - return null; - } else if (await TokenStorage.haveTokenInStorage()) { - return '/feed-screen'; - } else { - return '/auth-screen'; - } - } catch (e) { - return '/feed-screen'; - } - }, - ), - GoRoute( - path: '/update-screen', - name: 'updateScreen', - builder: (context, state) => const UpdateScreen(), - redirect: (context, state) async { - try { - final versions = (await sl().get('$baseUrl/api/app_versions')).data; - PackageInfo packageInfo = await PackageInfo.fromPlatform(); - String version = packageInfo.version; - Version currentVersion = Version.parse(version); - Version latestVersion = Version.parse(versions['min_version']); - if (latestVersion > currentVersion) { - return null; - } else { - return '/welcome-screen'; - } - } catch (e) { - return '/welcome-screen'; - } - }, - ), - GoRoute(path: ConsultationsArchiveScreen.path, name: ConsultationsArchiveScreen.name, builder: (context, state) => const ConsultationsArchiveScreen()), - GoRoute(path: ConsultantFinancesScreen.path, name: ConsultantFinancesScreen.name, builder: (context, state) => const ConsultantFinancesScreen()), - GoRoute(path: UserPurchasesScreen.path, name: UserPurchasesScreen.name, builder: (context, state) => const UserPurchasesScreen()), - GoRoute(path: '/user-finance-screen', name: 'userFinanceScreen', builder: (context, state) => const UserFinanceScreen()), - GoRoute( - path: CardSettingsScreen.path, - name: CardSettingsScreen.name, - builder: (context, state) { - final chosenPlanId = state.extra as int?; - return CardSettingsScreen(chosenPlanId: chosenPlanId); - }, - ), - GoRoute( - path: '/about-plan-screen', - name: 'aboutPlanScreen', - builder: (context, state) { - final slug = state.uri.queryParameters['slug']; - if (slug != null) { - return AboutPlanScreen(slug: slug, navigateToComments: false, index: null); - } - - final data = state.extra as Map; - return AboutPlanScreen(slug: data['slug'] as String, index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool); - }, - ), - GoRoute(path: '/glossary-screen', name: 'glossaryScreen', builder: (context, state) => const GlossaryScreen()), - GoRoute( - path: '/about-recipes-screen', - name: 'aboutRecipesScreen', - builder: (context, state) { - final slug = state.uri.queryParameters['slug']; - if (slug != null) { - return AboutRecipeScreen(recipeSlug: slug, navigateToComments: false, index: null); - } - - final data = state.extra as Map; - return AboutRecipeScreen(recipeSlug: data['recipe'] as String, index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool); - }, - ), - GoRoute( - path: RecipeCreateScreen.path, - name: RecipeCreateScreen.name, - builder: (context, state) => RecipeCreateScreen(recipe: state.extra != null ? state.extra as Recipe : null), - ), - GoRoute( - path: '/profile-screen', - name: 'profileScreen', - builder: (context, state) => ProfileScreen(author: state.extra as Author), - ), - GoRoute( - path: '/subs-screen/:slug', - name: 'subsScreen', - builder: (context, state) => SubsScreen(slug: state.pathParameters['slug']!, subType: state.extra as SubType), - ), - GoRoute( - path: '/about-diet-screen', - name: 'aboutDietScreen', - builder: (context, state) { - final slug = state.uri.queryParameters['slug']; - if (slug != null) { - return AboutDietScreen(slug: slug, index: null, navigateToComments: false); - } - final data = state.extra as Map; - return AboutDietScreen(slug: data['slug'] as String, index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool); - }, - ), - GoRoute( - path: '/blog-post-create-screen', - name: 'blogPostCreateScreen', - builder: (context, state) => BlogPostCreateScreen(feedBlogPostModel: state.extra != null ? state.extra as FeedBlogPostModel : null), - ), - GoRoute( - path: '/blog-post-screen', - name: 'blogPostScreen', - builder: (context, state) { - final id = state.uri.queryParameters['id']; - final parsedId = int.tryParse(id ?? ''); - if (parsedId != null) { - return BlogPostScreen(id: parsedId); - } - - return BlogPostScreen(id: state.extra as int); - }, - ), - GoRoute( - path: '/chat-screen', - name: 'chatScreen', - builder: (context, state) { - final data = state.extra as Map; - final uuid = data['uuid'] as String; - final sendInitialMessage = data['sendInitialMessage'] as bool?; - return ChatScreen(uuid: uuid, sendInitialMessage: sendInitialMessage); - }, - ), - GoRoute( - path: '/empty-chat-screen', - name: 'emptyChatScreen', - builder: (context, state) => EmptyChatScreen(author: state.extra as Author), - ), - GoRoute( - path: '/settings-screen', - name: 'settingsScreen', - builder: (context, state) => const SettingsScreen(), - routes: [ - GoRoute( - path: 'general-info-screen', - name: 'generalInfoScreen', - builder: (context, state) => GeneralInfoScreen(settingsEntity: state.extra as SettingsEntity), - ), - GoRoute( - path: 'other-info-screen', - name: 'otherInfoScreen', - builder: (context, state) => OtherInfoScreen(settingsEntity: state.extra as SettingsEntity), - ), - GoRoute( - path: 'physical-activity-screen', - name: 'physicalActivityScreen', - builder: (context, state) => PhysicalActivityScreen(settingsEntity: state.extra as SettingsEntity), - ), - GoRoute( - path: 'physical-form-screen', - name: 'physicalFormScreen', - builder: (context, state) => PhysicalFormScreen(settingsEntity: state.extra as SettingsEntity), - ), - GoRoute( - path: 'pressure-screen', - name: 'pressureScreen', - builder: (context, state) => PressureScreen(settingsEntity: state.extra as SettingsEntity), - ), - GoRoute( - path: 'change-password-screen', - name: 'changePasswordScreen', - builder: (context, state) => ChangePasswordScreen(settingsEntity: state.extra as SettingsEntity), - ), - GoRoute( - path: 'change-email-screen', - name: 'changeEmailScreen', - builder: (context, state) => ChangeEmailScreen(settingsEntity: state.extra as SettingsEntity), - ), - ], - ), - GoRoute(path: '/become-consultant-screen', name: 'becomeConsultantScreen', builder: (context, state) => const BecomeConsultantScreen()), - GoRoute( - path: '/stepper-become-consultant-screen', - name: 'stepperBecomeConsultantScreen', - builder: (context, state) => StepperBecomeConsultantScreen(slug: state.extra == null ? null : state.extra as String), - ), - GoRoute(path: '/calendar-screen', name: 'calendarScreen', builder: (context, state) => const CalendarScreen()), - GoRoute(path: '/support-screen', name: 'supportScreen', builder: (context, state) => const SupportScreen()), - GoRoute( - path: '/photo-view-screen/:index', - name: 'photoViewScreen', - builder: (context, state) => PhotoViewScreen(imageUrls: state.extra as List, index: int.parse(state.pathParameters['index']!)), - ), - GoRoute( - path: CreateDietScreen.path, - name: CreateDietScreen.name, - builder: (context, state) { - final diet = state.extra as Diet?; - return CreateDietScreen(diet: diet); - }, - ), - GoRoute( - path: DietPreviewScreen.path, - name: DietPreviewScreen.name, - builder: (context, state) { - final data = state.extra as Diet; - return DietPreviewScreen(diet: data); - }, - ), - GoRoute(path: CreatePlanScreen.path, name: CreatePlanScreen.name, builder: (context, state) => const CreatePlanScreen()), - GoRoute(path: MyDietsScreen.path, name: MyDietsScreen.name, builder: (context, state) => const MyDietsScreen()), - GoRoute(path: MyPlansScreen.path, name: MyPlansScreen.name, builder: (context, state) => const MyPlansScreen()), - GoRoute( - path: UserDetailsScreen.path, - name: UserDetailsScreen.name, - builder: (context, state) => UserDetailsScreen(slug: state.extra as String), - ), - GoRoute(path: DevToolsScreen.path, name: DevToolsScreen.name, builder: (context, state) => const DevToolsScreen()), - GoRoute(path: ConsultantRatingScreen.path, name: ConsultantRatingScreen.name, builder: (context, state) => const ConsultantRatingScreen()), - GoRoute( - path: InfoByOrderScreen.path, - name: InfoByOrderScreen.name, - builder: (context, state) { - final data = state.extra as Map; - final planId = data['planId'] as int; - final successfulPayment = data['successfulPayment'] as bool?; - return InfoByOrderScreen(planId: planId, successfulPayment: successfulPayment); - }, - ), - GoRoute( - path: PaymentScreen.path, - name: PaymentScreen.name, - builder: (context, state) { - final data = state.extra as Map; - final planId = data['planId'] as int; - final url = data['url'] as String; - return PaymentScreen(url: url, planId: planId); - }, - ), - GoRoute( - path: UserDiaryScreen.path, - name: UserDiaryScreen.name, - builder: (context, state) { - final data = state.extra as Map; - final diaryId = data['diaryId'] as int; - final currentPlanDay = data['currentDiaryDay'] as int; - return UserDiaryScreen(diaryId: diaryId, currentPlanDay: currentPlanDay); - }, - ), - GoRoute( - path: '/verify-screen', - name: 'verifyScreen', - builder: (context, state) => VerifyScreen(passKey: state.extra as String), - ), - GoRoute(path: '/verify-email-screen', name: 'verifyEmailScreen', builder: (context, state) => const VerifyEmailScreen()), - GoRoute(path: MyRecipesScreen.path, name: MyRecipesScreen.name, builder: (context, state) => const MyRecipesScreen()), - GoRoute(path: '/notifications-settings-screen', name: 'notificationsSettingsScreen', builder: (context, state) => const NotificationsSettingsScreen()), + ...AuthRoutes.createRoutes(), + ...MainNavigationRoutes.createRoutes(), + ...ContentRoutes.createRoutes(), + ...SettingsRoutes.createRoutes(), + ...OtherRoutes.createRoutes(), ], ); + + /// Обработка редиректов для deep links + static String? _handleRedirect(BuildContext context, GoRouterState state) { + final segments = state.uri.pathSegments; + if (segments.length >= 3) { + final type = segments[1]; + final contentIdentifier = segments[2]; + + switch (type) { + case 'recipe': + FlutterNativeSplash.remove(); + return '${RouteConstants.aboutRecipesScreen}?slug=$contentIdentifier'; + case 'blog_post': + FlutterNativeSplash.remove(); + return '${RouteConstants.blogPostScreen}?id=$contentIdentifier'; + case 'plan': + FlutterNativeSplash.remove(); + return '${RouteConstants.aboutPlanScreen}?slug=$contentIdentifier'; + case 'diet': + FlutterNativeSplash.remove(); + return '${RouteConstants.aboutDietScreen}?slug=$contentIdentifier'; + } + } + + return null; + } } diff --git a/lib/core/constants/app_constants.dart b/lib/core/constants/app_constants.dart new file mode 100644 index 00000000..f9c945c9 --- /dev/null +++ b/lib/core/constants/app_constants.dart @@ -0,0 +1,72 @@ +/// Константы приложения +class AppConstants { + // Размеры + static const double defaultPadding = 16.0; + static const double smallPadding = 8.0; + static const double largePadding = 24.0; + static const double extraLargePadding = 32.0; + + // Радиусы + static const double defaultBorderRadius = 8.0; + static const double smallBorderRadius = 4.0; + static const double largeBorderRadius = 16.0; + + // Высоты + static const double defaultButtonHeight = 48.0; + static const double smallButtonHeight = 36.0; + static const double largeButtonHeight = 56.0; + + // Ширины + static const double maxContentWidth = 400.0; + static const double minContentWidth = 280.0; + + // Анимации + static const Duration defaultAnimationDuration = Duration(milliseconds: 300); + static const Duration fastAnimationDuration = Duration(milliseconds: 150); + static const Duration slowAnimationDuration = Duration(milliseconds: 500); + + // Таймауты + static const Duration defaultTimeout = Duration(seconds: 30); + static const Duration shortTimeout = Duration(seconds: 10); + static const Duration longTimeout = Duration(minutes: 2); + + // Лимиты + static const int maxImageSize = 5 * 1024 * 1024; // 5MB + static const int maxVideoSize = 100 * 1024 * 1024; // 100MB + static const int maxTextLength = 1000; + static const int maxTitleLength = 100; + static const int maxDescriptionLength = 500; + + // Пагинация + static const int defaultPageSize = 20; + static const int smallPageSize = 10; + static const int largePageSize = 50; + + // Форматы + static const List supportedImageFormats = ['jpg', 'jpeg', 'png', 'webp']; + static const List supportedVideoFormats = ['mp4', 'mov', 'avi']; + + // Регулярные выражения + static const String emailRegex = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'; + static const String phoneRegex = r'^\+?[1-9]\d{1,14}$'; + static const String urlRegex = r'^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$'; + + // Сообщения + static const String defaultErrorMessage = 'Произошла ошибка. Попробуйте еще раз.'; + static const String networkErrorMessage = 'Проблема с подключением к интернету.'; + static const String timeoutErrorMessage = 'Превышено время ожидания.'; + static const String unknownErrorMessage = 'Неизвестная ошибка.'; + + // Ключи для SharedPreferences + static const String welcomeScreenShownKey = 'welcomeScreenWasShowed'; + static const String userTokenKey = 'userToken'; + static const String userDataKey = 'userData'; + static const String settingsKey = 'settings'; + + // API endpoints + static const String apiVersion = '/api'; + static const String authEndpoint = '/auth'; + static const String usersEndpoint = '/users'; + static const String postsEndpoint = '/posts'; + static const String commentsEndpoint = '/comments'; +} diff --git a/lib/core/cubit/base_cubit.dart b/lib/core/cubit/base_cubit.dart new file mode 100644 index 00000000..3cd6c9d9 --- /dev/null +++ b/lib/core/cubit/base_cubit.dart @@ -0,0 +1,42 @@ +import 'dart:developer'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +/// Базовый класс для всех Cubit'ов с унифицированной обработкой ошибок +abstract class BaseCubit extends Cubit { + BaseCubit(super.initialState); + + /// Унифицированная обработка ошибок + void handleError(Object error, [StackTrace? stackTrace]) { + log('Error in ${runtimeType}: $error', stackTrace: stackTrace); + // Здесь можно добавить отправку ошибок в Sentry или другую систему мониторинга + } + + /// Безопасное выполнение асинхронных операций + Future safeExecute(Future Function() operation) async { + try { + return await operation(); + } catch (error, stackTrace) { + handleError(error, stackTrace); + return null; + } + } + + /// Безопасное выполнение асинхронных операций с обработкой состояния + Future safeExecuteWithState( + Future Function() operation, { + required State loadingState, + required State Function(T result) successState, + required State Function(Object error) errorState, + }) async { + try { + emit(loadingState); + final result = await operation(); + emit(successState(result)); + return result; + } catch (error, stackTrace) { + handleError(error, stackTrace); + emit(errorState(error)); + return null; + } + } +} diff --git a/lib/core/cubit/state_utils.dart b/lib/core/cubit/state_utils.dart new file mode 100644 index 00000000..9c6dfb78 --- /dev/null +++ b/lib/core/cubit/state_utils.dart @@ -0,0 +1,28 @@ +/// Утилиты для работы с состояниями Cubit'ов +class StateUtils { + /// Проверяет, является ли состояние загрузочным + static bool isLoading(dynamic state) { + return state.toString().contains('loading') || state.toString().contains('Loading'); + } + + /// Проверяет, является ли состояние загруженным + static bool isLoaded(dynamic state) { + return state.toString().contains('loaded') || state.toString().contains('Loaded'); + } + + /// Проверяет, является ли состояние ошибочным + static bool isError(dynamic state) { + return state.toString().contains('error') || state.toString().contains('Error'); + } + + /// Получает сообщение об ошибке из состояния + static String? getErrorMessage(dynamic state) { + if (isError(state)) { + // Попытка извлечь сообщение об ошибке из состояния + final stateString = state.toString(); + final errorMatch = RegExp(r'error[:\s]+([^,}]+)').firstMatch(stateString); + return errorMatch?.group(1)?.trim(); + } + return null; + } +} diff --git a/lib/core/routing/auth_routes.dart b/lib/core/routing/auth_routes.dart new file mode 100644 index 00000000..fc4558d1 --- /dev/null +++ b/lib/core/routing/auth_routes.dart @@ -0,0 +1,37 @@ +import 'package:eatvio_app/core/routing/route_constants.dart'; +import 'package:eatvio_app/features/auth/presentation/pages/auth_screen.dart'; +import 'package:eatvio_app/features/auth/presentation/pages/forget_password_screen.dart'; +import 'package:eatvio_app/features/auth/presentation/pages/scan_qr_code_screen.dart'; +import 'package:eatvio_app/features/registration/presentation/pages/registration_screen.dart'; +import 'package:eatvio_app/features/registration/presentation/pages/verify_screen.dart'; +import 'package:eatvio_app/features/registration/presentation/pages/verify_email_screen.dart'; +import 'package:go_router/go_router.dart'; + +/// Маршруты для аутентификации +class AuthRoutes { + /// Создание маршрутов аутентификации + static List createRoutes() { + return [ + GoRoute( + path: RouteConstants.authScreen, + name: 'authScreen', + builder: (context, state) => const AuthScreen(), + routes: [ + GoRoute(path: RouteConstants.registrationScreen, name: 'registrationScreen', builder: (context, state) => const RegistrationScreen()), + GoRoute( + path: RouteConstants.forgetPasswordScreen, + name: 'forgetPasswordScreen', + builder: (context, state) => ForgetPasswordScreen(isAuth: state.extra as bool), + ), + GoRoute(path: RouteConstants.scanQRCodeScreen, name: 'scanQRCodeScreen', builder: (context, state) => const ScanQRCodeScreen()), + ], + ), + GoRoute( + path: RouteConstants.verifyScreen, + name: 'verifyScreen', + builder: (context, state) => VerifyScreen(passKey: state.extra as String), + ), + GoRoute(path: RouteConstants.verifyEmailScreen, name: 'verifyEmailScreen', builder: (context, state) => const VerifyEmailScreen()), + ]; + } +} diff --git a/lib/core/routing/content_routes.dart b/lib/core/routing/content_routes.dart new file mode 100644 index 00000000..69d7ebf4 --- /dev/null +++ b/lib/core/routing/content_routes.dart @@ -0,0 +1,73 @@ +import 'package:eatvio_app/core/routing/route_constants.dart'; +import 'package:eatvio_app/features/blog_post/presentation/pages/blog_post_screen.dart'; +import 'package:eatvio_app/features/blog_post_create/presentation/pages/blog_post_create_screen.dart'; +import 'package:eatvio_app/features/diets/presentation/pages/about_diet_screen.dart'; +import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart'; +import 'package:eatvio_app/features/plans/presentation/pages/about_plan_screen.dart'; +import 'package:eatvio_app/features/recipe/presentation/pages/about_recipe_screen.dart'; +import 'package:go_router/go_router.dart'; + +/// Маршруты для контента +class ContentRoutes { + /// Создание маршрутов контента + static List createRoutes() { + return [ + GoRoute( + path: RouteConstants.aboutPlanScreen, + name: 'aboutPlanScreen', + builder: (context, state) { + final slug = state.uri.queryParameters['slug']; + if (slug != null) { + return AboutPlanScreen(slug: slug, navigateToComments: false, index: null); + } + + final data = state.extra as Map; + return AboutPlanScreen(slug: data['slug'] as String, index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool); + }, + ), + GoRoute( + path: RouteConstants.aboutRecipesScreen, + name: 'aboutRecipesScreen', + builder: (context, state) { + final slug = state.uri.queryParameters['slug']; + if (slug != null) { + return AboutRecipeScreen(recipeSlug: slug, navigateToComments: false, index: null); + } + + final data = state.extra as Map; + return AboutRecipeScreen(recipeSlug: data['recipe'] as String, index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool); + }, + ), + GoRoute( + path: RouteConstants.aboutDietScreen, + name: 'aboutDietScreen', + builder: (context, state) { + final slug = state.uri.queryParameters['slug']; + if (slug != null) { + return AboutDietScreen(slug: slug, index: null, navigateToComments: false); + } + final data = state.extra as Map; + return AboutDietScreen(slug: data['slug'] as String, index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool); + }, + ), + GoRoute( + path: RouteConstants.blogPostCreateScreen, + name: 'blogPostCreateScreen', + builder: (context, state) => BlogPostCreateScreen(feedBlogPostModel: state.extra != null ? state.extra as FeedBlogPostModel : null), + ), + GoRoute( + path: RouteConstants.blogPostScreen, + name: 'blogPostScreen', + builder: (context, state) { + final id = state.uri.queryParameters['id']; + final parsedId = int.tryParse(id ?? ''); + if (parsedId != null) { + return BlogPostScreen(id: parsedId); + } + + return BlogPostScreen(id: state.extra as int); + }, + ), + ]; + } +} diff --git a/lib/core/routing/main_navigation_routes.dart b/lib/core/routing/main_navigation_routes.dart new file mode 100644 index 00000000..818f625c --- /dev/null +++ b/lib/core/routing/main_navigation_routes.dart @@ -0,0 +1,94 @@ +import 'package:eatvio_app/core/routing/route_constants.dart'; +import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; +import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_state.dart'; +import 'package:eatvio_app/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart'; +import 'package:eatvio_app/features/chat/presentation/pages/chats_screen.dart'; +import 'package:eatvio_app/features/consultations/presentation/pages/consultations_screen.dart'; +import 'package:eatvio_app/features/diary/presentation/pages/diary_screen.dart'; +import 'package:eatvio_app/features/feed/presentation/pages/feed_screen.dart'; +import 'package:eatvio_app/features/menu/presentation/pages/menu_screen.dart'; +import 'package:eatvio_app/features/notifications/presentation/notifications_screen.dart'; +import 'package:eatvio_app/features/profile/presentation/pages/profile_screen.dart'; +import 'package:eatvio_app/core/locator_service.dart'; +import 'package:eatvio_app/core/websockets/ws_manager.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; +import 'dart:developer'; + +/// Маршруты для основной навигации +class MainNavigationRoutes { + /// Создание маршрутов основной навигации + static List createRoutes() { + return [ + ShellRoute( + builder: (context, state, child) { + return BlocBuilder( + builder: (BuildContext context, ProfileAvatarState state) { + if (state is ProfileAvatarStateLoaded) { + return BottomNavigationBarWidget(profileModel: state.profile, child: child); + } + return const SizedBox.shrink(); + }, + ); + }, + routes: [ + GoRoute( + path: RouteConstants.myProfileScreen, + name: 'myProfileScreen', + pageBuilder: (context, state) { + return const NoTransitionPage(child: ProfileScreen()); + }, + ), + GoRoute( + path: RouteConstants.feedScreen, + name: 'feedScreen', + pageBuilder: (context, state) => const NoTransitionPage(child: FeedScreen()), + redirect: (context, state) async { + try { + await sl().initialize(); + } catch (e, st) { + log(e.toString(), stackTrace: st); + } + return null; + }, + ), + GoRoute( + path: RouteConstants.diaryScreen, + name: 'diaryScreen', + pageBuilder: (context, state) => const NoTransitionPage(child: DiaryScreen()), + ), + GoRoute( + path: ConsultationsScreen.path, + name: ConsultationsScreen.name, + pageBuilder: (context, state) => const NoTransitionPage(child: ConsultationsScreen()), + ), + GoRoute( + path: RouteConstants.chatSelectScreen, + name: 'chatSelectScreen', + pageBuilder: (context, state) => const NoTransitionPage(child: ChatsScreen()), + ), + GoRoute( + path: RouteConstants.notificationsScreen, + name: 'notificationsScreen', + pageBuilder: (context, state) => const NoTransitionPage(child: NotificationsScreen()), + ), + GoRoute( + path: RouteConstants.menuScreen, + name: 'menuScreen', + pageBuilder: (context, state) => NoTransitionPage( + child: BlocBuilder( + builder: (BuildContext context, ProfileAvatarState state) { + if (state is ProfileAvatarStateLoaded) { + return MenuScreen(profileModel: state.profile); + } + return const SizedBox.shrink(); + }, + ), + ), + ), + ], + ), + ]; + } +} diff --git a/lib/core/routing/other_routes.dart b/lib/core/routing/other_routes.dart new file mode 100644 index 00000000..43cf885f --- /dev/null +++ b/lib/core/routing/other_routes.dart @@ -0,0 +1,229 @@ +import 'package:dio/dio.dart'; +import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/firebase_api.dart'; +import 'package:eatvio_app/core/locator_service.dart'; +import 'package:eatvio_app/core/routing/route_constants.dart'; +import 'package:eatvio_app/core/token_storage.dart'; +import 'package:eatvio_app/features/calendar/presentation/pages/calendar_screen.dart'; +import 'package:eatvio_app/features/chat/presentation/pages/chat_screen.dart'; +import 'package:eatvio_app/features/diary/presentation/pages/diary_screen.dart'; +import 'package:eatvio_app/features/chat/presentation/pages/empty_chat_screen.dart'; +import 'package:eatvio_app/features/chat/presentation/pages/payment_screen.dart'; +import 'package:eatvio_app/features/chat/presentation/pages/photo_view_screen.dart'; +import 'package:eatvio_app/features/consultant/become_consultant/presentation/pages/become_consultant_screen.dart'; +import 'package:eatvio_app/features/consultant/become_consultant/presentation/pages/stepper_become_consultant_screen.dart'; +import 'package:eatvio_app/features/consultant_rating/presentation/consultant_rating_screen.dart'; +import 'package:eatvio_app/features/create_diet/presentation/create_diet_screen.dart'; +import 'package:eatvio_app/features/create_diet/presentation/diet_preview_screen.dart'; +import 'package:eatvio_app/features/create_plan/presentation/create_plan_screen.dart'; +import 'package:eatvio_app/features/create_recipe/presentation/pages/recipe_create_screen.dart'; +import 'package:eatvio_app/features/dev_tools/presentation/dev_tools_screen.dart'; +import 'package:eatvio_app/features/feed/models/recipes_model.dart'; +import 'package:eatvio_app/features/glossary/presentation/pages/glossary_screen.dart'; +import 'package:eatvio_app/features/menu/presentation/pages/blocked_users_screen.dart'; +import 'package:eatvio_app/features/menu/presentation/pages/notifications_settings_screen.dart'; +import 'package:eatvio_app/features/my_diets/presentation/my_diets_screen.dart'; +import 'package:eatvio_app/features/my_finance/presentation/pages/card_settings.dart'; +import 'package:eatvio_app/features/my_finance/presentation/pages/user_finance_screen.dart'; +import 'package:eatvio_app/features/my_plans/presentation/my_plans_screen.dart'; +import 'package:eatvio_app/features/order_info/presentation/info_by_order_screen.dart'; +import 'package:eatvio_app/features/profile/presentation/pages/profile_screen.dart'; +import 'package:eatvio_app/features/profile/presentation/pages/subs_screen.dart'; +import 'package:eatvio_app/features/recipe/presentation/pages/my_recipes_screen.dart'; +import 'package:eatvio_app/features/support/presentation/pages/support_screen.dart'; +import 'package:eatvio_app/features/update_screen/presentation/update_screen.dart'; +import 'package:eatvio_app/features/user_details/presentation/user_details_screen.dart'; +import 'package:eatvio_app/features/welcome/presentation/pages/welcome_screen.dart'; +import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/consultant_finances_screen.dart'; +import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/consultations_archive_screen.dart'; +import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/user_purchases_screen.dart'; +import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; +import 'package:go_router/go_router.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:version/version.dart'; + +/// Остальные маршруты приложения +class OtherRoutes { + /// Создание остальных маршрутов + static List createRoutes() { + return [ + // Welcome and update screens + GoRoute( + path: RouteConstants.welcomeScreen, + name: 'welcomeScreen', + builder: (context, state) => const WelcomeScreen(), + redirect: (context, state) async { + try { + final sharedPreferences = await SharedPreferences.getInstance(); + final welcomeScreenWasShowed = sharedPreferences.getBool('welcomeScreenWasShowed') ?? false; + await sl().registerPushToken(); + if (welcomeScreenWasShowed == false) { + return null; + } else if (await TokenStorage.haveTokenInStorage()) { + return RouteConstants.feedScreen; + } else { + return RouteConstants.authScreen; + } + } catch (e) { + return RouteConstants.feedScreen; + } + }, + ), + GoRoute( + path: RouteConstants.updateScreen, + name: 'updateScreen', + builder: (context, state) => const UpdateScreen(), + redirect: (context, state) async { + try { + final versions = (await sl().get('$baseUrl/api/app_versions')).data; + PackageInfo packageInfo = await PackageInfo.fromPlatform(); + String version = packageInfo.version; + Version currentVersion = Version.parse(version); + Version latestVersion = Version.parse(versions['min_version']); + if (latestVersion > currentVersion) { + return null; + } else { + return RouteConstants.welcomeScreen; + } + } catch (e) { + return RouteConstants.welcomeScreen; + } + }, + ), + + // Archive routes + GoRoute(path: ConsultationsArchiveScreen.path, name: ConsultationsArchiveScreen.name, builder: (context, state) => const ConsultationsArchiveScreen()), + GoRoute(path: ConsultantFinancesScreen.path, name: ConsultantFinancesScreen.name, builder: (context, state) => const ConsultantFinancesScreen()), + GoRoute(path: UserPurchasesScreen.path, name: UserPurchasesScreen.name, builder: (context, state) => const UserPurchasesScreen()), + + // Finance routes + GoRoute(path: RouteConstants.userFinanceScreen, name: 'userFinanceScreen', builder: (context, state) => const UserFinanceScreen()), + GoRoute( + path: CardSettingsScreen.path, + name: CardSettingsScreen.name, + builder: (context, state) { + final chosenPlanId = state.extra as int?; + return CardSettingsScreen(chosenPlanId: chosenPlanId); + }, + ), + + // Content creation routes + GoRoute( + path: RecipeCreateScreen.path, + name: RecipeCreateScreen.name, + builder: (context, state) => RecipeCreateScreen(recipe: state.extra != null ? state.extra as Recipe : null), + ), + GoRoute( + path: CreateDietScreen.path, + name: CreateDietScreen.name, + builder: (context, state) { + final diet = state.extra as Diet?; + return CreateDietScreen(diet: diet); + }, + ), + GoRoute( + path: DietPreviewScreen.path, + name: DietPreviewScreen.name, + builder: (context, state) { + final data = state.extra as Diet; + return DietPreviewScreen(diet: data); + }, + ), + GoRoute(path: RouteConstants.createPlanScreen, name: 'createPlanScreen', builder: (context, state) => const CreatePlanScreen()), + GoRoute(path: MyDietsScreen.path, name: MyDietsScreen.name, builder: (context, state) => const MyDietsScreen()), + GoRoute(path: MyPlansScreen.path, name: MyPlansScreen.name, builder: (context, state) => const MyPlansScreen()), + GoRoute(path: MyRecipesScreen.path, name: MyRecipesScreen.name, builder: (context, state) => const MyRecipesScreen()), + + // Profile routes + GoRoute( + path: RouteConstants.profileScreen, + name: 'profileScreen', + builder: (context, state) => ProfileScreen(author: state.extra as Author), + ), + GoRoute( + path: RouteConstants.subsScreen, + name: 'subsScreen', + builder: (context, state) => SubsScreen(slug: state.pathParameters['slug']!, subType: state.extra as SubType), + ), + + // Chat routes + GoRoute( + path: RouteConstants.chatScreen, + name: 'chatScreen', + builder: (context, state) { + final data = state.extra as Map; + final uuid = data['uuid'] as String; + final sendInitialMessage = data['sendInitialMessage'] as bool?; + return ChatScreen(uuid: uuid, sendInitialMessage: sendInitialMessage); + }, + ), + GoRoute( + path: RouteConstants.emptyChatScreen, + name: 'emptyChatScreen', + builder: (context, state) => EmptyChatScreen(author: state.extra as Author), + ), + + // Consultant routes + GoRoute(path: RouteConstants.becomeConsultantScreen, name: 'becomeConsultantScreen', builder: (context, state) => const BecomeConsultantScreen()), + GoRoute( + path: RouteConstants.stepperBecomeConsultantScreen, + name: 'stepperBecomeConsultantScreen', + builder: (context, state) => StepperBecomeConsultantScreen(slug: state.extra == null ? null : state.extra as String), + ), + GoRoute(path: RouteConstants.calendarScreen, name: 'calendarScreen', builder: (context, state) => const CalendarScreen()), + GoRoute(path: RouteConstants.supportScreen, name: 'supportScreen', builder: (context, state) => const SupportScreen()), + GoRoute( + path: RouteConstants.photoViewScreen, + name: 'photoViewScreen', + builder: (context, state) => PhotoViewScreen(imageUrls: state.extra as List, index: int.parse(state.pathParameters['index']!)), + ), + + // Other routes + GoRoute(path: RouteConstants.glossaryScreen, name: 'glossaryScreen', builder: (context, state) => const GlossaryScreen()), + GoRoute(path: RouteConstants.blockedUsersScreen, name: 'blockedUsersScreen', builder: (context, state) => const BlockedUsersScreen()), + GoRoute( + path: RouteConstants.notificationsSettingsScreen, + name: 'notificationsSettingsScreen', + builder: (context, state) => const NotificationsSettingsScreen(), + ), + GoRoute( + path: UserDetailsScreen.path, + name: UserDetailsScreen.name, + builder: (context, state) => UserDetailsScreen(slug: state.extra as String), + ), + GoRoute(path: DevToolsScreen.path, name: DevToolsScreen.name, builder: (context, state) => const DevToolsScreen()), + GoRoute(path: ConsultantRatingScreen.path, name: ConsultantRatingScreen.name, builder: (context, state) => const ConsultantRatingScreen()), + GoRoute( + path: InfoByOrderScreen.path, + name: InfoByOrderScreen.name, + builder: (context, state) { + final data = state.extra as Map; + final planId = data['planId'] as int; + final successfulPayment = data['successfulPayment'] as bool?; + return InfoByOrderScreen(planId: planId, successfulPayment: successfulPayment); + }, + ), + GoRoute( + path: PaymentScreen.path, + name: PaymentScreen.name, + builder: (context, state) { + final data = state.extra as Map; + final planId = data['planId'] as int; + final url = data['url'] as String; + return PaymentScreen(url: url, planId: planId); + }, + ), + GoRoute( + path: UserDiaryScreen.path, + name: UserDiaryScreen.name, + builder: (context, state) { + final data = state.extra as Map; + final diaryId = data['diaryId'] as int; + final currentPlanDay = data['currentDiaryDay'] as int; + return UserDiaryScreen(diaryId: diaryId, currentPlanDay: currentPlanDay); + }, + ), + ]; + } +} diff --git a/lib/core/routing/route_constants.dart b/lib/core/routing/route_constants.dart new file mode 100644 index 00000000..dcc82c17 --- /dev/null +++ b/lib/core/routing/route_constants.dart @@ -0,0 +1,84 @@ +/// Константы маршрутов приложения +class RouteConstants { + // Auth routes + static const String authScreen = '/auth-screen'; + static const String registrationScreen = 'registration-screen'; + static const String forgetPasswordScreen = 'forget-password-screen'; + static const String scanQRCodeScreen = 'scan-qr-code-screen'; + static const String verifyScreen = '/verify-screen'; + static const String verifyEmailScreen = '/verify-email-screen'; + + // Main navigation routes + static const String myProfileScreen = '/my-profile-screen'; + static const String feedScreen = '/feed-screen'; + static const String diaryScreen = '/diary-screen'; + static const String consultationsScreen = '/consultations-screen'; + static const String chatSelectScreen = '/chat-select-screen'; + static const String notificationsScreen = '/notifications-screen'; + static const String menuScreen = '/menu-screen'; + + // Content routes + static const String aboutPlanScreen = '/about-plan-screen'; + static const String aboutRecipesScreen = '/about-recipes-screen'; + static const String aboutDietScreen = '/about-diet-screen'; + static const String blogPostScreen = '/blog-post-screen'; + static const String blogPostCreateScreen = '/blog-post-create-screen'; + + // Profile routes + static const String profileScreen = '/profile-screen'; + static const String subsScreen = '/subs-screen'; + + // Settings routes + static const String settingsScreen = '/settings-screen'; + static const String generalInfoScreen = 'general-info-screen'; + static const String otherInfoScreen = 'other-info-screen'; + static const String physicalActivityScreen = 'physical-activity-screen'; + static const String physicalFormScreen = 'physical-form-screen'; + static const String pressureScreen = 'pressure-screen'; + static const String changePasswordScreen = 'change-password-screen'; + static const String changeEmailScreen = 'change-email-screen'; + + // Consultant routes + static const String becomeConsultantScreen = '/become-consultant-screen'; + static const String stepperBecomeConsultantScreen = '/stepper-become-consultant-screen'; + static const String consultantRatingScreen = '/consultant-rating-screen'; + + // Other routes + static const String welcomeScreen = '/welcome-screen'; + static const String updateScreen = '/update-screen'; + static const String calendarScreen = '/calendar-screen'; + static const String supportScreen = '/support-screen'; + static const String photoViewScreen = '/photo-view-screen'; + static const String chatScreen = '/chat-screen'; + static const String emptyChatScreen = '/empty-chat-screen'; + static const String glossaryScreen = '/glossary-screen'; + static const String blockedUsersScreen = '/blocked-users-screen'; + static const String notificationsSettingsScreen = '/notifications-settings-screen'; + + // Finance routes + static const String userFinanceScreen = '/user-finance-screen'; + static const String cardSettingsScreen = '/card-settings-screen'; + static const String orderInfoScreen = '/order-info-screen'; + static const String paymentScreen = '/payment-screen'; + + // Content creation routes + static const String recipeCreateScreen = '/recipe-create-screen'; + static const String createDietScreen = '/create-diet-screen'; + static const String dietPreviewScreen = '/diet-preview-screen'; + static const String createPlanScreen = '/create-plan-screen'; + static const String myDietsScreen = '/my-diets-screen'; + static const String myPlansScreen = '/my-plans-screen'; + static const String myRecipesScreen = '/my-recipes-screen'; + + // User details + static const String userDetailsScreen = '/user-details-screen'; + static const String userDiaryScreen = '/user-diary-screen'; + + // Dev tools + static const String devToolsScreen = '/dev-tools-screen'; + + // Archive routes + static const String consultationsArchiveScreen = '/consultations-archive-screen'; + static const String consultantFinancesScreen = '/consultant-finances-screen'; + static const String userPurchasesScreen = '/user-purchases-screen'; +} diff --git a/lib/core/routing/settings_routes.dart b/lib/core/routing/settings_routes.dart new file mode 100644 index 00000000..8316617c --- /dev/null +++ b/lib/core/routing/settings_routes.dart @@ -0,0 +1,62 @@ +import 'package:eatvio_app/core/routing/route_constants.dart'; +import 'package:eatvio_app/features/settings/entities/settings_entity.dart'; +import 'package:eatvio_app/features/settings/presentation/pages/change_email_screen.dart'; +import 'package:eatvio_app/features/settings/presentation/pages/change_password_screen.dart'; +import 'package:eatvio_app/features/settings/presentation/pages/general_info_screen.dart'; +import 'package:eatvio_app/features/settings/presentation/pages/other_info_screen.dart'; +import 'package:eatvio_app/features/settings/presentation/pages/physical_activity_screen.dart'; +import 'package:eatvio_app/features/settings/presentation/pages/physical_form_screen.dart'; +import 'package:eatvio_app/features/settings/presentation/pages/pressure_screen.dart'; +import 'package:eatvio_app/features/settings/presentation/pages/settings_screen.dart'; +import 'package:go_router/go_router.dart'; + +/// Маршруты для настроек +class SettingsRoutes { + /// Создание маршрутов настроек + static List createRoutes() { + return [ + GoRoute( + path: RouteConstants.settingsScreen, + name: 'settingsScreen', + builder: (context, state) => const SettingsScreen(), + routes: [ + GoRoute( + path: RouteConstants.generalInfoScreen, + name: 'generalInfoScreen', + builder: (context, state) => GeneralInfoScreen(settingsEntity: state.extra as SettingsEntity), + ), + GoRoute( + path: RouteConstants.otherInfoScreen, + name: 'otherInfoScreen', + builder: (context, state) => OtherInfoScreen(settingsEntity: state.extra as SettingsEntity), + ), + GoRoute( + path: RouteConstants.physicalActivityScreen, + name: 'physicalActivityScreen', + builder: (context, state) => PhysicalActivityScreen(settingsEntity: state.extra as SettingsEntity), + ), + GoRoute( + path: RouteConstants.physicalFormScreen, + name: 'physicalFormScreen', + builder: (context, state) => PhysicalFormScreen(settingsEntity: state.extra as SettingsEntity), + ), + GoRoute( + path: RouteConstants.pressureScreen, + name: 'pressureScreen', + builder: (context, state) => PressureScreen(settingsEntity: state.extra as SettingsEntity), + ), + GoRoute( + path: RouteConstants.changePasswordScreen, + name: 'changePasswordScreen', + builder: (context, state) => ChangePasswordScreen(settingsEntity: state.extra as SettingsEntity), + ), + GoRoute( + path: RouteConstants.changeEmailScreen, + name: 'changeEmailScreen', + builder: (context, state) => ChangeEmailScreen(settingsEntity: state.extra as SettingsEntity), + ), + ], + ), + ]; + } +} diff --git a/lib/core/utils/validation_utils.dart b/lib/core/utils/validation_utils.dart new file mode 100644 index 00000000..1add6cda --- /dev/null +++ b/lib/core/utils/validation_utils.dart @@ -0,0 +1,87 @@ +import 'package:eatvio_app/core/constants/app_constants.dart'; + +/// Утилиты для валидации +class ValidationUtils { + /// Валидация email + static bool isValidEmail(String email) { + if (email.isEmpty) return false; + final regex = RegExp(AppConstants.emailRegex); + return regex.hasMatch(email); + } + + /// Валидация телефона + static bool isValidPhone(String phone) { + if (phone.isEmpty) return false; + final regex = RegExp(AppConstants.phoneRegex); + return regex.hasMatch(phone); + } + + /// Валидация URL + static bool isValidUrl(String url) { + if (url.isEmpty) return false; + final regex = RegExp(AppConstants.urlRegex); + return regex.hasMatch(url); + } + + /// Валидация длины текста + static bool isValidTextLength(String text, {int? maxLength}) { + final length = maxLength ?? AppConstants.maxTextLength; + return text.length <= length; + } + + /// Валидация длины заголовка + static bool isValidTitleLength(String title) { + return title.isNotEmpty && title.length <= AppConstants.maxTitleLength; + } + + /// Валидация длины описания + static bool isValidDescriptionLength(String description) { + return description.length <= AppConstants.maxDescriptionLength; + } + + /// Валидация пароля + static bool isValidPassword(String password) { + // Минимум 8 символов, хотя бы одна буква и одна цифра + if (password.length < 8) return false; + final hasLetter = RegExp(r'[a-zA-Z]').hasMatch(password); + final hasDigit = RegExp(r'[0-9]').hasMatch(password); + return hasLetter && hasDigit; + } + + /// Валидация имени пользователя + static bool isValidUsername(String username) { + // От 3 до 20 символов, только буквы, цифры и подчеркивания + if (username.length < 3 || username.length > 20) return false; + final regex = RegExp(r'^[a-zA-Z0-9_]+$'); + return regex.hasMatch(username); + } + + /// Получение сообщения об ошибке валидации + static String? getValidationErrorMessage(String? value, ValidationType type) { + if (value == null || value.isEmpty) { + return 'Поле не может быть пустым'; + } + + switch (type) { + case ValidationType.email: + return isValidEmail(value) ? null : 'Введите корректный email'; + case ValidationType.phone: + return isValidPhone(value) ? null : 'Введите корректный номер телефона'; + case ValidationType.url: + return isValidUrl(value) ? null : 'Введите корректный URL'; + case ValidationType.password: + return isValidPassword(value) ? null : 'Пароль должен содержать минимум 8 символов, включая буквы и цифры'; + case ValidationType.username: + return isValidUsername(value) ? null : 'Имя пользователя должно содержать от 3 до 20 символов (буквы, цифры, подчеркивания)'; + case ValidationType.title: + return isValidTitleLength(value) ? null : 'Заголовок не может быть пустым и должен содержать не более ${AppConstants.maxTitleLength} символов'; + case ValidationType.description: + return isValidDescriptionLength(value) ? null : 'Описание не может содержать более ${AppConstants.maxDescriptionLength} символов'; + case ValidationType.text: + return isValidTextLength(value) ? null : 'Текст не может содержать более ${AppConstants.maxTextLength} символов'; + } + } +} + +/// Типы валидации +enum ValidationType { email, phone, url, password, username, title, description, text } diff --git a/lib/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_cubit.dart b/lib/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_cubit.dart index df228298..6e93944a 100644 --- a/lib/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_cubit.dart +++ b/lib/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_cubit.dart @@ -1,61 +1,34 @@ -import 'dart:developer'; - +import 'package:eatvio_app/core/cubit/base_cubit.dart'; import 'package:eatvio_app/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_state.dart'; import 'package:eatvio_app/features/blog_post_create/repositories/blog_post_repository.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_image_compress/flutter_image_compress.dart'; -class BlogPostCreateCubit extends Cubit { +class BlogPostCreateCubit extends BaseCubit { final BlogPostRepository blogPostRepository; - BlogPostCreateCubit({ - required this.blogPostRepository, - }) : super(const BlogPostCreateState()); + BlogPostCreateCubit({required this.blogPostRepository}) : super(const BlogPostCreateState()); - Future createBlogPost({ - String? title, - String? videoUrl, - String? description, - XFile? image, - }) async { + Future createBlogPost({String? title, String? videoUrl, String? description, XFile? image}) async { try { emit(const BlogPostCreateState.loading()); - await blogPostRepository.createBlogPost( - title, - videoUrl, - description, - image, - ); + await blogPostRepository.createBlogPost(title, videoUrl, description, image); + emit(const BlogPostCreateState.loaded()); return true; - } catch (e, st) { - log(e.toString(), stackTrace: st); + } catch (error, stackTrace) { + handleError(error, stackTrace); emit(const BlogPostCreateState.loaded()); return false; } } - Future updateBlogPost({ - required int id, - String? title, - String? videoUrl, - String? description, - XFile? image, - String? oldImageUrl, - }) async { + Future updateBlogPost({required int id, String? title, String? videoUrl, String? description, XFile? image, String? oldImageUrl}) async { try { emit(const BlogPostCreateState.loading()); - await blogPostRepository.updateBlogPost( - id, - title, - videoUrl, - description, - image, - oldImageUrl, - ); + await blogPostRepository.updateBlogPost(id, title, videoUrl, description, image, oldImageUrl); emit(const BlogPostCreateState.loaded()); return true; - } catch (e, st) { - log(e.toString(), stackTrace: st); + } catch (error, stackTrace) { + handleError(error, stackTrace); emit(const BlogPostCreateState.loaded()); return false; } @@ -65,8 +38,8 @@ class BlogPostCreateCubit extends Cubit { try { await blogPostRepository.deleteBlogPost(id); return true; - } catch (e, st) { - log(e.toString(), stackTrace: st); + } catch (error, stackTrace) { + handleError(error, stackTrace); return false; } } From 766d5ba248fd573f9a6088227adab8296aa84f73 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Thu, 2 Oct 2025 00:07:28 +0300 Subject: [PATCH 47/78] fix: cubit --- lib/core/di/cubit_registrations.dart | 41 ++++++++-------------------- 1 file changed, 11 insertions(+), 30 deletions(-) diff --git a/lib/core/di/cubit_registrations.dart b/lib/core/di/cubit_registrations.dart index 156e43b1..babff4f6 100644 --- a/lib/core/di/cubit_registrations.dart +++ b/lib/core/di/cubit_registrations.dart @@ -23,6 +23,7 @@ import 'package:eatvio_app/features/feed/presentation/bloc/consultants_cubit/con import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_cubit.dart'; +import 'package:eatvio_app/features/feed/presentation/bloc/profile_cubit/profile_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes_cubit.dart'; import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/archive/consultations_archive_cubit.dart'; import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/consultant_finances/consultant_finances_cubit.dart'; @@ -60,15 +61,9 @@ class CubitRegistrations { /// Регистрация Cubit'ов как синглтонов static void _registerSingletonCubits() { sl.registerLazySingleton(() => NewMessagesCubit()); - sl.registerLazySingleton( - () => NotificationsCubit(wsManager: sl(), notificationsRepository: sl()), - ); - sl.registerLazySingleton( - () => ProfileAvatarCubit(profileRepository: sl()), - ); - sl.registerLazySingleton( - () => ChatsCubit(chatRepository: sl(), wsManager: sl(), profileRepository: sl()), - ); + sl.registerLazySingleton(() => NotificationsCubit(wsManager: sl(), notificationsRepository: sl())); + sl.registerLazySingleton(() => ProfileAvatarCubit(profileRepository: sl())); + sl.registerLazySingleton(() => ChatsCubit(chatRepository: sl(), wsManager: sl(), profileRepository: sl())); } /// Регистрация Cubit'ов как фабрик @@ -81,34 +76,25 @@ class CubitRegistrations { // Feed related cubits sl.registerFactory(() => FeedCubit(feedRepository: sl())); - sl.registerFactory( - () => RecipesCubit(feedRepository: sl(), recipeRepository: sl(), aboutRecipeRepository: sl()), - ); + sl.registerFactory(() => RecipesCubit(feedRepository: sl(), recipeRepository: sl(), aboutRecipeRepository: sl())); sl.registerFactory(() => DietsCubit(feedRepository: sl(), dietsRepository: sl())); sl.registerFactory(() => PlansCubit(feedRepository: sl(), planRepository: sl())); sl.registerFactory(() => ConsultantsCubit(feedRepository: sl())); sl.registerFactory(() => CommentsCubit(feedRepository: sl(), commentsRepository: sl())); + sl.registerFactory(() => FeedProfileCubit(profileRepository: sl())); // Chat related cubits - sl.registerFactory( - () => ChatRoomCubit(cardsRepository: sl(), chatRepository: sl(), wsManager: sl()), - ); + sl.registerFactory(() => ChatRoomCubit(cardsRepository: sl(), chatRepository: sl(), wsManager: sl())); // Profile related cubits - sl.registerFactory( - () => ProfileCubit(profileRepository: sl(), chatRepository: sl(), consultantRepository: sl()), - ); + sl.registerFactory(() => ProfileCubit(profileRepository: sl(), chatRepository: sl(), consultantRepository: sl())); sl.registerFactory(() => profile_plans.PlansCubit(plansRepository: sl())); sl.registerFactory(() => SubCubit(profileRepository: sl())); // Content creation cubits - sl.registerFactory( - () => CreateDietCubit(profileRepository: sl(), createDietRepository: sl(), glossaryRepository: sl()), - ); + sl.registerFactory(() => CreateDietCubit(profileRepository: sl(), createDietRepository: sl(), glossaryRepository: sl())); sl.registerFactory(() => CreatePlanCubit(createPlanRepository: sl())); - sl.registerFactory( - () => CreateRecipeCubit(recipeRepository: sl(), glossaryRepository: sl()), - ); + sl.registerFactory(() => CreateRecipeCubit(recipeRepository: sl(), glossaryRepository: sl())); sl.registerFactory(() => BlogPostCreateCubit(blogPostRepository: sl())); // Content viewing cubits @@ -135,12 +121,7 @@ class CubitRegistrations { ), ); sl.registerFactory( - () => SettingsCubit( - profileRepository: sl(), - settingsRepository: sl(), - consultantRepository: sl(), - consultationsRepository: sl(), - ), + () => SettingsCubit(profileRepository: sl(), settingsRepository: sl(), consultantRepository: sl(), consultationsRepository: sl()), ); sl.registerFactory(() => RegisterConsultantCubit(consultantRepository: sl())); sl.registerFactory(() => ComplainingCubit(repository: sl())); From 086e6cf94ea6aefad278ff2255eb1b2ff0f3fa30 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Thu, 2 Oct 2025 00:13:35 +0300 Subject: [PATCH 48/78] fix: recipe --- lib/core/di/repository_registrations.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/core/di/repository_registrations.dart b/lib/core/di/repository_registrations.dart index 71d1523d..6d467810 100644 --- a/lib/core/di/repository_registrations.dart +++ b/lib/core/di/repository_registrations.dart @@ -12,6 +12,7 @@ import 'package:eatvio_app/features/consultant_rating/repositories/consultant_ra import 'package:eatvio_app/features/consultations/repositories/consultations_repository.dart'; import 'package:eatvio_app/features/create_diet/repositories/create_diet_repository.dart'; import 'package:eatvio_app/features/create_plan/repositories/create_plan_repository.dart'; +import 'package:eatvio_app/features/create_recipe/reporitories/recipe_repository.dart'; import 'package:eatvio_app/features/diary/repositories/diary_repository.dart'; import 'package:eatvio_app/features/diets/repositories/diet_repository.dart'; import 'package:eatvio_app/features/feed/repositories/feed_repository.dart'; @@ -56,6 +57,7 @@ class RepositoryRegistrations { sl.registerLazySingleton(() => PlansRepository(plansService: sl())); sl.registerLazySingleton(() => PlanRepository(planService: sl())); sl.registerLazySingleton(() => AboutRecipeRepository(recipeService: sl())); + sl.registerLazySingleton(() => RecipeRepository(recipeService: sl())); sl.registerLazySingleton(() => ComplainingRepository(service: sl())); sl.registerLazySingleton(() => AuthRepository(authService: sl())); sl.registerLazySingleton(() => CardsRepository(cardsService: sl())); From 09d025252d6587bde222d797b158462aa6df7711 Mon Sep 17 00:00:00 2001 From: misha Date: Thu, 2 Oct 2025 10:25:10 +0300 Subject: [PATCH 49/78] feat(fixes): fixed analyzer issue --- lib/core/locator_service.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/core/locator_service.dart b/lib/core/locator_service.dart index ffe747a9..c9c61e56 100644 --- a/lib/core/locator_service.dart +++ b/lib/core/locator_service.dart @@ -46,7 +46,6 @@ import 'package:eatvio_app/features/blog_post_create/services/blog_post_service. import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/new_messages_cubit/new_messages_cubit.dart'; import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/consultant_finances/consultant_finances_cubit.dart'; import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/user_purchases/user_purchases_cubit.dart'; -import 'package:eatvio_app/features/internet_connection_checker/internet_connection_checker_screen.dart'; import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_cubit.dart'; import 'package:eatvio_app/features/menu/presentation/bloc/notification_settings_cubit/notification_settings_cubit.dart'; import 'package:eatvio_app/features/menu/presentation/repositories/blocked_users_repository.dart'; From 5ee86ddc9200d5e19504bb3cff6fd9826025ffda Mon Sep 17 00:00:00 2001 From: misha Date: Fri, 3 Oct 2025 15:40:05 +0300 Subject: [PATCH 50/78] feat(diary): fixed wrong cpfc loading after changing a portion --- ...otlin-compiler-10039299475187077818.salive | 0 .../presentation/pages/diary_screen.dart | 4 +- .../widgets/add_product_modal.dart | 34 +++--- ...ssary_moderation_request_modal_widget.dart | 115 +++++------------- .../diary/repositories/diary_repository.dart | 2 +- 5 files changed, 53 insertions(+), 102 deletions(-) delete mode 100644 android/.kotlin/sessions/kotlin-compiler-10039299475187077818.salive diff --git a/android/.kotlin/sessions/kotlin-compiler-10039299475187077818.salive b/android/.kotlin/sessions/kotlin-compiler-10039299475187077818.salive deleted file mode 100644 index e69de29b..00000000 diff --git a/lib/features/diary/presentation/pages/diary_screen.dart b/lib/features/diary/presentation/pages/diary_screen.dart index 6b636857..ae6f9e10 100644 --- a/lib/features/diary/presentation/pages/diary_screen.dart +++ b/lib/features/diary/presentation/pages/diary_screen.dart @@ -62,7 +62,9 @@ class DiaryScreenContent extends StatelessWidget { log(state.diary.id.toString()); } if (state is DiaryStateError) { - return Scaffold(body: DataErrorWidget(onReload: () => context.read().fetchDiaryV2(needLoading: false, diaryId: diaryId))); + return Scaffold( + body: DataErrorWidget(onReload: () => context.read().fetchDiaryV2(needLoading: false, diaryId: diaryId)), + ); } return Scaffold( diff --git a/lib/features/diary/presentation/widgets/add_product_modal.dart b/lib/features/diary/presentation/widgets/add_product_modal.dart index 9b6c2536..4b0a3806 100644 --- a/lib/features/diary/presentation/widgets/add_product_modal.dart +++ b/lib/features/diary/presentation/widgets/add_product_modal.dart @@ -25,26 +25,30 @@ class _AddProductModalState extends State { final quantityController = TextEditingController(); GlossaryModel? selectedGlossary; List glossariesList = []; - + late final String? _previousProductName; @override void initState() { super.initState(); if (widget.portion != null) { + _previousProductName = widget.portion!.productName; productNameController.text = widget.portion!.productName; quantityController.text = '${widget.portion!.quantity}'; - selectedGlossary = GlossaryModel( - id: widget.portion!.id, - name: widget.portion!.productName, - grInPortion: widget.portion!.portion == 'gr' ? 0 : 1, - energyOn100Gr: 0, - proteinsOn100Gr: 0, - fatsOn100Gr: 0, - carbohydratesOn100Gr: 0, - energyOnPortion: 0, - proteinsOnPortion: 0, - fatsOnPortion: 0, - carbohydratesOnPortion: 0, - ); + final productNutritionalValueId = widget.portion?.productNutritionalValueId; + if (productNutritionalValueId != null) { + selectedGlossary = GlossaryModel( + id: productNutritionalValueId, + name: widget.portion!.productName, + grInPortion: widget.portion!.portion == 'gr' ? 0 : 1, + energyOn100Gr: 0, + proteinsOn100Gr: 0, + fatsOn100Gr: 0, + carbohydratesOn100Gr: 0, + energyOnPortion: 0, + proteinsOnPortion: 0, + fatsOnPortion: 0, + carbohydratesOnPortion: 0, + ); + } } } @@ -191,7 +195,7 @@ class _AddProductModalState extends State { ); } Navigator.of(context).pop(); - if (selectedGlossary == null) { + if (selectedGlossary == null && _previousProductName != productNameController.text) { showModalBottomSheet( isScrollControlled: true, useRootNavigator: true, diff --git a/lib/features/diary/presentation/widgets/glossary_moderation_request_modal_widget.dart b/lib/features/diary/presentation/widgets/glossary_moderation_request_modal_widget.dart index 69b316a7..c9359cd9 100644 --- a/lib/features/diary/presentation/widgets/glossary_moderation_request_modal_widget.dart +++ b/lib/features/diary/presentation/widgets/glossary_moderation_request_modal_widget.dart @@ -9,11 +9,7 @@ class GlossaryModerationRequestModalWidget extends StatefulWidget { final DiaryCubit diaryCubit; final String productName; - const GlossaryModerationRequestModalWidget({ - super.key, - required this.productName, - required this.diaryCubit, - }); + const GlossaryModerationRequestModalWidget({super.key, required this.productName, required this.diaryCubit}); @override State createState() => _GlossaryModerationRequestModalWidgetState(); @@ -55,21 +51,13 @@ class _GlossaryModerationRequestModalWidgetState extends State data = ProductInMeal(productName: productName, portion: portion, quantity: quantity).toJson(); - data['product_nutritional_value_id'] = productNutritionalValueId; + if (productNutritionalValueId != null) data['product_nutritional_value_id'] = productNutritionalValueId; data['state'] = portionState.name; return await diaryService.updateProductInMeal(diary: diaryId, typeNo: mealId, portionModel: portionId, productInMeal: data); } From 37cedb223858a75f4b0e9994972673ac2b418143 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Fri, 3 Oct 2025 16:57:22 +0300 Subject: [PATCH 51/78] fix(rating_consultant): Issue-513 --- assets/icons/consultant_rating_icon.svg | 1 + lib/core/di/cubit_registrations.dart | 2 +- .../models/statistics_model.dart | 1 + .../consultant_rating_screen.dart | 11 ++- .../cubit/consultant_rating_cubit.dart | 10 ++- .../cubit/consultant_rating_state.dart | 1 + .../widgets/statistic_widget.dart | 2 +- .../feed/repositories/feed_repository.dart | 4 +- lib/features/feed/services/feed_service.dart | 79 +++++-------------- .../menu/presentation/pages/menu_screen.dart | 2 +- lib/generated/intl/messages_en.dart | 3 + lib/generated/l10n.dart | 10 +++ lib/l10n/intl_en.arb | 1 + 13 files changed, 56 insertions(+), 71 deletions(-) create mode 100644 assets/icons/consultant_rating_icon.svg diff --git a/assets/icons/consultant_rating_icon.svg b/assets/icons/consultant_rating_icon.svg new file mode 100644 index 00000000..493b4d95 --- /dev/null +++ b/assets/icons/consultant_rating_icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/lib/core/di/cubit_registrations.dart b/lib/core/di/cubit_registrations.dart index babff4f6..ec2de9ce 100644 --- a/lib/core/di/cubit_registrations.dart +++ b/lib/core/di/cubit_registrations.dart @@ -135,7 +135,7 @@ class CubitRegistrations { sl.registerFactory(() => UserPurchasesCubit(consultationsRepository: sl())); sl.registerFactory(() => BlockedUsersCubit(blockedUsersRepository: sl())); sl.registerFactory(() => NotificationSettingsCubit(notificationSettingsRepository: sl())); - sl.registerFactory(() => ConsultantRatingCubit(sl())); + sl.registerFactory(() => ConsultantRatingCubit(feedRepository: sl(), repository: sl(), profileRepository: sl())); sl.registerFactory(() => ViewsCubit(viewsRepository: sl())); sl.registerFactory(() => UserDetailsCubit(profileRepository: sl())); } diff --git a/lib/features/consultant_rating/models/statistics_model.dart b/lib/features/consultant_rating/models/statistics_model.dart index bbac675b..1e04eebc 100644 --- a/lib/features/consultant_rating/models/statistics_model.dart +++ b/lib/features/consultant_rating/models/statistics_model.dart @@ -13,6 +13,7 @@ abstract class StatisticsModel with _$StatisticsModel { @JsonKey(name: 'count_diets') int? dietsCount, @JsonKey(name: 'count_consultants') int? consultationsCount, @JsonKey(name: 'count_products_nutritional_values') int? qwe, + @JsonKey(name: 'referral_link') String? refLink, }) = _StatisticsModel; factory StatisticsModel.fromJson(Map json) => _$StatisticsModelFromJson(json); diff --git a/lib/features/consultant_rating/presentation/consultant_rating_screen.dart b/lib/features/consultant_rating/presentation/consultant_rating_screen.dart index 8c6cb3d4..000367e0 100644 --- a/lib/features/consultant_rating/presentation/consultant_rating_screen.dart +++ b/lib/features/consultant_rating/presentation/consultant_rating_screen.dart @@ -1,6 +1,7 @@ import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/core/utils/toasts.dart'; import 'package:eatvio_app/core/widgets/custom_outlined_button.dart'; import 'package:eatvio_app/core/widgets/data_error_widget.dart'; import 'package:eatvio_app/features/consultant_rating/presentation/cubit/consultant_rating_cubit.dart'; @@ -10,6 +11,7 @@ import 'package:eatvio_app/features/consultant_rating/presentation/widgets/ratin import 'package:eatvio_app/features/consultant_rating/presentation/widgets/statistic_widget.dart'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class ConsultantRatingScreen extends StatelessWidget { @@ -53,8 +55,8 @@ class ConsultantRatingPageContent extends StatelessWidget { child: Column( children: [ const SizedBox(height: 16), - const RatingWidget( - rating: 1058, + RatingWidget( + rating: state.absoluteRating!, ), const SizedBox(height: 16), Padding( @@ -67,7 +69,10 @@ class ConsultantRatingPageContent extends StatelessWidget { side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), ), - onPressed: () {}, + onPressed: () { + Clipboard.setData(ClipboardData(text: state.statistics?.refLink ?? '')); + showInfoToast(s.link_was_copied); + }, ), ), const SizedBox(height: 16), diff --git a/lib/features/consultant_rating/presentation/cubit/consultant_rating_cubit.dart b/lib/features/consultant_rating/presentation/cubit/consultant_rating_cubit.dart index 291f450c..d3ff72a9 100644 --- a/lib/features/consultant_rating/presentation/cubit/consultant_rating_cubit.dart +++ b/lib/features/consultant_rating/presentation/cubit/consultant_rating_cubit.dart @@ -2,17 +2,23 @@ import 'dart:developer'; import 'package:eatvio_app/features/consultant_rating/presentation/cubit/consultant_rating_state.dart'; import 'package:eatvio_app/features/consultant_rating/repositories/consultant_rating_repository.dart'; +import 'package:eatvio_app/features/feed/repositories/feed_repository.dart'; +import 'package:eatvio_app/features/profile/repositories/profile_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class ConsultantRatingCubit extends Cubit { final ConsultantRatingRepository repository; - ConsultantRatingCubit(this.repository) : super(ConsultantRatingState()); + final FeedRepository feedRepository; + final ProfileRepository profileRepository; + + ConsultantRatingCubit({required this.repository, required this.feedRepository, required this.profileRepository}) : super(ConsultantRatingState()); Future getConsultantStatistics({String? date}) async { try { emit(state.copyWith(isStatisticsLoading: true)); final statistics = await repository.getConsultantStatistics(date: date); - emit(state.copyWith(isStatisticsLoading: false, statistics: statistics, error: null)); + final absoluteRating = await feedRepository.getConsultants((await profileRepository.getMyProfile()).name); + emit(state.copyWith(isStatisticsLoading: false, statistics: statistics, error: null, absoluteRating: absoluteRating.first.totalScores)); } catch (e, st) { emit(state.copyWith(isStatisticsLoading: true, error: e.toString())); log(e.toString(), stackTrace: st); diff --git a/lib/features/consultant_rating/presentation/cubit/consultant_rating_state.dart b/lib/features/consultant_rating/presentation/cubit/consultant_rating_state.dart index 20cc458f..7c1e0942 100644 --- a/lib/features/consultant_rating/presentation/cubit/consultant_rating_state.dart +++ b/lib/features/consultant_rating/presentation/cubit/consultant_rating_state.dart @@ -12,5 +12,6 @@ abstract class ConsultantRatingState with _$ConsultantRatingState { @Default(null) StatisticsModel? statistics, @Default(null) String? error, @Default([]) List ratings, + @Default(0) int? absoluteRating, }) = _ConsultantRatingState; } diff --git a/lib/features/consultant_rating/presentation/widgets/statistic_widget.dart b/lib/features/consultant_rating/presentation/widgets/statistic_widget.dart index 3be44129..ef148adf 100644 --- a/lib/features/consultant_rating/presentation/widgets/statistic_widget.dart +++ b/lib/features/consultant_rating/presentation/widgets/statistic_widget.dart @@ -71,7 +71,7 @@ class StatisticWidget extends StatelessWidget { children: [ Expanded(child: StatisticBouble(number: statistics?.consultationsCount ?? 0, title: s.consultations_count)), const SizedBox(width: 8), - Expanded(child: StatisticBouble(number: statistics?.qwe ?? 0, title: s.active_consultations_count)), + Expanded(child: StatisticBouble(number: statistics?.qwe ?? 0, title: s.added_products_in_glossary)), ], ), const SizedBox( diff --git a/lib/features/feed/repositories/feed_repository.dart b/lib/features/feed/repositories/feed_repository.dart index 108a8b11..e540486f 100644 --- a/lib/features/feed/repositories/feed_repository.dart +++ b/lib/features/feed/repositories/feed_repository.dart @@ -133,9 +133,9 @@ class FeedRepository { } } - Future> getConsultants() async { + Future> getConsultants([String? name]) async { try { - return (await feedService.getConsultants()).data; + return (await feedService.getConsultants(name: name)).data; } catch (e) { rethrow; } diff --git a/lib/features/feed/services/feed_service.dart b/lib/features/feed/services/feed_service.dart index 265c2d38..c756f8c7 100644 --- a/lib/features/feed/services/feed_service.dart +++ b/lib/features/feed/services/feed_service.dart @@ -16,15 +16,10 @@ abstract class FeedService { factory FeedService(Dio dio, {String baseUrl}) = _FeedService; @GET('/api/blogPosts/index') - Future getBlogPostList({ - @Query('page') required int page, - @Query('query_search') String? query, - }); + Future getBlogPostList({@Query('page') required int page, @Query('query_search') String? query}); @GET('/api/blogPosts/show/{postId}') - Future getBlogPost({ - @Path('postId') required int postId, - }); + Future getBlogPost({@Path('postId') required int postId}); @GET('/api/recipes/index') Future getRecipes({ @@ -53,88 +48,50 @@ abstract class FeedService { }); @GET('/api/blogPosts/{id}/comments/show') - Future getBlogPostComments({ - @Path() required String id, - }); + Future getBlogPostComments({@Path() required String id}); @POST('/api/blogPosts/{id}/comment/create') - Future sendBlogPostComment({ - @Path() required String id, - @Part() required String body, - @Part() int? parentCommentId, - }); + Future sendBlogPostComment({@Path() required String id, @Part() required String body, @Part() int? parentCommentId}); @GET('/api/diets/{id}/comments/show') - Future getDietsComments({ - @Path() required String id, - }); + Future getDietsComments({@Path() required String id}); @POST('/api/diets/{id}/comment/create') - Future sendDietsComment({ - @Path() required String id, - @Part() required String body, - @Part() int? parentCommentId, - }); + Future sendDietsComment({@Path() required String id, @Part() required String body, @Part() int? parentCommentId}); @GET('/api/plans/{slug}/comments/show') - Future getPlansComments({ - @Path() required String slug, - }); + Future getPlansComments({@Path() required String slug}); @POST('/api/plans/{slug}/comment/create') - Future sendPlansComment({ - @Path() required String slug, - @Part() required String body, - @Part() int? parentCommentId, - }); + Future sendPlansComment({@Path() required String slug, @Part() required String body, @Part() int? parentCommentId}); @GET('/api/recipes/{id}/comments/show') - Future getRecipesComments({ - @Path() required String id, - }); + Future getRecipesComments({@Path() required String id}); @POST('/api/recipes/{id}/comment/create') - Future sendRecipesComment({ - @Path() required String id, - @Part() required String body, - @Part() int? parentCommentId, - }); + Future sendRecipesComment({@Path() required String id, @Part() required String body, @Part() int? parentCommentId}); @POST('/api/blogPosts/{id}/like') - Future likeBlogPost({ - @Path() required int id, - }); + Future likeBlogPost({@Path() required int id}); @POST('/api/blogPosts/{id}/unliked') - Future unlikedBlogPost({ - @Path() required int id, - }); + Future unlikedBlogPost({@Path() required int id}); @GET('/api/consultants/index') - Future getConsultants(); + Future getConsultants({@Query('filters[name][\$startsWith]') String? name}); @POST('/api/blogPosts/{id}/view') - Future viewPost({ - @Path() required int id, - }); + Future viewPost({@Path() required int id}); @POST('/api/recipes/{slug}/view') - Future viewRecipe({ - @Path() required String slug, - }); + Future viewRecipe({@Path() required String slug}); @POST('/api/diets/{id}/view') - Future viewDiet({ - @Path() required int id, - }); + Future viewDiet({@Path() required int id}); @POST('/api/plans/{slug}/view') - Future viewPlan({ - @Path() required String slug, - }); + Future viewPlan({@Path() required String slug}); @POST('/api/views') - Future viewContent({ - @Body() required ViewModel viewModel, - }); + Future viewContent({@Body() required ViewModel viewModel}); } diff --git a/lib/features/menu/presentation/pages/menu_screen.dart b/lib/features/menu/presentation/pages/menu_screen.dart index 5c350d3c..123e5faf 100644 --- a/lib/features/menu/presentation/pages/menu_screen.dart +++ b/lib/features/menu/presentation/pages/menu_screen.dart @@ -58,7 +58,7 @@ class MenuScreen extends StatelessWidget { if (profileModel.roles.contains('consultant')) _divider(context), if (profileModel.roles.contains('consultant')) _tile( - leading: 'assets/icons/finance.svg', + leading: 'assets/icons/consultant_rating_icon.svg', title: 'Мой рейтинг', onTap: () { GoRouter.of(context).push(ConsultantRatingScreen.path); diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart index c054a4b5..dc9e4c16 100644 --- a/lib/generated/intl/messages_en.dart +++ b/lib/generated/intl/messages_en.dart @@ -66,6 +66,9 @@ class MessageLookup extends MessageLookupByLibrary { "add_card": MessageLookupByLibrary.simpleMessage("Привязать карту"), "add_cover": MessageLookupByLibrary.simpleMessage("Добавьте обложку"), "add_measure": MessageLookupByLibrary.simpleMessage("Добавить замер"), + "added_products_in_glossary": MessageLookupByLibrary.simpleMessage( + "Кол-во продуктов добавленных в глоссарий", + ), "adding_plan": MessageLookupByLibrary.simpleMessage("Добавление плана"), "additional_information": MessageLookupByLibrary.simpleMessage( "Дополнительная информация", diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index 85f74918..0ccdeac3 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -2399,6 +2399,16 @@ class S { ); } + /// `Кол-во продуктов добавленных в глоссарий` + String get added_products_in_glossary { + return Intl.message( + 'Кол-во продуктов добавленных в глоссарий', + name: 'added_products_in_glossary', + desc: '', + args: [], + ); + } + /// `Кол-во активных консультаций` String get active_consultations_count { return Intl.message( diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 57145fb9..1fc575a8 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -300,6 +300,7 @@ "published_recipes_count": "Кол-во опубликованных рецептов", "published_diets_count": "Кол-во опубликованных диет", "consultations_count": "Кол-во проведенных консулдьтаций", + "added_products_in_glossary": "Кол-во продуктов добавленных в глоссарий", "active_consultations_count": "Кол-во активных консультаций", "rating_history": "Начисление рейтинга", "something_went_wrong": "Упс! Что-то пошло не так", From f35cdd26a3abbce88ddbcc92601ec602d15ce1d2 Mon Sep 17 00:00:00 2001 From: misha Date: Mon, 6 Oct 2025 13:33:12 +0300 Subject: [PATCH 52/78] feat(diary): fexed wrong day selecting logic --- lib/features/diary/presentation/bloc/diary_cubit.dart | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/features/diary/presentation/bloc/diary_cubit.dart b/lib/features/diary/presentation/bloc/diary_cubit.dart index d23cb8d4..53beebe0 100644 --- a/lib/features/diary/presentation/bloc/diary_cubit.dart +++ b/lib/features/diary/presentation/bloc/diary_cubit.dart @@ -49,10 +49,14 @@ class DiaryCubit extends Cubit { String comment = ''; Future setSelectedDay(DateTime day) async { + final prevDay = selectedDay; + selectedDay = day; final canSelect = await fetchDiaryV2(needLoading: true); - if (canSelect) { - selectedDay = day; + + if (!canSelect) { + selectedDay = prevDay; } + return canSelect; } From 5275273830a9cb00ae15b9771a2509d9c7d66671 Mon Sep 17 00:00:00 2001 From: misha Date: Mon, 6 Oct 2025 16:46:59 +0300 Subject: [PATCH 53/78] feat(socialAuth): added profile data fetching for social aunthentication --- lib/core/cubit/base_cubit.dart | 2 +- .../auth/entities/auth_result_entity.dart | 51 ++++ .../auth/models/yandex_profile_model.dart | 30 +++ .../auth/presentation/pages/auth_screen.dart | 28 +-- lib/features/auth/sdk/social_auth_facade.dart | 220 +++++++++++------- 5 files changed, 228 insertions(+), 103 deletions(-) create mode 100644 lib/features/auth/entities/auth_result_entity.dart create mode 100644 lib/features/auth/models/yandex_profile_model.dart diff --git a/lib/core/cubit/base_cubit.dart b/lib/core/cubit/base_cubit.dart index 3cd6c9d9..03ff3cc2 100644 --- a/lib/core/cubit/base_cubit.dart +++ b/lib/core/cubit/base_cubit.dart @@ -7,7 +7,7 @@ abstract class BaseCubit extends Cubit { /// Унифицированная обработка ошибок void handleError(Object error, [StackTrace? stackTrace]) { - log('Error in ${runtimeType}: $error', stackTrace: stackTrace); + log('Error in $runtimeType: $error', stackTrace: stackTrace); // Здесь можно добавить отправку ошибок в Sentry или другую систему мониторинга } diff --git a/lib/features/auth/entities/auth_result_entity.dart b/lib/features/auth/entities/auth_result_entity.dart new file mode 100644 index 00000000..2f296e5b --- /dev/null +++ b/lib/features/auth/entities/auth_result_entity.dart @@ -0,0 +1,51 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:vk_id/vk_id.dart'; + +part 'auth_result_entity.freezed.dart'; + +@freezed +abstract class AuthResultEntity with _$AuthResultEntity { + const factory AuthResultEntity({ + required String accessToken, + required String userId, + String? refreshToken, + String? firstName, + String? lastName, + String? email, + String? phone, + GenderEnum? gender, + String? birthDate, + }) = _AuthResultEntity; +} + +enum GenderEnum { + male, + female, + notSet; + + static GenderEnum? fromVkGender(VkSex? vkGender) { + switch (vkGender) { + case VkSex.male: + return GenderEnum.male; + case VkSex.female: + return GenderEnum.female; + case VkSex.notStated: + return GenderEnum.notSet; + case null: + return null; + } + } + + static GenderEnum? fromString(String? gender) { + switch (gender) { + case 'male': + return GenderEnum.male; + case 'female': + return GenderEnum.female; + case 'notSet': + return GenderEnum.notSet; + default: + return null; + } + } +} diff --git a/lib/features/auth/models/yandex_profile_model.dart b/lib/features/auth/models/yandex_profile_model.dart new file mode 100644 index 00000000..9bb84998 --- /dev/null +++ b/lib/features/auth/models/yandex_profile_model.dart @@ -0,0 +1,30 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'yandex_profile_model.freezed.dart'; +part 'yandex_profile_model.g.dart'; + +@freezed +abstract class YandexProfileModel with _$YandexProfileModel { + factory YandexProfileModel({ + required String id, + required String login, + required String birthday, + @JsonKey(name: 'client_id') required String clientId, + @JsonKey(name: 'display_name') required String displayName, + @JsonKey(name: 'real_name') required String realName, + @JsonKey(name: 'first_name') required String firstName, + @JsonKey(name: 'last_name') required String lastName, + @JsonKey(name: 'sex') required String sex, + @JsonKey(name: 'default_email') required String defaultEmail, + @JsonKey(name: 'default_phone') required YandexPhoneData defaultPhone, + }) = _YandexProfileModel; + + factory YandexProfileModel.fromJson(Map json) => _$YandexProfileModelFromJson(json); +} + +@freezed +abstract class YandexPhoneData with _$YandexPhoneData { + factory YandexPhoneData({required int id, required String number}) = _YandexPhoneData; + + factory YandexPhoneData.fromJson(Map json) => _$YandexPhoneDataFromJson(json); +} diff --git a/lib/features/auth/presentation/pages/auth_screen.dart b/lib/features/auth/presentation/pages/auth_screen.dart index 30ef29cf..6f49ea2a 100644 --- a/lib/features/auth/presentation/pages/auth_screen.dart +++ b/lib/features/auth/presentation/pages/auth_screen.dart @@ -1,6 +1,7 @@ // ignore_for_file: unused_element, duplicate_ignore import 'dart:async'; +import 'dart:developer'; import 'package:eatvio_app/core/app_router.dart'; import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/firebase_api.dart'; @@ -127,7 +128,7 @@ class _AuthScreenState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ _SocialButton( - onTap: () => _onVkPressed(context), + onTap: () => _onVkPressed(), backgroundColor: const Color(0xFF2787F5), tooltip: 'VK ID', child: SvgPicture.asset('assets/icons/vk_logo.svg', height: 24, width: 24), @@ -388,19 +389,14 @@ class _SocialButton extends StatelessWidget { } } -void _onVkPressed(BuildContext context) async { +void _onVkPressed() async { try { - final accessToken = await SocialAuthFacade.signInWithVk(context: context); + 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) { - await _reconnectAfterAuth(); - } - } } catch (e) { + log(e.toString()); } } @@ -410,11 +406,8 @@ void _onOkPressed() async { if (accessToken == null) { return; } - final isAuthenticated = await sl().getTokenBySocial(provider: 'ok', accessToken: accessToken); - if (isAuthenticated) { - await _reconnectAfterAuth(); - } } catch (e) { + log(e.toString()); } } @@ -424,21 +417,14 @@ void _onMailRuPressed() async { if (accessToken == null) { return; } - final isAuthenticated = await sl().getTokenBySocial(provider: 'mailru', accessToken: accessToken); - if (isAuthenticated) { - await _reconnectAfterAuth(); - } } catch (e) { + log(e.toString()); } } 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 { diff --git a/lib/features/auth/sdk/social_auth_facade.dart b/lib/features/auth/sdk/social_auth_facade.dart index 74959498..a235f03d 100644 --- a/lib/features/auth/sdk/social_auth_facade.dart +++ b/lib/features/auth/sdk/social_auth_facade.dart @@ -1,80 +1,125 @@ import 'dart:async'; +import 'dart:convert'; +import 'dart:developer'; +import 'package:eatvio_app/features/auth/entities/auth_result_entity.dart'; +import 'package:eatvio_app/features/auth/models/yandex_profile_model.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_login_yandex/flutter_login_yandex.dart'; import 'package:eatvio_app/core/app_router.dart'; import 'package:eatvio_app/core/env_config.dart'; +import 'package:http/http.dart' as http; 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({required BuildContext context}) async { + static Future signInWithVk() async { + final context = AppRouter.navigatorKey.currentContext; + if (context == null) return null; final result = await signInWithVkAndFetchProfile(context: context); return result?.accessToken; } - static Future signInWithVkAndFetchProfile({required BuildContext context}) async { + static Future signInWithVkAndFetchProfile({required BuildContext context}) async { final clientIdStr = EnvConfig.vkClientId.trim(); if (clientIdStr.isEmpty) { return null; } try { - dynamic controller = VkIDController(clID: int.parse(clientIdStr)); + final controller = VkIDController(clID: int.parse(clientIdStr)); final authUri = controller.generateAuthorizeLinkWithCodeVerifier(); - String authUrl; - if (authUri is MapEntry) { - authUrl = authUri.value.toString(); - } else { - authUrl = authUri.toString(); - } - + final authUrl = authUri.value.toString(); + final codeVerifier = authUri.key; final redirectScheme = 'vk$clientIdStr://'; final redirectParams = await _presentOAuthWebView(context: context, initialUrl: authUrl, redirectSchemePrefix: redirectScheme); if (redirectParams == null) { return null; } - - final code = redirectParams['code']; - final deviceId = redirectParams['device_id']; - if (code == null || deviceId == null) { - return null; - } - return VkAuthResult(accessToken: code); + return await _parseVKAuthResult(redirectParams: redirectParams, controller: controller, codeVerifier: codeVerifier); } catch (e) { return null; } } + static Future _parseVKAuthResult({ + required Map redirectParams, + required VkIDController controller, + required String codeVerifier, + }) async { + final code = redirectParams['code']; + final deviceId = redirectParams['device_id']; + final state = redirectParams['state']; + if (code == null || deviceId == null || state == null) { + return null; + } + final authRes = await controller.retrieveOAuthToken(authorizationCode: code, deviceId: deviceId, codeVerifier: codeVerifier, state: state); + final auth = authRes.result; + if (auth == null) { + var msg = "Retrieve OAuth token error"; + final err = authRes.error?.vkErr; + if (err != null) { + msg += ": ${err.error} - ${err.description}"; + } + log(msg); + return null; + } + final profileRes = await controller.getProfileInfo(); + final profile = profileRes.result; + if (profile == null) { + var msg = "Profile info load error"; + final err = profileRes.error?.vkErr; + if (err != null) { + msg += ": ${err.error} - ${err.description}"; + } + log(msg); + return null; + } + + return AuthResultEntity( + accessToken: auth.accessToken, + refreshToken: auth.refreshToken, + userId: profile.userId.toString(), + firstName: profile.firstName, + lastName: profile.lastName, + email: profile.email, + phone: profile.phone, + gender: GenderEnum.fromVkGender(profile.parsedSex), + birthDate: profile.bDayDt?.toString(), + ); + } + + Future?> exchangeCodeForToken(String code, String clientId, String clientSecret) async { + final response = await http.post( + Uri.parse('https://oauth.yandex.ru/token'), + headers: {'Content-Type': 'application/x-www-form-urlencoded'}, + body: {'grant_type': 'authorization_code', 'code': code, 'client_id': clientId, 'client_secret': clientSecret}, + ); + + if (response.statusCode == 200) { + return jsonDecode(response.body) as Map; + } else { + log('Error exchanging code: ${response.body}'); + } + return null; + } + static Future signInWithOk() async { final context = AppRouter.navigatorKey.currentContext; if (context == null) return null; - return _signInWithVkIdProvider(context: context, provider: 'ok'); + final vkAuthRes = await _signInWithVkIdProvider(context: context, provider: VkProvider.okRu); + return vkAuthRes?.accessToken; } static Future signInWithMailRu() async { final context = AppRouter.navigatorKey.currentContext; if (context == null) return null; - return _signInWithVkIdProvider(context: context, provider: 'mail'); + final vkAuthRes = await _signInWithVkIdProvider(context: context, provider: VkProvider.mailRu); + return vkAuthRes?.accessToken; } - static Future signInWithYandex() async { + static Future signInWithYandex() async { final clientId = EnvConfig.yandexClientId.trim(); final redirectUri = EnvConfig.yandexRedirectUri.trim(); @@ -87,7 +132,7 @@ class SocialAuthFacade { if (context == null) return null; final authorizeUrl = Uri.https('oauth.yandex.ru', '/authorize', { - 'response_type': 'token', + 'response_type': 'code', 'client_id': clientId, 'redirect_uri': redirectUri, 'force_confirm': 'yes', @@ -99,62 +144,78 @@ class SocialAuthFacade { if (params == null) return null; - final token = params['access_token']; - if (token != null && token.isNotEmpty) { - return token; - } - final code = params['code']; - if (code != null && code.isNotEmpty) { - return code; - } - - return null; - } catch (e) { - 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 (_) { + if (code == null || code.isEmpty) { return null; } + + final tokens = await _getTokensFomYandex(code: code, clientId: clientId, clientSecret: EnvConfig.yandexClientSecret); + if (tokens == null) { + return null; + } + final response = await http.get(Uri.parse('https://login.yandex.ru/info?format=json'), headers: {'Authorization': 'OAuth ${tokens.accessToken}'}); + + final Map json = jsonDecode(response.body); + final yandexProfileData = YandexProfileModel.fromJson(json); + return AuthResultEntity( + accessToken: tokens.accessToken, + refreshToken: tokens.refreshToken, + userId: yandexProfileData.id, + firstName: yandexProfileData.firstName, + lastName: yandexProfileData.lastName, + email: yandexProfileData.defaultEmail, + phone: yandexProfileData.defaultPhone.number, + gender: GenderEnum.fromString(yandexProfileData.sex), + birthDate: yandexProfileData.birthday, + ); + } catch (e) { + log(e.toString()); } + return null; } - static Future _signInWithVkIdProvider({required BuildContext context, required String provider}) async { + static Future<({String accessToken, String refreshToken})?> _getTokensFomYandex({ + required String code, + required String clientId, + required String clientSecret, + }) async { + final response = await http.post( + Uri.parse('https://oauth.yandex.ru/token'), + headers: {'Content-Type': 'application/x-www-form-urlencoded'}, + body: {'grant_type': 'authorization_code', 'code': code, 'client_id': clientId, 'client_secret': clientSecret}, + ); + + if (response.statusCode != 200) { + log('Yandex token exchange failed: ${response.body}'); + return null; + } + + final tokens = jsonDecode(response.body); + final accessToken = tokens['access_token'] as String; + final refreshToken = tokens['refresh_token'] as String; + return (accessToken: accessToken, refreshToken: refreshToken); + } + + static Future _signInWithVkIdProvider({required BuildContext context, required VkProvider provider}) async { final clientIdStr = EnvConfig.vkClientId.trim(); if (clientIdStr.isEmpty) { return null; } try { - dynamic controller = VkIDController(clID: int.parse(clientIdStr)); + final controller = VkIDController(clID: int.parse(clientIdStr)); - dynamic authUri; - try { - authUri = controller.generateAuthorizeLinkWithCodeVerifier(provider: provider); - } catch (e) { - try { - authUri = controller.generateAuthorizeLinkWithCodeVerifier(provider: null); - } catch (e2) { - authUri = controller.generateAuthorizeLinkWithCodeVerifier(); - } - } - - // Extract URL from MapEntry - String authUrl; - if (authUri is MapEntry) { - authUrl = authUri.value.toString(); - } else { - authUrl = authUri.toString(); - } + final authUri = controller.generateAuthorizeLinkWithCodeVerifier(provider: provider); + final codeVerifier = authUri.key; + final authUrl = authUri.value.toString(); final redirectScheme = 'vk$clientIdStr://'; final redirectParams = await _presentOAuthWebView(context: context, initialUrl: authUrl, redirectSchemePrefix: redirectScheme); if (redirectParams == null) return null; + final code = redirectParams['code']; final deviceId = redirectParams['device_id']; if (code == null || deviceId == null) return null; - return code; + + return await _parseVKAuthResult(redirectParams: redirectParams, controller: controller, codeVerifier: codeVerifier); } catch (e) { return null; } @@ -190,12 +251,9 @@ class SocialAuthFacade { } return NavigationDecision.navigate; }, - onPageStarted: (url) { - }, - onPageFinished: (url) { - }, - onWebResourceError: (error) { - }, + onPageStarted: (url) {}, + onPageFinished: (url) {}, + onWebResourceError: (error) {}, ), ); @@ -204,7 +262,7 @@ class SocialAuthFacade { } catch (e) { return null; } - + if (!context.mounted) return null; return showDialog>( context: context, barrierDismissible: false, From 03448ebd2d5e205b0fde4aa644d1ba0b4f0daab2 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Mon, 6 Oct 2025 17:27:11 +0300 Subject: [PATCH 54/78] feat(oauth): Issue-691 --- lib/core/routing/auth_routes.dart | 2 + lib/core/routing/route_constants.dart | 1 + .../pages/register_via_social_screen.dart | 100 ++++++++++++++++++ 3 files changed, 103 insertions(+) create mode 100644 lib/features/auth/presentation/pages/register_via_social_screen.dart diff --git a/lib/core/routing/auth_routes.dart b/lib/core/routing/auth_routes.dart index fc4558d1..67ecbca6 100644 --- a/lib/core/routing/auth_routes.dart +++ b/lib/core/routing/auth_routes.dart @@ -1,6 +1,7 @@ import 'package:eatvio_app/core/routing/route_constants.dart'; import 'package:eatvio_app/features/auth/presentation/pages/auth_screen.dart'; import 'package:eatvio_app/features/auth/presentation/pages/forget_password_screen.dart'; +import 'package:eatvio_app/features/auth/presentation/pages/register_via_social_screen.dart'; import 'package:eatvio_app/features/auth/presentation/pages/scan_qr_code_screen.dart'; import 'package:eatvio_app/features/registration/presentation/pages/registration_screen.dart'; import 'package:eatvio_app/features/registration/presentation/pages/verify_screen.dart'; @@ -32,6 +33,7 @@ class AuthRoutes { builder: (context, state) => VerifyScreen(passKey: state.extra as String), ), GoRoute(path: RouteConstants.verifyEmailScreen, name: 'verifyEmailScreen', builder: (context, state) => const VerifyEmailScreen()), + GoRoute(path: RouteConstants.registerViaSocialScreen, name: 'registerViaSocialScreen', builder: (context, state) => const RegisterViaSocialScreen()), ]; } } diff --git a/lib/core/routing/route_constants.dart b/lib/core/routing/route_constants.dart index dcc82c17..1702b425 100644 --- a/lib/core/routing/route_constants.dart +++ b/lib/core/routing/route_constants.dart @@ -7,6 +7,7 @@ class RouteConstants { static const String scanQRCodeScreen = 'scan-qr-code-screen'; static const String verifyScreen = '/verify-screen'; static const String verifyEmailScreen = '/verify-email-screen'; + static const String registerViaSocialScreen = '/register-via-social-screen'; // Main navigation routes static const String myProfileScreen = '/my-profile-screen'; diff --git a/lib/features/auth/presentation/pages/register_via_social_screen.dart b/lib/features/auth/presentation/pages/register_via_social_screen.dart new file mode 100644 index 00000000..24c4fc46 --- /dev/null +++ b/lib/features/auth/presentation/pages/register_via_social_screen.dart @@ -0,0 +1,100 @@ +import 'package:eatvio_app/core/widgets/custom_text_form_field.dart'; +import 'package:flutter/material.dart'; +import 'package:mask_text_input_formatter/mask_text_input_formatter.dart'; + +class RegisterViaSocialScreen extends StatefulWidget { + const RegisterViaSocialScreen({super.key}); + + @override + State createState() => _RegisterViaSocialScreenState(); +} + +class _RegisterViaSocialScreenState extends State { + final maskFormatter = MaskTextInputFormatter(mask: '+7 (###) ### ## ##', filter: {"#": RegExp(r'[0-9]')}); + final maskFormatterForBirthday = MaskTextInputFormatter(mask: '##.##.####', filter: {"#": RegExp(r'[0-9]')}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(toolbarHeight: 0), + body: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 24), + const Center( + child: Text( + 'Регистрация', + style: TextStyle(fontSize: 24, fontWeight: FontWeight.w700, height: 1.1, color: Color(0xFF323F4B)), + ), + ), + const SizedBox(height: 24), + Row( + children: [ + SizedBox( + width: MediaQuery.of(context).size.width / 2 - 22, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + const Text('Имя'), + const SizedBox(height: 4), + CustomTextFormField(controller: TextEditingController(), hintText: 'Введите имя', onChanged: (newValue) {}), + ], + ), + ), + const SizedBox(width: 12), + SizedBox( + width: MediaQuery.of(context).size.width / 2 - 22, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Фамилия'), + const SizedBox(height: 4), + CustomTextFormField(controller: TextEditingController(), hintText: 'Введите фамилию', onChanged: (newValue) {}), + ], + ), + ), + ], + ), + const SizedBox(height: 12), + const Text('Почта'), + const SizedBox(height: 4), + CustomTextFormField( + controller: TextEditingController(), + textInputType: TextInputType.emailAddress, + keyboardType: TextInputType.emailAddress, + hintText: 'Введите email', + autofocus: true, + textInputAction: TextInputAction.next, + onChanged: (newValue) {}, + ), + const SizedBox(height: 12), + const Text('Номер телефона'), + const SizedBox(height: 4), + CustomTextFormField( + textInputAction: TextInputAction.done, + hintText: '+7 (999) 999 99 99', + controller: TextEditingController(), + keyboardType: TextInputType.number, + inputFormatters: [maskFormatter], + onChanged: (newValue) {}, + ), + const SizedBox(height: 12), + const Text('Дата рождения'), + const SizedBox(height: 4), + CustomTextFormField( + textInputAction: TextInputAction.done, + hintText: '01.01.2000', + controller: TextEditingController(), + keyboardType: TextInputType.number, + inputFormatters: [maskFormatterForBirthday], + onChanged: (newValue) {}, + ), + ], + ), + ), + ); + } +} From b0d2d86a18053ce5d876d5bb7fa475b2642af5b1 Mon Sep 17 00:00:00 2001 From: misha Date: Tue, 7 Oct 2025 18:03:36 +0300 Subject: [PATCH 55/78] feat(tests): added 2 e2e tests --- .../auth_screen/auth_login_test.dart | 38 +++++++++++++++++++ .../auth_screen/auth_logo_button_test.dart | 32 ++++++++++++++++ .../common_cases/skip_welcome_screen.dart | 8 ++++ ios/Runner/AppDelegate.swift | 19 +++++++++- ios/Runner/Info.plist | 19 ++++++++++ lib/app_keys.dart | 13 +++++++ lib/core/app_initializer.dart | 16 +++++++- .../auth/presentation/pages/auth_screen.dart | 28 ++++++++++---- .../presentation/pages/welcome_screen.dart | 2 + lib/generated/l10n.dart | 7 ++-- pubspec.yaml | 2 + 11 files changed, 170 insertions(+), 14 deletions(-) create mode 100644 integration_test/auth_screen/auth_login_test.dart create mode 100644 integration_test/auth_screen/auth_logo_button_test.dart create mode 100644 integration_test/common_cases/skip_welcome_screen.dart create mode 100644 lib/app_keys.dart diff --git a/integration_test/auth_screen/auth_login_test.dart b/integration_test/auth_screen/auth_login_test.dart new file mode 100644 index 00000000..711e0aa3 --- /dev/null +++ b/integration_test/auth_screen/auth_login_test.dart @@ -0,0 +1,38 @@ +import 'package:eatvio_app/app_keys.dart'; +import 'package:eatvio_app/core/app_initializer.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:eatvio_app/main.dart' as app; + +import '../common_cases/skip_welcome_screen.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + setUpAll(() async { + await AppInitializer.initialize(buildType: AppBuildType.tests); + }); + + testWidgets('Signs in successfully with valid credentials', (WidgetTester tester) async { + await tester.pumpWidget(const app.MyApp()); + await tester.pumpAndSettle(const Duration(seconds: 3)); + await skipWelcomeScreen(tester); + final emailFieldFinder = find.byKey(AppKeys.emailTextFieldAuthScreen); + final passwordFieldFinder = find.byKey(AppKeys.passwordTextFieldAuthScreen); + final signInButtonFinder = find.byKey(AppKeys.signInButtonAuthScreen); + + expect(emailFieldFinder, findsOneWidget, reason: 'Email text field should be found.'); + expect(passwordFieldFinder, findsOneWidget, reason: 'Password text field should be found.'); + expect(signInButtonFinder, findsOneWidget, reason: 'Sign In button should be found.'); + + await tester.enterText(emailFieldFinder, 'reshetnik.n+user3@gmail.com'); + await tester.enterText(passwordFieldFinder, '1234567890'); + await tester.pumpAndSettle(); + + await tester.tap(signInButtonFinder); + + await tester.pumpAndSettle(const Duration(seconds: 3)); + + expect(signInButtonFinder, findsNothing, reason: 'Sign-in button should not be visible after successful login and navigation.'); + }); +} diff --git a/integration_test/auth_screen/auth_logo_button_test.dart b/integration_test/auth_screen/auth_logo_button_test.dart new file mode 100644 index 00000000..c427f327 --- /dev/null +++ b/integration_test/auth_screen/auth_logo_button_test.dart @@ -0,0 +1,32 @@ +import 'package:eatvio_app/app_keys.dart'; +import 'package:eatvio_app/core/app_initializer.dart'; +import 'package:eatvio_app/features/dev_tools/presentation/dev_tools_screen.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:eatvio_app/main.dart' as app; + +import '../common_cases/skip_welcome_screen.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + setUpAll(() async { + await AppInitializer.initialize(buildType: AppBuildType.tests); + }); + + testWidgets('tapping logo 7 times opens dev tools', (WidgetTester tester) async { + await tester.pumpWidget(const app.MyApp()); + await tester.pumpAndSettle(const Duration(seconds: 3)); + await skipWelcomeScreen(tester); + final logoButtonFinder = find.byKey(AppKeys.hiddenLogoButtonAuthScreen); + expect(logoButtonFinder, findsOneWidget, reason: 'The logo GestureDetector should be found in auth screen'); + + for (int i = 0; i < 7; i++) { + await tester.tap(logoButtonFinder); + await tester.pump(const Duration(milliseconds: 50)); + } + await tester.pumpAndSettle(); + + expect(find.byType(DevToolsScreen), findsOneWidget, reason: 'DevToolsScreen should be visible after tapping the logo 7 times.'); + }); +} diff --git a/integration_test/common_cases/skip_welcome_screen.dart b/integration_test/common_cases/skip_welcome_screen.dart new file mode 100644 index 00000000..20f8899b --- /dev/null +++ b/integration_test/common_cases/skip_welcome_screen.dart @@ -0,0 +1,8 @@ +import 'package:eatvio_app/app_keys.dart'; +import 'package:flutter_test/flutter_test.dart'; + +Future skipWelcomeScreen(WidgetTester tester) async { + final welcomeScreenButtonFinder = find.byKey(AppKeys.welcomeScreenButton); + await tester.tap(welcomeScreenButtonFinder); + await tester.pumpAndSettle(); +} \ No newline at end of file diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index b6363034..fc70b0b0 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -1,5 +1,6 @@ -import UIKit import Flutter +import UIKit +import YandexLoginSDK @main @objc class AppDelegate: FlutterAppDelegate { @@ -10,4 +11,20 @@ import Flutter GeneratedPluginRegistrant.register(with: self) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } + + @available(iOS 8.0, *) + func application( + _ application: UIApplication, continue userActivity: NSUserActivity, + restorationHandler: @escaping ([Any]?) -> Void + ) -> Bool { + YXLSdk.shared.processUserActivity(userActivity) + return true + } + + override public func application( + _ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:] + ) -> Bool { + return YXLSdk.shared.handleOpen( + url, sourceApplication: options[UIApplication.OpenURLOptionsKey.sourceApplication] as? String) + } } diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 7e82c5b2..42ec3497 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -67,5 +67,24 @@ UIViewControllerBasedStatusBarAppearance + LSApplicationQueriesSchemes + + yandexauth + yandexauth2 + yandexauth4 + + YAClientId + 5399b03636104c6d920b87c1ad329254 + CFBundleURLTypes + + + CFBundleURLName + YandexLoginSDK + CFBundleURLSchemes + + yx5399b03636104c6d920b87c1ad329254 + + + diff --git a/lib/app_keys.dart b/lib/app_keys.dart new file mode 100644 index 00000000..3e732506 --- /dev/null +++ b/lib/app_keys.dart @@ -0,0 +1,13 @@ +import 'package:flutter/material.dart'; + +class AppKeys { + + // Auth Screen + static const hiddenLogoButtonAuthScreen = ValueKey('hiddenLogoButtonAuthScreen'); + static const emailTextFieldAuthScreen = ValueKey('emailTextFieldAuthScreen'); + static const passwordTextFieldAuthScreen = ValueKey('passwordTextFieldAuthScreen'); + static const signInButtonAuthScreen = ValueKey('signInButtonAuthScreen'); + + // Welcome Screen + static const welcomeScreenButton = ValueKey('welcomeScreenButton'); +} diff --git a/lib/core/app_initializer.dart b/lib/core/app_initializer.dart index a3798a34..45e89fcf 100644 --- a/lib/core/app_initializer.dart +++ b/lib/core/app_initializer.dart @@ -1,4 +1,5 @@ import 'dart:io' as io; +import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/env_config.dart'; import 'package:eatvio_app/core/locator_service.dart' as di; import 'package:eatvio_app/firebase_options.dart'; @@ -12,10 +13,19 @@ import 'package:sentry_flutter/sentry_flutter.dart'; /// Класс для инициализации приложения class AppInitializer { + /// Инициализация всех сервисов приложения - static Future initialize() async { - await _initializeSentry(); + static Future initialize({AppBuildType buildType = AppBuildType.prod}) async { await _initializeEnvironment(); + switch (buildType) { + case AppBuildType.tests: + baseUrl = EnvConfig.devBaseUrl; + buildType = AppBuildType.tests; + break; + case AppBuildType.prod: + break; + } + await _initializeSentry(); await _initializeFirebase(); await _initializeDependencyInjection(); await _initializeDebugMode(); @@ -66,3 +76,5 @@ class AppInitializer { }, appRunner: appRunner); } } + +enum AppBuildType { prod, tests } diff --git a/lib/features/auth/presentation/pages/auth_screen.dart b/lib/features/auth/presentation/pages/auth_screen.dart index 6f49ea2a..ce6fee32 100644 --- a/lib/features/auth/presentation/pages/auth_screen.dart +++ b/lib/features/auth/presentation/pages/auth_screen.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:developer'; +import 'package:eatvio_app/app_keys.dart'; import 'package:eatvio_app/core/app_router.dart'; import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/firebase_api.dart'; @@ -46,7 +47,7 @@ class _AuthScreenState extends State { late StreamSubscription keyboardSubscription; bool isFocused = false; int tapsCount = 0; - + bool _isLoading = false; @override void initState() { super.initState(); @@ -92,6 +93,7 @@ class _AuthScreenState extends State { @override Widget build(BuildContext context) { var shortestSide = MediaQuery.sizeOf(context).shortestSide; + final screenSize = MediaQuery.of(context).size; final bool useMobileLayout = shortestSide < 600; if (!useMobileLayout) isFocused = true; final s = S.of(context); @@ -102,9 +104,10 @@ class _AuthScreenState extends State { builder: (BuildContext context, AuthState state) { return Scaffold( appBar: AppBar( + toolbarHeight: screenSize.height * 0.15, centerTitle: true, - toolbarHeight: isFocused ? 64 : MediaQuery.of(context).size.width - 64, title: GestureDetector( + key: AppKeys.hiddenLogoButtonAuthScreen, onTap: () { tapsCount++; if (tapsCount == 7) { @@ -112,7 +115,7 @@ class _AuthScreenState extends State { GoRouter.of(context).pushNamed(DevToolsScreen.name); } }, - child: SvgPicture.asset(ImageConstant.imgLogoEatvio, height: isFocused ? 64 : MediaQuery.of(context).size.width - 64, fit: BoxFit.contain), + child: SvgPicture.asset(ImageConstant.imgLogoEatvio, height: screenSize.height * 0.20), ), ), body: SafeArea( @@ -122,8 +125,8 @@ class _AuthScreenState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ // Social auth buttons at the top (outlined) - const Text('Авторизация через:', style: TextStyle(fontWeight: FontWeight.w600)), - const SizedBox(height: 8), + const Center(child: Text('Авторизация через')), + const SizedBox(height: 12), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -164,6 +167,7 @@ class _AuthScreenState extends State { textInputAction: TextInputAction.next, keyboardType: TextInputType.emailAddress, textInputType: TextInputType.emailAddress, + key: AppKeys.emailTextFieldAuthScreen, hintText: 'Введите email', ), const SizedBox(height: 16), @@ -172,6 +176,7 @@ class _AuthScreenState extends State { CustomTextFormField( obscureText: hidePassword, controller: passwordController, + key: AppKeys.passwordTextFieldAuthScreen, hintText: 'Введите пароль', suffix: IconButton( onPressed: () { @@ -249,21 +254,28 @@ class _AuthScreenState extends State { height: 48, width: MediaQuery.of(context).size.width - 88, child: FloatingActionButton.extended( + key: AppKeys.signInButtonAuthScreen, heroTag: 'authButton', highlightElevation: 0, elevation: 0, focusElevation: 0, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), - backgroundColor: state is AuthStateLoading ? Colors.black.withAlpha(50) : appTheme.blueA400, - onPressed: state is AuthStateLoading + backgroundColor: (state is AuthStateLoading || _isLoading) ? Colors.black.withAlpha(50) : appTheme.blueA400, + onPressed: (state is AuthStateLoading || _isLoading) ? null : () async { + setState(() { + _isLoading = true; + }); final isAuthenticated = await authCubit.getTokenByCredentials(emailController.text, passwordController.text); if (isAuthenticated) { if (context.mounted) { - _reconnectWebSocket(context); + await _reconnectWebSocket(context); } } + setState(() { + _isLoading = false; + }); }, label: state is AuthStateLoading ? const CupertinoActivityIndicator() diff --git a/lib/features/welcome/presentation/pages/welcome_screen.dart b/lib/features/welcome/presentation/pages/welcome_screen.dart index 31b4bf0f..d0b8f867 100644 --- a/lib/features/welcome/presentation/pages/welcome_screen.dart +++ b/lib/features/welcome/presentation/pages/welcome_screen.dart @@ -1,3 +1,4 @@ +import 'package:eatvio_app/app_keys.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/token_storage.dart'; import 'package:flutter/material.dart'; @@ -79,6 +80,7 @@ class _WelcomeScreenState extends State { ), const SizedBox(height: 24), InkWell( + key: AppKeys.welcomeScreenButton, onTap: () async { final sharedPreferences = await SharedPreferences.getInstance(); await sharedPreferences.setBool('welcomeScreenWasShowed', true); diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index 0ccdeac3..1055e664 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -28,9 +28,10 @@ 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; diff --git a/pubspec.yaml b/pubspec.yaml index 98a4429f..0891053c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -88,6 +88,8 @@ dev_dependencies: sdk: flutter flutter_lints: ^6.0.0 + integration_test: + sdk: flutter flutter: uses-material-design: true From 79de6ee6fa16e81b845ab76d1015bbac4cc9307d Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Tue, 7 Oct 2025 22:01:44 +0300 Subject: [PATCH 56/78] feat(registration): Issue-691 register_via_social_screen.dart --- .../pages/register_via_social_screen.dart | 167 ++++++++++++++++-- 1 file changed, 157 insertions(+), 10 deletions(-) diff --git a/lib/features/auth/presentation/pages/register_via_social_screen.dart b/lib/features/auth/presentation/pages/register_via_social_screen.dart index 24c4fc46..211b9eae 100644 --- a/lib/features/auth/presentation/pages/register_via_social_screen.dart +++ b/lib/features/auth/presentation/pages/register_via_social_screen.dart @@ -1,6 +1,10 @@ +import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/widgets/custom_text_form_field.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:mask_text_input_formatter/mask_text_input_formatter.dart'; +import 'package:url_launcher/url_launcher.dart'; class RegisterViaSocialScreen extends StatefulWidget { const RegisterViaSocialScreen({super.key}); @@ -66,7 +70,6 @@ class _RegisterViaSocialScreenState extends State { textInputType: TextInputType.emailAddress, keyboardType: TextInputType.emailAddress, hintText: 'Введите email', - autofocus: true, textInputAction: TextInputAction.next, onChanged: (newValue) {}, ), @@ -82,19 +85,163 @@ class _RegisterViaSocialScreenState extends State { onChanged: (newValue) {}, ), const SizedBox(height: 12), - const Text('Дата рождения'), - const SizedBox(height: 4), - CustomTextFormField( - textInputAction: TextInputAction.done, - hintText: '01.01.2000', - controller: TextEditingController(), - keyboardType: TextInputType.number, - inputFormatters: [maskFormatterForBirthday], - onChanged: (newValue) {}, + Row( + children: [ + SizedBox( + width: MediaQuery.of(context).size.width / 2 - 22, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + const Text('Дата рождения'), + const SizedBox(height: 4), + CustomTextFormField( + textInputAction: TextInputAction.done, + hintText: '01.01.2000', + controller: TextEditingController(), + keyboardType: TextInputType.number, + inputFormatters: [maskFormatterForBirthday], + onChanged: (newValue) {}, + ), + ], + ), + ), + const SizedBox(width: 12), + SizedBox( + width: MediaQuery.of(context).size.width / 2 - 22, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Пол'), + const SizedBox(height: 4), + Row( + children: [ + Expanded(child: _genderButton('М', true)), + const SizedBox(width: 4), + Expanded(child: _genderButton('Ж', false)), + ], + ), + ], + ), + ), + ], + ), + const SizedBox(height: 12), + Row( + children: [ + SizedBox( + width: MediaQuery.of(context).size.width / 2 - 22, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + const Text('Вес'), + const SizedBox(height: 4), + CustomTextFormField(controller: TextEditingController(), hintText: 'Введите вес', onChanged: (newValue) {}), + ], + ), + ), + const SizedBox(width: 12), + SizedBox( + width: MediaQuery.of(context).size.width / 2 - 22, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Рост'), + const SizedBox(height: 4), + CustomTextFormField(controller: TextEditingController(), hintText: 'Введите рост', onChanged: (newValue) {}), + ], + ), + ), + ], + ), + const SizedBox(height: 28), + const Center( + child: Text( + 'Проверьте правильность заполненных\nданных. Пол, рост и вес требуются для\nрасчета вашей дневной нормы калорий', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF323F4B)), + textAlign: TextAlign.center, + ), + ), + const SizedBox(height: 28), + InkWell( + borderRadius: BorderRadius.circular(20), + onTap: () {}, + child: Ink( + width: MediaQuery.of(context).size.width - 32, + height: 52, + decoration: BoxDecoration(color: appTheme.blueA400, borderRadius: BorderRadius.circular(20)), + child: const Center( + child: Text( + 'Зарегистрироваться', + style: TextStyle(fontWeight: FontWeight.w600, fontSize: 18, color: Colors.white), + ), + ), + ), + ), + const SizedBox(height: 16), + Center( + child: RichText( + textAlign: TextAlign.center, + text: TextSpan( + text: 'Нажимая "Зарегистрироваться", Вы соглашаетесь\n', + style: const TextStyle(fontSize: 16, color: Colors.black), + children: [ + const TextSpan( + text: 'c ', + style: TextStyle(fontSize: 16, color: Colors.black), + ), + if (!hidePlans) ...[ + TextSpan( + text: ' Публичной офертой', + style: TextStyle(fontSize: 16, color: appTheme.blueA400, decorationColor: appTheme.blueA400, decoration: TextDecoration.underline), + recognizer: TapGestureRecognizer()..onTap = () async => await launchUrl(Uri.parse('$baseUrl/oferta_bs.pdf')), + ), + const TextSpan( + text: ' и ', + style: TextStyle(fontSize: 16, color: Colors.black), + ), + ], + TextSpan( + text: 'Политикой конфиденциальности и защитой персональных данных', + style: TextStyle(fontSize: 16, color: appTheme.blueA400, decorationColor: appTheme.blueA400, decoration: TextDecoration.underline), + recognizer: TapGestureRecognizer()..onTap = () async => await launchUrl(Uri.parse('$baseUrl/docs/privacy.pdf')), + ), + ], + ), + ), ), ], ), ), ); } + + Widget _genderButton(String gender, bool isActive) { + return InkWell( + onTap: () {}, + borderRadius: BorderRadius.circular(20), + child: Ink( + height: 52, + decoration: BoxDecoration( + color: const Color(0xFFF6F6F6), + borderRadius: BorderRadius.circular(20), + border: isActive ? Border.all( + width: 2, + color: appTheme.blueA400, + ) : null, + ), + child: Center( + child: Text( + gender, + style: TextStyle( + color: isActive ? appTheme.blueA400 : const Color(0xFF323F4B), + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ); + } } From 87c0d4ec2ae01b3ecd638c6a3c15d3891cc11c4c Mon Sep 17 00:00:00 2001 From: misha Date: Wed, 8 Oct 2025 14:04:25 +0300 Subject: [PATCH 57/78] feat(analyzer): fixed issues --- lib/core/app_config.dart | 31 +++++++------------------------ 1 file changed, 7 insertions(+), 24 deletions(-) diff --git a/lib/core/app_config.dart b/lib/core/app_config.dart index 7017e570..3fb06b39 100644 --- a/lib/core/app_config.dart +++ b/lib/core/app_config.dart @@ -6,7 +6,6 @@ import 'package:eatvio_app/features/notifications/presentation/notifications_cub import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart'; -import 'package:eatvio_app/features/internet_connection_checker/internet_connection_checker_screen.dart'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -17,25 +16,12 @@ class AppConfig { /// Список глобальных BlocProvider'ов static List get globalBlocProviders => [ BlocProvider(create: (context) => sl()), - BlocProvider( - create: (context) => sl()..getNotifications(), - ), - BlocProvider( - create: (context) => sl()..loadFeedBlogPostList(), - ), - BlocProvider( - create: (context) => sl()..getMyProfile(), - ), - BlocProvider( - create: (context) => sl()..getChatRoomsWithLoading(), - ), - BlocProvider( - create: (context) => sl(), - ), - BlocProvider( - lazy: false, - create: (context) => sl(), - ), + BlocProvider(create: (context) => sl()..getNotifications()), + BlocProvider(create: (context) => sl()..loadFeedBlogPostList()), + BlocProvider(create: (context) => sl()..getMyProfile()), + BlocProvider(create: (context) => sl()..getChatRoomsWithLoading()), + BlocProvider(create: (context) => sl()), + BlocProvider(lazy: false, create: (context) => sl()), ]; /// Делегаты локализации @@ -47,8 +33,5 @@ class AppConfig { ]; /// Поддерживаемые локали - static const List supportedLocales = [ - Locale.fromSubtags(languageCode: 'ru'), - ]; - + static const List supportedLocales = [Locale.fromSubtags(languageCode: 'ru')]; } From 9ac576221ab5b0e8699a171191ae3b5e732be85e Mon Sep 17 00:00:00 2001 From: misha Date: Wed, 8 Oct 2025 17:23:51 +0300 Subject: [PATCH 58/78] feat(tests): changed base url handling logic, added build type checking for push permission --- .../auth_screen/auth_login_test.dart | 7 +- .../auth_screen/auth_logo_button_test.dart | 3 +- lib/core/app_config.dart | 24 +++++- lib/core/app_initializer.dart | 29 +++----- lib/core/const.dart | 3 - lib/core/di/app_config_registration.dart | 12 +++ lib/core/di/external_registrations.dart | 6 +- lib/core/firebase_api.dart | 6 +- .../phone_verify_error_interceptor.dart | 0 lib/core/locator_service.dart | 17 +++-- lib/core/routing/other_routes.dart | 4 +- lib/core/websockets/ws_manager.dart | 7 +- lib/core/widgets/custom_popup.dart | 10 +-- .../auth/presentation/pages/auth_screen.dart | 7 +- .../pages/scan_qr_code_screen.dart | 31 ++------ .../presentation/pages/blog_post_screen.dart | 8 +- .../pages/blog_post_create_screen.dart | 4 +- .../repositories/blog_post_repository.dart | 5 +- .../widgets/bottom_navigation_bar_widget.dart | 5 +- .../presentation/pages/photo_view_screen.dart | 5 +- .../chat/presentation/widgets/order_card.dart | 5 +- .../presentation/pages/last_step_screen.dart | 4 +- .../presentation/cubit/create_diet_cubit.dart | 4 +- .../create_recipe_cubit.dart | 7 +- .../presentation/dev_tools_screen.dart | 20 ++--- .../widgets/photo_gallery_widget.dart | 7 +- .../presentation/pages/about_diet_screen.dart | 4 +- .../presentation/widgets/author_card.dart | 5 +- .../presentation/widgets/more_options.dart | 7 +- .../widgets/blog_post_widget.dart | 15 ++-- .../presentation/widgets/comments_widget.dart | 6 +- .../widgets/consultant_widget.dart | 74 +++++-------------- .../presentation/widgets/diets_widget.dart | 7 +- .../presentation/widgets/plan_widget.dart | 9 ++- .../presentation/widgets/recipes_widget.dart | 7 +- .../presentation/pages/glossary_screen.dart | 5 +- .../menu/presentation/pages/menu_screen.dart | 3 +- .../widgets/blocked_user_widget.dart | 5 +- .../presentation/widgets/my_diet_widget.dart | 7 +- .../presentation/widgets/my_plan_widget.dart | 7 +- .../presentation/widgets/plan_info.dart | 5 +- .../presentation/pages/about_plan_screen.dart | 4 +- .../presentation/pages/profile_screen.dart | 11 +-- .../widgets/profile_blog_post_widget.dart | 13 ++-- .../presentation/widgets/sub_widget.dart | 5 +- .../pages/about_recipe_screen.dart | 4 +- .../widgets/my_recipe_widget.dart | 6 +- .../presentation/widgets/steps_widget.dart | 5 +- .../presentation/pages/verify_screen.dart | 5 +- .../pages/webview_registration_screen.dart | 4 +- .../presentation/pages/support_screen.dart | 8 +- lib/main.dart | 11 +-- 52 files changed, 245 insertions(+), 237 deletions(-) create mode 100644 lib/core/di/app_config_registration.dart rename lib/core/{dio => interceptors}/phone_verify_error_interceptor.dart (100%) diff --git a/integration_test/auth_screen/auth_login_test.dart b/integration_test/auth_screen/auth_login_test.dart index 711e0aa3..ce538770 100644 --- a/integration_test/auth_screen/auth_login_test.dart +++ b/integration_test/auth_screen/auth_login_test.dart @@ -1,9 +1,8 @@ import 'package:eatvio_app/app_keys.dart'; import 'package:eatvio_app/core/app_initializer.dart'; +import 'package:eatvio_app/main.dart' as app; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import 'package:eatvio_app/main.dart' as app; - import '../common_cases/skip_welcome_screen.dart'; void main() { @@ -13,7 +12,7 @@ void main() { await AppInitializer.initialize(buildType: AppBuildType.tests); }); - testWidgets('Signs in successfully with valid credentials', (WidgetTester tester) async { + testWidgets('Signs in successfully with valid credentials', (WidgetTester tester) async { await tester.pumpWidget(const app.MyApp()); await tester.pumpAndSettle(const Duration(seconds: 3)); await skipWelcomeScreen(tester); @@ -25,7 +24,7 @@ void main() { expect(passwordFieldFinder, findsOneWidget, reason: 'Password text field should be found.'); expect(signInButtonFinder, findsOneWidget, reason: 'Sign In button should be found.'); - await tester.enterText(emailFieldFinder, 'reshetnik.n+user3@gmail.com'); + await tester.enterText(emailFieldFinder, 'reshetnick.mike+user4@gmail.com'); await tester.enterText(passwordFieldFinder, '1234567890'); await tester.pumpAndSettle(); diff --git a/integration_test/auth_screen/auth_logo_button_test.dart b/integration_test/auth_screen/auth_logo_button_test.dart index c427f327..c7ac598d 100644 --- a/integration_test/auth_screen/auth_logo_button_test.dart +++ b/integration_test/auth_screen/auth_logo_button_test.dart @@ -4,7 +4,6 @@ import 'package:eatvio_app/features/dev_tools/presentation/dev_tools_screen.dart import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:eatvio_app/main.dart' as app; - import '../common_cases/skip_welcome_screen.dart'; void main() { @@ -15,7 +14,7 @@ void main() { }); testWidgets('tapping logo 7 times opens dev tools', (WidgetTester tester) async { - await tester.pumpWidget(const app.MyApp()); + await tester.pumpWidget(const app.MyApp()); await tester.pumpAndSettle(const Duration(seconds: 3)); await skipWelcomeScreen(tester); final logoButtonFinder = find.byKey(AppKeys.hiddenLogoButtonAuthScreen); diff --git a/lib/core/app_config.dart b/lib/core/app_config.dart index 3fb06b39..03513645 100644 --- a/lib/core/app_config.dart +++ b/lib/core/app_config.dart @@ -1,3 +1,5 @@ +import 'package:eatvio_app/core/app_initializer.dart'; +import 'package:eatvio_app/core/env_config.dart'; import 'package:eatvio_app/core/internet_connection_cubit/internet_connection_cubit.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/views/cubit/views_cubit.dart'; @@ -13,8 +15,24 @@ import 'package:flutter_localizations/flutter_localizations.dart'; /// Конфигурация приложения class AppConfig { + AppConfig({required this.buildType}){ + _baseUrl = switch (buildType) { + AppBuildType.prod => EnvConfig.baseUrl, + AppBuildType.tests => EnvConfig.devBaseUrl, + AppBuildType.dev => EnvConfig.devBaseUrl, + }; + } + + final AppBuildType buildType; + late final String _baseUrl; + String get baseUrl => _baseUrl; + + void setBaseUrl(String baseUrls) { + _baseUrl = baseUrls; + } + /// Список глобальных BlocProvider'ов - static List get globalBlocProviders => [ + List get globalBlocProviders => [ BlocProvider(create: (context) => sl()), BlocProvider(create: (context) => sl()..getNotifications()), BlocProvider(create: (context) => sl()..loadFeedBlogPostList()), @@ -25,7 +43,7 @@ class AppConfig { ]; /// Делегаты локализации - static const List localizationDelegates = [ + List get localizationDelegates => [ S.delegate, GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, @@ -33,5 +51,5 @@ class AppConfig { ]; /// Поддерживаемые локали - static const List supportedLocales = [Locale.fromSubtags(languageCode: 'ru')]; + List get supportedLocales => [const Locale.fromSubtags(languageCode: 'ru')]; } diff --git a/lib/core/app_initializer.dart b/lib/core/app_initializer.dart index 45e89fcf..57ccbfb0 100644 --- a/lib/core/app_initializer.dart +++ b/lib/core/app_initializer.dart @@ -1,10 +1,12 @@ import 'dart:io' as io; -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/env_config.dart'; import 'package:eatvio_app/core/locator_service.dart' as di; +import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/firebase_options.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; @@ -13,21 +15,12 @@ import 'package:sentry_flutter/sentry_flutter.dart'; /// Класс для инициализации приложения class AppInitializer { - /// Инициализация всех сервисов приложения - static Future initialize({AppBuildType buildType = AppBuildType.prod}) async { + static Future initialize({required AppBuildType buildType}) async { await _initializeEnvironment(); - switch (buildType) { - case AppBuildType.tests: - baseUrl = EnvConfig.devBaseUrl; - buildType = AppBuildType.tests; - break; - case AppBuildType.prod: - break; - } - await _initializeSentry(); + await _initializeDependencyInjection(buildType: buildType); + await _initializeSentry(); await _initializeFirebase(); - await _initializeDependencyInjection(); await _initializeDebugMode(); await _initializeAnalytics(); } @@ -46,12 +39,14 @@ class AppInitializer { /// Инициализация Firebase static Future _initializeFirebase() async { await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); - await FirebaseMessaging.instance.requestPermission(); + if (sl().buildType == AppBuildType.prod) { + await FirebaseMessaging.instance.requestPermission(); + } } /// Инициализация системы внедрения зависимостей - static Future _initializeDependencyInjection() async { - await di.init(); + static Future _initializeDependencyInjection({required AppBuildType buildType}) async { + await di.init(buildType: buildType); } /// Настройка режима отладки @@ -77,4 +72,4 @@ class AppInitializer { } } -enum AppBuildType { prod, tests } +enum AppBuildType { prod, tests, dev } diff --git a/lib/core/const.dart b/lib/core/const.dart index a5f06149..5aab4fd4 100644 --- a/lib/core/const.dart +++ b/lib/core/const.dart @@ -1,6 +1,3 @@ -import 'package:eatvio_app/core/env_config.dart'; - -String baseUrl = EnvConfig.devBaseUrl; bool hidePlans = const bool.fromEnvironment('HIDE_PLANS', defaultValue: false); const buildDate = String.fromEnvironment('BUILD_DATE', defaultValue: ''); diff --git a/lib/core/di/app_config_registration.dart b/lib/core/di/app_config_registration.dart new file mode 100644 index 00000000..de003d20 --- /dev/null +++ b/lib/core/di/app_config_registration.dart @@ -0,0 +1,12 @@ +import 'package:eatvio_app/core/app_config.dart'; +import 'package:eatvio_app/core/app_initializer.dart'; +import 'package:get_it/get_it.dart'; + +class AppConfigRegistration { + static final sl = GetIt.instance; + + /// Регистрация AppConfig + static void registerAppConfig({required AppBuildType buildType}) { + sl.registerLazySingleton(() => AppConfig(buildType: buildType)); + } +} diff --git a/lib/core/di/external_registrations.dart b/lib/core/di/external_registrations.dart index 38b3caa0..231eeef3 100644 --- a/lib/core/di/external_registrations.dart +++ b/lib/core/di/external_registrations.dart @@ -1,6 +1,6 @@ import 'package:dio/dio.dart'; -import 'package:eatvio_app/core/const.dart'; -import 'package:eatvio_app/core/dio/phone_verify_error_interceptor.dart'; +import 'package:eatvio_app/core/app_config.dart'; +import 'package:eatvio_app/core/interceptors/phone_verify_error_interceptor.dart'; import 'package:eatvio_app/core/interceptors/token_interceptor.dart'; import 'package:get_it/get_it.dart'; @@ -18,7 +18,7 @@ class ExternalRegistrations { sl.registerLazySingleton( () => Dio( BaseOptions( - baseUrl: baseUrl, + baseUrl: sl().baseUrl, headers: { 'accept': 'application/json', 'content-type': 'application/json', diff --git a/lib/core/firebase_api.dart b/lib/core/firebase_api.dart index 2d89bd43..7f4baad7 100644 --- a/lib/core/firebase_api.dart +++ b/lib/core/firebase_api.dart @@ -1,5 +1,7 @@ import 'dart:developer'; import 'dart:io'; +import 'package:eatvio_app/core/app_config.dart'; +import 'package:eatvio_app/core/app_initializer.dart'; import 'package:eatvio_app/core/utils/toasts.dart'; import 'package:flutter/services.dart'; @@ -19,7 +21,9 @@ class FirebaseApi { if (await _isHuaweiDevice()) { token = await _getHuaweiPushTokenNative(); } else { - await _firebaseMessaging.requestPermission(); + if ((sl().buildType == AppBuildType.prod)) { + await _firebaseMessaging.requestPermission(); + } token = await _firebaseMessaging.getToken(); } return token; diff --git a/lib/core/dio/phone_verify_error_interceptor.dart b/lib/core/interceptors/phone_verify_error_interceptor.dart similarity index 100% rename from lib/core/dio/phone_verify_error_interceptor.dart rename to lib/core/interceptors/phone_verify_error_interceptor.dart diff --git a/lib/core/locator_service.dart b/lib/core/locator_service.dart index 8299fea2..d3ed9b95 100644 --- a/lib/core/locator_service.dart +++ b/lib/core/locator_service.dart @@ -1,3 +1,5 @@ +import 'package:eatvio_app/core/app_initializer.dart'; +import 'package:eatvio_app/core/di/app_config_registration.dart'; import 'package:eatvio_app/core/di/cubit_registrations.dart'; import 'package:eatvio_app/core/di/external_registrations.dart'; import 'package:eatvio_app/core/di/repository_registrations.dart'; @@ -9,19 +11,22 @@ import 'package:get_it/get_it.dart'; final sl = GetIt.instance; /// Инициализация системы внедрения зависимостей -Future init() async { +Future init({required AppBuildType buildType}) async { + // Регистрация AppConfig + AppConfigRegistration.registerAppConfig(buildType: buildType); + // Регистрация внешних зависимостей (Dio, etc.) ExternalRegistrations.registerAll(); - + // Регистрация сервисов ServiceRegistrations.registerAll(); - + // Регистрация репозиториев RepositoryRegistrations.registerAll(); - + // Регистрация Cubit'ов CubitRegistrations.registerAll(); - + // Регистрация WebSocket менеджера sl.registerLazySingleton(() => WsManager(wsService: sl())); -} \ No newline at end of file +} diff --git a/lib/core/routing/other_routes.dart b/lib/core/routing/other_routes.dart index 43cf885f..7ae20a0f 100644 --- a/lib/core/routing/other_routes.dart +++ b/lib/core/routing/other_routes.dart @@ -1,5 +1,5 @@ import 'package:dio/dio.dart'; -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/firebase_api.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/routing/route_constants.dart'; @@ -76,7 +76,7 @@ class OtherRoutes { builder: (context, state) => const UpdateScreen(), redirect: (context, state) async { try { - final versions = (await sl().get('$baseUrl/api/app_versions')).data; + final versions = (await sl().get('${sl().baseUrl}/api/app_versions')).data; PackageInfo packageInfo = await PackageInfo.fromPlatform(); String version = packageInfo.version; Version currentVersion = Version.parse(version); diff --git a/lib/core/websockets/ws_manager.dart b/lib/core/websockets/ws_manager.dart index 528f2ae9..24430f6c 100644 --- a/lib/core/websockets/ws_manager.dart +++ b/lib/core/websockets/ws_manager.dart @@ -3,6 +3,7 @@ import 'dart:developer'; import 'package:dart_pusher_channels/dart_pusher_channels.dart'; import 'package:easy_debounce/easy_debounce.dart'; +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/websockets/ws_service.dart'; @@ -111,7 +112,7 @@ class WsManager { _privateNotificationChannel = client?.privateChannel( '$newNotificationsChannelName.$userId', authorizationDelegate: EndpointAuthorizableChannelTokenAuthorizationDelegate.forPrivateChannel( - authorizationEndpoint: Uri.parse('$baseUrl/broadcasting/auth'), + authorizationEndpoint: Uri.parse('${sl().baseUrl}/broadcasting/auth'), headers: { 'Authorization': 'Bearer $token', }, @@ -124,7 +125,7 @@ class WsManager { _privateMessagesChannel = client?.privateChannel( '$newMessagesChannelName.$userId', authorizationDelegate: EndpointAuthorizableChannelTokenAuthorizationDelegate.forPrivateChannel( - authorizationEndpoint: Uri.parse('$baseUrl/broadcasting/auth'), + authorizationEndpoint: Uri.parse('${sl().baseUrl}/broadcasting/auth'), headers: { 'Authorization': 'Bearer $token', }, @@ -137,7 +138,7 @@ class WsManager { _privateDirectChatChannel = client?.privateChannel( '$directChatChannelName.$chatId', authorizationDelegate: EndpointAuthorizableChannelTokenAuthorizationDelegate.forPrivateChannel( - authorizationEndpoint: Uri.parse('$baseUrl/broadcasting/auth'), + authorizationEndpoint: Uri.parse('${sl().baseUrl}/broadcasting/auth'), headers: { 'Authorization': 'Bearer $token', }, diff --git a/lib/core/widgets/custom_popup.dart b/lib/core/widgets/custom_popup.dart index b5934f4d..dbe0090b 100644 --- a/lib/core/widgets/custom_popup.dart +++ b/lib/core/widgets/custom_popup.dart @@ -1,4 +1,4 @@ -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/enums/complained_content.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; @@ -86,7 +86,7 @@ class CustomPopup extends StatelessWidget { if (isAuthor) { addItem('assets/icons/copy2.svg', 'Скопировать ссылку', () { - Clipboard.setData(ClipboardData(text: '$baseUrl/ru/$path')); + Clipboard.setData(ClipboardData(text: '${sl().baseUrl}/ru/$path')); showInfoToast(s.link_was_copied); }); addItem('assets/icons/share2.svg', 'Поделиться', () { @@ -94,7 +94,7 @@ class CustomPopup extends StatelessWidget { SharePlus.instance.share( ShareParams( uri: Uri.parse( - '$baseUrl/ru/$path', + '${sl().baseUrl}/ru/$path', ), sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, ), @@ -140,7 +140,7 @@ class CustomPopup extends StatelessWidget { }); } else { addItem('assets/icons/copy2.svg', 'Скопировать ссылку', () { - Clipboard.setData(ClipboardData(text: '$baseUrl/ru/$path')); + Clipboard.setData(ClipboardData(text: '${sl().baseUrl}/ru/$path')); showInfoToast(s.link_was_copied); }); addItem('assets/icons/share2.svg', 'Поделиться', () { @@ -148,7 +148,7 @@ class CustomPopup extends StatelessWidget { SharePlus.instance.share( ShareParams( uri: Uri.parse( - '$baseUrl/ru/$path', + '${sl().baseUrl}/ru/$path', ), sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, ), diff --git a/lib/features/auth/presentation/pages/auth_screen.dart b/lib/features/auth/presentation/pages/auth_screen.dart index ce6fee32..102f31cb 100644 --- a/lib/features/auth/presentation/pages/auth_screen.dart +++ b/lib/features/auth/presentation/pages/auth_screen.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'dart:developer'; import 'package:eatvio_app/app_keys.dart'; +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/app_router.dart'; import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/firebase_api.dart'; @@ -229,7 +230,7 @@ class _AuthScreenState extends State { decorationColor: appTheme.blueA400, decoration: TextDecoration.underline, ), - recognizer: TapGestureRecognizer()..onTap = () async => await launchUrl(Uri.parse('$baseUrl/oferta_bs.pdf')), + recognizer: TapGestureRecognizer()..onTap = () async => await launchUrl(Uri.parse('${sl().baseUrl}/oferta_bs.pdf')), ), const TextSpan(text: ' и '), ], @@ -241,7 +242,7 @@ class _AuthScreenState extends State { decorationColor: appTheme.blueA400, decoration: TextDecoration.underline, ), - recognizer: TapGestureRecognizer()..onTap = () async => await launchUrl(Uri.parse('$baseUrl/docs/privacy.pdf')), + recognizer: TapGestureRecognizer()..onTap = () async => await launchUrl(Uri.parse('${sl().baseUrl}/docs/privacy.pdf')), ), ], ), @@ -308,7 +309,7 @@ class _AuthScreenState extends State { children: [ const Text('Где взять QR-код?\n', style: TextStyle(fontSize: 18)), Text( - '1. Войдите на сайте $baseUrl в свой аккаунт\n\n2. Перейдите в настройки профиля\n\n3. В разделе "Доп информация" перейдите на "QR Login"', + '1. Войдите на сайте ${sl().baseUrl} в свой аккаунт\n\n2. Перейдите в настройки профиля\n\n3. В разделе "Доп информация" перейдите на "QR Login"', ), const SizedBox(height: 12), Row( diff --git a/lib/features/auth/presentation/pages/scan_qr_code_screen.dart b/lib/features/auth/presentation/pages/scan_qr_code_screen.dart index 83296f0e..723f95e9 100644 --- a/lib/features/auth/presentation/pages/scan_qr_code_screen.dart +++ b/lib/features/auth/presentation/pages/scan_qr_code_screen.dart @@ -1,8 +1,8 @@ import 'dart:io'; import 'package:dio/dio.dart'; +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/app_router.dart'; -import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/firebase_api.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/token_storage.dart'; @@ -40,17 +40,11 @@ class _ScanQRCodeScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - title: const Text('Сканирование QR-кода'), - centerTitle: true, - ), + appBar: AppBar(title: const Text('Сканирование QR-кода'), centerTitle: true), body: Column( children: [ Expanded( - child: QRView( - key: qrKey, - onQRViewCreated: _onQRViewCreated, - ), + child: QRView(key: qrKey, onQRViewCreated: _onQRViewCreated), ), ], ), @@ -68,17 +62,9 @@ class _ScanQRCodeScreenState extends State { final scannedUrl = scanData.code; final uri = Uri.parse(scannedUrl ?? ''); final scannedBaseUrl = (uri.hasAuthority) ? '${uri.scheme}://${uri.host}' : ''; - baseUrl = scannedBaseUrl; - sl().options.baseUrl = baseUrl; - final response = await Dio().getUri( - uri, - options: Options( - headers: { - 'accept': 'application/json', - 'content-type': 'application/json', - }, - ), - ); + sl().setBaseUrl(scannedBaseUrl); + sl().options.baseUrl = scannedBaseUrl; + final response = await Dio().getUri(uri, options: Options(headers: {'accept': 'application/json', 'content-type': 'application/json'})); final token = response.data['token']; await TokenStorage.saveTokenInStorage(token); try { @@ -97,10 +83,7 @@ class _ScanQRCodeScreenState extends State { } catch (e) { rethrow; } - AppRouter.navigatorKey.currentContext?.go( - '/feed-screen', - extra: {'needToInitializeWebSocket': false}, - ); + AppRouter.navigatorKey.currentContext?.go('/feed-screen', extra: {'needToInitializeWebSocket': false}); showSuccessToast('Добро пожаловать!'); } catch (e) { await controller.resumeCamera(); 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 61b64f9e..76562bcd 100644 --- a/lib/features/blog_post/presentation/pages/blog_post_screen.dart +++ b/lib/features/blog_post/presentation/pages/blog_post_screen.dart @@ -1,6 +1,6 @@ import 'dart:io'; import 'package:cached_network_image/cached_network_image.dart'; -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/enums/complained_content.dart'; import 'package:eatvio_app/core/utils/date_time_utils.dart'; import 'package:eatvio_app/core/widgets/custom_popup.dart'; @@ -207,7 +207,7 @@ class _BlogPostScreenState extends State { children: [ CircleAvatar( radius: 16, - foregroundImage: NetworkImage(baseUrl + (blogPostState.blogPostModel?.author.authorProfileIcon ?? '')), + foregroundImage: NetworkImage(sl().baseUrl + (blogPostState.blogPostModel?.author.authorProfileIcon ?? '')), backgroundImage: const AssetImage('assets/images/no_avatar.png'), ), const SizedBox(width: 8), @@ -239,7 +239,7 @@ class _BlogPostScreenState extends State { ), const SizedBox(height: 8), if (blogPostState.blogPostModel?.image.isNotEmpty ?? false) - CachedNetworkImage(imageUrl: '$baseUrl/storage/${blogPostState.blogPostModel?.image ?? ''}'), + CachedNetworkImage(imageUrl: '${sl().baseUrl}/storage/${blogPostState.blogPostModel?.image ?? ''}'), Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Row( @@ -267,7 +267,7 @@ class _BlogPostScreenState extends State { final box = context.findRenderObject() as RenderBox?; SharePlus.instance.share( ShareParams( - uri: Uri.parse('$baseUrl/ru/blog_post/${widget.id}'), + uri: Uri.parse('${sl().baseUrl}/ru/blog_post/${widget.id}'), sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, ), ); diff --git a/lib/features/blog_post_create/presentation/pages/blog_post_create_screen.dart b/lib/features/blog_post_create/presentation/pages/blog_post_create_screen.dart index 6be43b3a..40ad3dfa 100644 --- a/lib/features/blog_post_create/presentation/pages/blog_post_create_screen.dart +++ b/lib/features/blog_post_create/presentation/pages/blog_post_create_screen.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'dart:io'; +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/app_router.dart'; -import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/permission_handler/permission_handler.dart'; import 'package:eatvio_app/core/theme/app_decoration.dart'; import 'package:eatvio_app/core/widgets/show_error_dialog.dart'; @@ -304,7 +304,7 @@ class _BlogPostCreateScreenState extends State { child: ClipRRect( borderRadius: BorderRadius.circular(16), child: Image( - image: NetworkImage('$baseUrl/storage/${widget.feedBlogPostModel!.image}'), + image: NetworkImage('${sl().baseUrl}/storage/${widget.feedBlogPostModel!.image}'), ), ), ), diff --git a/lib/features/blog_post_create/repositories/blog_post_repository.dart b/lib/features/blog_post_create/repositories/blog_post_repository.dart index aeea8474..48f4f0c1 100644 --- a/lib/features/blog_post_create/repositories/blog_post_repository.dart +++ b/lib/features/blog_post_create/repositories/blog_post_repository.dart @@ -1,7 +1,8 @@ import 'dart:developer'; import 'dart:io'; import 'package:dio/dio.dart'; -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; +import 'package:eatvio_app/core/locator_service.dart'; import 'package:http/http.dart' as http; import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart'; import 'package:eatvio_app/features/blog_post_create/services/blog_post_service.dart'; @@ -36,7 +37,7 @@ class BlogPostRepository { Future downloadImage(String imageUrl) async { try { // Загрузка изображения - final response = await http.get(Uri.parse('$baseUrl/storage/$imageUrl')); + final response = await http.get(Uri.parse('${sl().baseUrl}/storage/$imageUrl')); if (response.statusCode == 200) { // Получение временной директории diff --git a/lib/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart b/lib/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart index a67fbbaf..b1e846ca 100644 --- a/lib/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart +++ b/lib/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart @@ -1,5 +1,6 @@ import 'package:badges/badges.dart' as badges; -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; +import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/utils/image_constant.dart'; import 'package:eatvio_app/features/dev_tools/presentation/dev_tools_screen.dart'; import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart'; @@ -102,7 +103,7 @@ class _BottomNavigationBarWidgetState extends State { label: '', icon: CircleAvatar( foregroundImage: NetworkImage( - widget.profileModel.profilePhotoUrl.contains('https') ? widget.profileModel.profilePhotoUrl : baseUrl + widget.profileModel.profilePhotoUrl, + widget.profileModel.profilePhotoUrl.contains('https') ? widget.profileModel.profilePhotoUrl : sl().baseUrl + widget.profileModel.profilePhotoUrl, ), backgroundImage: const AssetImage('assets/images/no_avatar.png'), radius: 16, diff --git a/lib/features/chat/presentation/pages/photo_view_screen.dart b/lib/features/chat/presentation/pages/photo_view_screen.dart index 1c17d40e..3cbdc744 100644 --- a/lib/features/chat/presentation/pages/photo_view_screen.dart +++ b/lib/features/chat/presentation/pages/photo_view_screen.dart @@ -1,6 +1,7 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:dismissible_page/dismissible_page.dart'; -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; +import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/app_decoration.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; @@ -88,7 +89,7 @@ class _PhotoViewScreenState extends State { color: Colors.white, ), title: Text( - widget.imageUrls[index].replaceAll('$baseUrl/storage/chat_files/', ''), + widget.imageUrls[index].replaceAll('${sl().baseUrl}/storage/chat_files/', ''), style: const TextStyle( color: Colors.white, fontSize: 12, diff --git a/lib/features/chat/presentation/widgets/order_card.dart b/lib/features/chat/presentation/widgets/order_card.dart index 31d99aad..2b7b50b2 100644 --- a/lib/features/chat/presentation/widgets/order_card.dart +++ b/lib/features/chat/presentation/widgets/order_card.dart @@ -1,4 +1,5 @@ -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; +import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/utils/localization_utils.dart'; @@ -47,7 +48,7 @@ class _OrderCardState extends State { ClipRRect( borderRadius: BorderRadius.circular(12), child: Image.network( - '$baseUrl/storage/${widget.plan.image}', + '${sl().baseUrl}/storage/${widget.plan.image}', width: double.infinity, fit: BoxFit.cover, ), diff --git a/lib/features/consultant/become_consultant/presentation/pages/last_step_screen.dart b/lib/features/consultant/become_consultant/presentation/pages/last_step_screen.dart index d44920cb..734387f1 100644 --- a/lib/features/consultant/become_consultant/presentation/pages/last_step_screen.dart +++ b/lib/features/consultant/become_consultant/presentation/pages/last_step_screen.dart @@ -1,4 +1,6 @@ +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/features/consultant/become_consultant/presentation/bloc/register_consultant_cubit/register_consultant_cubit.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:flutter/material.dart'; @@ -49,7 +51,7 @@ class _LastStepScreenState extends State { Padding( padding: const EdgeInsets.symmetric(horizontal: 4), child: TextButton( - onPressed: () async => await launchUrl(Uri.parse(hidePlans ? '$baseUrl/docs/privacy.pdf' : '$baseUrl/oferta_bs.pdf')), + onPressed: () async => await launchUrl(Uri.parse(hidePlans ? '${sl().baseUrl}/docs/privacy.pdf' : '${sl().baseUrl}/oferta_bs.pdf')), child: Text( hidePlans ? 'Политика конфиденциальности и пользовательское соглашение' : 'Договор офёрты', style: const TextStyle(height: 1, fontSize: 16, color: Color(0xFF323F4B), decoration: TextDecoration.underline), diff --git a/lib/features/create_diet/presentation/cubit/create_diet_cubit.dart b/lib/features/create_diet/presentation/cubit/create_diet_cubit.dart index 6eb41358..757101ae 100644 --- a/lib/features/create_diet/presentation/cubit/create_diet_cubit.dart +++ b/lib/features/create_diet/presentation/cubit/create_diet_cubit.dart @@ -2,8 +2,10 @@ import 'dart:convert'; import 'dart:io'; import 'dart:developer'; import 'package:dio/dio.dart'; +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart'; +import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/features/create_diet/presentation/cubit/create_diet_state.dart'; import 'package:eatvio_app/features/create_diet/repositories/create_diet_repository.dart'; import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; @@ -53,7 +55,7 @@ class CreateDietCubit extends Cubit { name: diet.name, duration: diet.duration, type: type, - imageUrl: '$baseUrl/storage/${diet.image}', + imageUrl: '${sl().baseUrl}/storage/${diet.image}', ), ); emit(state.copyWith(dietState: diet.state, durationIndex: durationValues.indexOf(diet.duration))); diff --git a/lib/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart b/lib/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart index 4ca081d4..4541782b 100644 --- a/lib/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart +++ b/lib/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart @@ -1,7 +1,8 @@ import 'dart:developer'; import 'dart:io'; -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; +import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/features/create_recipe/entities/recipe_create_entity.dart'; import 'package:eatvio_app/features/create_recipe/models/ingredient.dart' as i; import 'package:eatvio_app/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_state.dart'; @@ -187,13 +188,13 @@ class CreateRecipeCubit extends Cubit { } Future recipeToEntity(Recipe recipe) async { - final file = await downloadImageAsFile('$baseUrl/storage/${recipe.image}'); + final file = await downloadImageAsFile('${sl().baseUrl}/storage/${recipe.image}'); final List steps = await Future.wait( recipe.steps!.map( (e) async => StepEntity( description: e.description, - image: e.image.isEmpty ? null : await downloadImageAsFile('$baseUrl/storage/${e.image}'), + image: e.image.isEmpty ? null : await downloadImageAsFile('${sl().baseUrl}/storage/${e.image}'), ), ), ); diff --git a/lib/features/dev_tools/presentation/dev_tools_screen.dart b/lib/features/dev_tools/presentation/dev_tools_screen.dart index 4922a87d..0be58bee 100644 --- a/lib/features/dev_tools/presentation/dev_tools_screen.dart +++ b/lib/features/dev_tools/presentation/dev_tools_screen.dart @@ -1,8 +1,8 @@ import 'dart:io'; import 'package:dio/dio.dart'; +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/app_router.dart'; -import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/firebase_api.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/utils/toasts.dart'; @@ -84,7 +84,7 @@ class _DevToolsScreenContentState extends State { void initState() { super.initState(); setState(() { - baseUrlForTest = baseUrl; + baseUrlForTest = sl().baseUrl; }); testsStates = List.generate(testCases.length, (index) => TestState.notRun); } @@ -111,8 +111,8 @@ class _DevToolsScreenContentState extends State { onPressed: () { setState(() { baseUrlForTest = controller.text; - baseUrl = baseUrlForTest.toLowerCase(); - sl().options.baseUrl = baseUrl; + sl().setBaseUrl(baseUrlForTest.toLowerCase()); + sl().options.baseUrl = baseUrlForTest.toLowerCase(); controller.clear(); }); }, @@ -122,23 +122,23 @@ class _DevToolsScreenContentState extends State { ), const SizedBox(height: 10), CustomOutlinedButton( - text: baseUrl == EnvConfig.devBaseUrl ? 'Set production base url' : 'Set dev2 base url', + text: sl().baseUrl == EnvConfig.devBaseUrl ? 'Set production base url' : 'Set dev2 base url', onPressed: () { setState(() { - if (baseUrl == EnvConfig.baseUrl) { + if (sl().baseUrl == EnvConfig.baseUrl) { baseUrlForTest = EnvConfig.devBaseUrl; } else { baseUrlForTest = EnvConfig.baseUrl; } - baseUrl = baseUrlForTest.toLowerCase(); - sl().options.baseUrl = baseUrl; + sl().setBaseUrl(baseUrlForTest.toLowerCase()); + sl().options.baseUrl = baseUrlForTest.toLowerCase(); controller.clear(); }); }, buttonStyle: const ButtonStyle(), ), - SizedBox(height: baseUrlForTest != baseUrl ? 40 : 10), - if (baseUrl.toLowerCase() != EnvConfig.baseUrl.toLowerCase()) + SizedBox(height: baseUrlForTest != sl().baseUrl ? 40 : 10), + if (sl().baseUrl.toLowerCase() != EnvConfig.baseUrl.toLowerCase()) ListView.builder( shrinkWrap: true, itemCount: testCases.length, diff --git a/lib/features/diary/presentation/widgets/photo_gallery_widget.dart b/lib/features/diary/presentation/widgets/photo_gallery_widget.dart index 352b6519..87643817 100644 --- a/lib/features/diary/presentation/widgets/photo_gallery_widget.dart +++ b/lib/features/diary/presentation/widgets/photo_gallery_widget.dart @@ -1,8 +1,9 @@ import 'dart:io'; import 'package:cached_network_image/cached_network_image.dart'; +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/app_router.dart'; -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/permission_handler/permission_handler.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/features/diary/models/diary_day_model.dart'; @@ -139,7 +140,7 @@ class _PhotoGalleryWidgetState extends State { ClipRRect( borderRadius: BorderRadius.circular(12), child: CachedNetworkImage( - imageUrl: '$baseUrl/storage/${state.diary.meals[mealIndex].photos[currentSelectedPhotoIndex].path}', + imageUrl: '${sl().baseUrl}/storage/${state.diary.meals[mealIndex].photos[currentSelectedPhotoIndex].path}', ), ), if (!state.isConsultantView && !state.isDiaryFinished && !state.isDiaryCommented) @@ -211,7 +212,7 @@ class _PhotoGalleryWidgetState extends State { height: 52, width: 52, child: CachedNetworkImage( - imageUrl: '$baseUrl/storage/${state.diary.meals[mealIndex].photos[i].path}', + imageUrl: '${sl().baseUrl}/storage/${state.diary.meals[mealIndex].photos[i].path}', fit: BoxFit.fitWidth, ), ), diff --git a/lib/features/diets/presentation/pages/about_diet_screen.dart b/lib/features/diets/presentation/pages/about_diet_screen.dart index e4dd4a75..e06989f1 100644 --- a/lib/features/diets/presentation/pages/about_diet_screen.dart +++ b/lib/features/diets/presentation/pages/about_diet_screen.dart @@ -1,7 +1,7 @@ import 'dart:io'; import 'package:cached_network_image/cached_network_image.dart'; -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/enums/complained_content.dart'; import 'package:eatvio_app/core/enums/product_type.dart'; import 'package:eatvio_app/core/utils/localization_utils.dart'; @@ -353,7 +353,7 @@ class _AboutDietScreenState extends State { Stack( children: [ CachedNetworkImage( - imageUrl: '$baseUrl/storage/${dietState.diet.image}', + imageUrl: '${sl().baseUrl}/storage/${dietState.diet.image}', height: 300, width: MediaQuery.sizeOf(context).width, fit: BoxFit.fill, diff --git a/lib/features/diets/presentation/widgets/author_card.dart b/lib/features/diets/presentation/widgets/author_card.dart index 6fff0b93..548b9263 100644 --- a/lib/features/diets/presentation/widgets/author_card.dart +++ b/lib/features/diets/presentation/widgets/author_card.dart @@ -1,4 +1,5 @@ -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; +import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/features/diets/presentation/widgets/info_bouble.dart'; import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; import 'package:eatvio_app/core/theme/custom_text_style.dart'; @@ -37,7 +38,7 @@ class AuthorCard extends StatelessWidget { decoration: BoxDecoration( shape: BoxShape.circle, image: DecorationImage( - image: NetworkImage(baseUrl + author.authorProfileIcon), + image: NetworkImage(sl().baseUrl + author.authorProfileIcon), fit: BoxFit.cover, ), ), diff --git a/lib/features/diets/presentation/widgets/more_options.dart b/lib/features/diets/presentation/widgets/more_options.dart index 4c28e5c9..480b9915 100644 --- a/lib/features/diets/presentation/widgets/more_options.dart +++ b/lib/features/diets/presentation/widgets/more_options.dart @@ -1,5 +1,6 @@ -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/enums/complained_content.dart'; +import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/utils/toasts.dart'; import 'package:eatvio_app/features/complaining/presentation/complaining_bottom_sheet.dart'; import 'package:eatvio_app/core/widgets/custom_divider.dart'; @@ -35,7 +36,7 @@ class MoreOptions extends StatelessWidget { onTap: disableActions == true ? null : () { - Clipboard.setData(ClipboardData(text: '$baseUrl/ru/$path')); + Clipboard.setData(ClipboardData(text: '${sl().baseUrl}/ru/$path')); showInfoToast(s.link_was_copied); }, child: Row( @@ -62,7 +63,7 @@ class MoreOptions extends StatelessWidget { final box = context.findRenderObject() as RenderBox?; SharePlus.instance.share( ShareParams( - uri: Uri.parse('$baseUrl/ru/$path',), + uri: Uri.parse('${sl().baseUrl}/ru/$path',), sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, ), ); diff --git a/lib/features/feed/presentation/widgets/blog_post_widget.dart b/lib/features/feed/presentation/widgets/blog_post_widget.dart index e001a038..261e421a 100644 --- a/lib/features/feed/presentation/widgets/blog_post_widget.dart +++ b/lib/features/feed/presentation/widgets/blog_post_widget.dart @@ -1,7 +1,8 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:dismissible_page/dismissible_page.dart'; -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/enums/complained_content.dart'; +import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/utils/date_time_utils.dart'; import 'package:eatvio_app/core/widgets/custom_popup.dart'; @@ -64,7 +65,7 @@ class _BlogPostWidgetState extends State { authorName: state.feedBlogPostList[widget.index].author.authorName, authorProfileIcon: state.feedBlogPostList[widget.index].author.authorProfileIcon.contains('https') ? state.feedBlogPostList[widget.index].author.authorProfileIcon - : baseUrl + state.feedBlogPostList[widget.index].author.authorProfileIcon, + : sl().baseUrl + state.feedBlogPostList[widget.index].author.authorProfileIcon, authorIsMe: state.feedBlogPostList[widget.index].author.authorIsMe, ), ); @@ -75,7 +76,7 @@ class _BlogPostWidgetState extends State { foregroundImage: NetworkImage( state.feedBlogPostList[widget.index].author.authorProfileIcon.contains('https') ? state.feedBlogPostList[widget.index].author.authorProfileIcon - : baseUrl + state.feedBlogPostList[widget.index].author.authorProfileIcon, + : sl().baseUrl + state.feedBlogPostList[widget.index].author.authorProfileIcon, ), backgroundImage: const AssetImage('assets/images/no_avatar.png'), ), @@ -157,15 +158,15 @@ class _BlogPostWidgetState extends State { onTap: () { context.pushTransparentRoute( rootNavigator: true, - PhotoViewScreen(imageUrls: ['$baseUrl/storage/${state.feedBlogPostList[widget.index].image}'], index: 0), + PhotoViewScreen(imageUrls: ['${sl().baseUrl}/storage/${state.feedBlogPostList[widget.index].image}'], index: 0), ); }, child: Hero( - tag: '$baseUrl/storage/${state.feedBlogPostList[widget.index].image}', + tag: '${sl().baseUrl}/storage/${state.feedBlogPostList[widget.index].image}', child: CachedNetworkImage( fit: BoxFit.fitWidth, width: MediaQuery.of(context).size.width, - imageUrl: '$baseUrl/storage/${state.feedBlogPostList[widget.index].image}', + imageUrl: '${sl().baseUrl}/storage/${state.feedBlogPostList[widget.index].image}', errorWidget: (context, str, object) => const SizedBox.shrink(), ), ), @@ -219,7 +220,7 @@ 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('${sl().baseUrl}/ru/blog_post/${state.feedBlogPostList[widget.index].id}'), sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, ), ); diff --git a/lib/features/feed/presentation/widgets/comments_widget.dart b/lib/features/feed/presentation/widgets/comments_widget.dart index 3617fe33..b313ecaa 100644 --- a/lib/features/feed/presentation/widgets/comments_widget.dart +++ b/lib/features/feed/presentation/widgets/comments_widget.dart @@ -1,5 +1,5 @@ +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/comments/presentation/comments_actions_bottom_sheet.dart'; -import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/enums/complained_content.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/utils/date_time_utils.dart'; @@ -99,13 +99,13 @@ class _CommentsWidgetState extends State { extra: Author( authorSlug: comment.authorSlug, authorName: comment.authorName, - authorProfileIcon: comment.authorProfileIcon.contains('https') ? comment.authorProfileIcon : baseUrl + comment.authorProfileIcon, + authorProfileIcon: comment.authorProfileIcon.contains('https') ? comment.authorProfileIcon : sl().baseUrl + comment.authorProfileIcon, authorIsMe: isMe, ), ); }, child: CircleAvatar( - foregroundImage: NetworkImage(comment.authorProfileIcon.contains('https') ? comment.authorProfileIcon : baseUrl + comment.authorProfileIcon), + foregroundImage: NetworkImage(comment.authorProfileIcon.contains('https') ? comment.authorProfileIcon : sl().baseUrl + comment.authorProfileIcon), backgroundImage: const AssetImage('assets/images/no_avatar.png'), radius: radius, ), diff --git a/lib/features/feed/presentation/widgets/consultant_widget.dart b/lib/features/feed/presentation/widgets/consultant_widget.dart index 3b4a4b80..57d34b07 100644 --- a/lib/features/feed/presentation/widgets/consultant_widget.dart +++ b/lib/features/feed/presentation/widgets/consultant_widget.dart @@ -1,5 +1,5 @@ import 'package:cached_network_image/cached_network_image.dart'; -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; @@ -14,10 +14,7 @@ import 'package:go_router/go_router.dart'; class ConsultantWidget extends StatelessWidget { final FeedConsultantModel consultant; - const ConsultantWidget({ - super.key, - required this.consultant, - }); + const ConsultantWidget({super.key, required this.consultant}); @override Widget build(BuildContext context) { @@ -36,10 +33,7 @@ class ConsultantWidget extends StatelessWidget { borderRadius: BorderRadius.circular(20), child: Ink( padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - color: const Color(0xFFF6F6F6), - ), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: const Color(0xFFF6F6F6)), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -51,46 +45,32 @@ class ConsultantWidget extends StatelessWidget { children: [ Ink( padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(8), - ), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(8)), child: Text( '${consultant.ratingPlace}', - style: TextStyle( - fontSize: 20, - color: appTheme.blueA400, - fontWeight: FontWeight.w600, - ), + style: TextStyle(fontSize: 20, color: appTheme.blueA400, fontWeight: FontWeight.w600), ), ), const SizedBox(width: 8), - CircleAvatar( - foregroundImage: NetworkImage(baseUrl + consultant.photo), - backgroundImage: const AssetImage('assets/images/no_avatar.png'), - ), + if (consultant.photo.contains('https')) + CircleAvatar(foregroundImage: NetworkImage(consultant.photo), backgroundImage: const AssetImage('assets/images/no_avatar.png')) + else + CircleAvatar( + foregroundImage: NetworkImage(sl().baseUrl + consultant.photo), + backgroundImage: const AssetImage('assets/images/no_avatar.png'), + ), const SizedBox(width: 8), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( consultant.name, - style: const TextStyle( - height: 1, - fontWeight: FontWeight.w500, - fontSize: 15, - color: Color(0xFF323F4B), - ), + style: const TextStyle(height: 1, fontWeight: FontWeight.w500, fontSize: 15, color: Color(0xFF323F4B)), ), const SizedBox(height: 4), Text( 'Рейтинг: ${consultant.totalScores}', - style: TextStyle( - color: appTheme.blueA400, - height: 1, - fontWeight: FontWeight.w500, - fontSize: 12, - ), + style: TextStyle(color: appTheme.blueA400, height: 1, fontWeight: FontWeight.w500, fontSize: 12), ), ], ), @@ -98,21 +78,13 @@ class ConsultantWidget extends StatelessWidget { ), Ink( padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(4), - color: Colors.white, - ), - child: SvgPicture.asset( - 'assets/icons/go_to_arrow.svg', - ), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(4), color: Colors.white), + child: SvgPicture.asset('assets/icons/go_to_arrow.svg'), ), ], ), const SizedBox(height: 8), - Text( - consultant.description, - maxLines: 2, - ), + Text(consultant.description, maxLines: 2), if (consultant.badges.isNotEmpty) const SizedBox(height: 8), _badges(consultant.badges, context), ], @@ -128,16 +100,8 @@ class ConsultantWidget extends StatelessWidget { children: [ for (int i = 0; i < badges.length; i++) ...[ badges[i].image.contains('svg') - ? SvgPicture.network( - '$baseUrl/storage/${badges[i].image}', - width: 28, - height: 28, - ) - : CachedNetworkImage( - imageUrl: '$baseUrl/storage/${badges[i].image}', - width: 28, - height: 28, - ), + ? SvgPicture.network('${sl().baseUrl}/storage/${badges[i].image}', width: 28, height: 28) + : CachedNetworkImage(imageUrl: '${sl().baseUrl}/storage/${badges[i].image}', width: 28, height: 28), i < badges.length - 1 ? const SizedBox(width: 8) : const SizedBox.shrink(), ], ], diff --git a/lib/features/feed/presentation/widgets/diets_widget.dart b/lib/features/feed/presentation/widgets/diets_widget.dart index d91cdd74..1f39ff72 100644 --- a/lib/features/feed/presentation/widgets/diets_widget.dart +++ b/lib/features/feed/presentation/widgets/diets_widget.dart @@ -1,7 +1,8 @@ import 'package:cached_network_image/cached_network_image.dart'; -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/enums/complained_content.dart'; import 'package:eatvio_app/core/enums/product_type.dart'; +import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/widgets/bookmark_button.dart'; import 'package:eatvio_app/core/widgets/custom_popup.dart'; import 'package:eatvio_app/core/widgets/info_row.dart'; @@ -56,7 +57,7 @@ class _DietsWidgetState extends State { ClipRRect( borderRadius: const BorderRadius.vertical(top: Radius.circular(12)), child: CachedNetworkImage( - imageUrl: '$baseUrl/storage/${widget.diet.image}', + imageUrl: '${sl().baseUrl}/storage/${widget.diet.image}', fit: BoxFit.fitWidth, width: MediaQuery.of(context).size.width, ), @@ -189,7 +190,7 @@ class _DietsWidgetState extends State { final box = context.findRenderObject() as RenderBox?; final result = await SharePlus.instance.share( ShareParams( - uri: Uri.parse('$baseUrl/ru/diet/${widget.diet.id}'), + uri: Uri.parse('${sl().baseUrl}/ru/diet/${widget.diet.id}'), sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, ), ); diff --git a/lib/features/feed/presentation/widgets/plan_widget.dart b/lib/features/feed/presentation/widgets/plan_widget.dart index 588d61d0..ae43d830 100644 --- a/lib/features/feed/presentation/widgets/plan_widget.dart +++ b/lib/features/feed/presentation/widgets/plan_widget.dart @@ -1,7 +1,8 @@ import 'package:cached_network_image/cached_network_image.dart'; -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/enums/complained_content.dart'; import 'package:eatvio_app/core/enums/product_type.dart'; +import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/widgets/bookmark_button.dart'; import 'package:eatvio_app/core/widgets/custom_popup.dart'; import 'package:eatvio_app/core/widgets/info_row.dart'; @@ -61,7 +62,7 @@ class _PlanWidgetState extends State { ClipRRect( borderRadius: const BorderRadius.vertical(top: Radius.circular(12)), child: CachedNetworkImage( - imageUrl: '$baseUrl/storage/${widget.plan.image}', + imageUrl: '${sl().baseUrl}/storage/${widget.plan.image}', fit: BoxFit.cover, width: MediaQuery.of(context).size.width, height: 426, @@ -187,7 +188,7 @@ class _PlanWidgetState extends State { decoration: BoxDecoration( shape: BoxShape.circle, image: DecorationImage( - image: NetworkImage(baseUrl + widget.plan.author.authorProfileIcon), + image: NetworkImage(sl().baseUrl + widget.plan.author.authorProfileIcon), fit: BoxFit.cover, ), ), @@ -263,7 +264,7 @@ class _PlanWidgetState extends State { final box = context.findRenderObject() as RenderBox?; final result = await SharePlus.instance.share( ShareParams( - uri: Uri.parse('$baseUrl/ru/diet/${widget.plan.id}'), + uri: Uri.parse('${sl().baseUrl}/ru/diet/${widget.plan.id}'), sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, ), ); diff --git a/lib/features/feed/presentation/widgets/recipes_widget.dart b/lib/features/feed/presentation/widgets/recipes_widget.dart index c0dbeb39..5fb415b1 100644 --- a/lib/features/feed/presentation/widgets/recipes_widget.dart +++ b/lib/features/feed/presentation/widgets/recipes_widget.dart @@ -1,8 +1,9 @@ // ignore_for_file: dead_code import 'package:cached_network_image/cached_network_image.dart'; -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/enums/complained_content.dart'; +import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/utils/localization_utils.dart'; import 'package:eatvio_app/core/widgets/bookmark_button.dart'; @@ -75,7 +76,7 @@ class _RecipesWidgetState extends State { ClipRRect( borderRadius: const BorderRadius.vertical(top: Radius.circular(12)), child: CachedNetworkImage( - imageUrl: '$baseUrl/storage/${widget.recipe.image}', + imageUrl: '${sl().baseUrl}/storage/${widget.recipe.image}', fit: BoxFit.fitWidth, width: MediaQuery.of(context).size.width, ), @@ -231,7 +232,7 @@ class _RecipesWidgetState extends State { final box = context.findRenderObject() as RenderBox?; SharePlus.instance.share( ShareParams( - uri: Uri.parse('$baseUrl/ru/recipe/${widget.recipe.id}'), + uri: Uri.parse('${sl().baseUrl}/ru/recipe/${widget.recipe.id}'), sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, ), ); diff --git a/lib/features/glossary/presentation/pages/glossary_screen.dart b/lib/features/glossary/presentation/pages/glossary_screen.dart index 7205b3d3..22050f00 100644 --- a/lib/features/glossary/presentation/pages/glossary_screen.dart +++ b/lib/features/glossary/presentation/pages/glossary_screen.dart @@ -1,4 +1,5 @@ -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; +import 'package:eatvio_app/core/locator_service.dart'; import 'package:flutter/material.dart'; import 'package:webview_flutter/webview_flutter.dart'; @@ -18,7 +19,7 @@ class _GlossaryScreenState extends State { _controller = WebViewController() ..setJavaScriptMode(JavaScriptMode.unrestricted) - ..loadRequest(Uri.parse('$baseUrl/product_nutritional_value')); + ..loadRequest(Uri.parse('${sl().baseUrl}/product_nutritional_value')); } @override diff --git a/lib/features/menu/presentation/pages/menu_screen.dart b/lib/features/menu/presentation/pages/menu_screen.dart index 123e5faf..7d237245 100644 --- a/lib/features/menu/presentation/pages/menu_screen.dart +++ b/lib/features/menu/presentation/pages/menu_screen.dart @@ -1,4 +1,5 @@ import 'package:dio/dio.dart'; +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/token_storage.dart'; @@ -117,7 +118,7 @@ class MenuScreen extends StatelessWidget { CupertinoDialogAction( onPressed: () async { try { - await sl().post('$baseUrl/api/profile/logout'); + await sl().post('${sl().baseUrl}/api/profile/logout'); } finally { await TokenStorage.removeTokenInStorage(); if (context.mounted) { diff --git a/lib/features/menu/presentation/widgets/blocked_user_widget.dart b/lib/features/menu/presentation/widgets/blocked_user_widget.dart index 494df21c..a107a4df 100644 --- a/lib/features/menu/presentation/widgets/blocked_user_widget.dart +++ b/lib/features/menu/presentation/widgets/blocked_user_widget.dart @@ -1,4 +1,5 @@ -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; +import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_cubit.dart'; import 'package:eatvio_app/features/profile/models/profile_model.dart'; @@ -33,7 +34,7 @@ class _BlockedUserWidgetState extends State { child: Row( children: [ CircleAvatar( - foregroundImage: NetworkImage(baseUrl + widget.profile.profilePhotoUrl), + foregroundImage: NetworkImage(sl().baseUrl + widget.profile.profilePhotoUrl), backgroundImage: const AssetImage('assets/images/no_avatar.png'), radius: 28, ), diff --git a/lib/features/my_diets/presentation/widgets/my_diet_widget.dart b/lib/features/my_diets/presentation/widgets/my_diet_widget.dart index 965bd088..d6f41d7f 100644 --- a/lib/features/my_diets/presentation/widgets/my_diet_widget.dart +++ b/lib/features/my_diets/presentation/widgets/my_diet_widget.dart @@ -1,6 +1,7 @@ import 'package:cached_network_image/cached_network_image.dart'; +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/app_router.dart'; -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/enums/product_type.dart'; @@ -56,7 +57,7 @@ class _MyDietWidgetState extends State { child: ClipRRect( borderRadius: const BorderRadius.vertical(top: Radius.circular(12)), child: CachedNetworkImage( - imageUrl: '$baseUrl/storage/${widget.diet.image}', + imageUrl: '${sl().baseUrl}/storage/${widget.diet.image}', fit: BoxFit.fitWidth, width: MediaQuery.of(context).size.width, ), @@ -153,7 +154,7 @@ class _MyDietWidgetState extends State { final box = context.findRenderObject() as RenderBox?; await SharePlus.instance.share( ShareParams( - uri: Uri.parse('$baseUrl/ru/diet/${widget.diet.id}'), + uri: Uri.parse('${sl().baseUrl}/ru/diet/${widget.diet.id}'), sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, ), ); 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 7ecdc653..5d057be5 100644 --- a/lib/features/my_plans/presentation/widgets/my_plan_widget.dart +++ b/lib/features/my_plans/presentation/widgets/my_plan_widget.dart @@ -1,5 +1,6 @@ import 'package:cached_network_image/cached_network_image.dart'; -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; +import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/enums/product_type.dart'; @@ -42,7 +43,7 @@ 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: '${sl().baseUrl}/storage/${widget.plan.image}', fit: BoxFit.fitWidth, width: MediaQuery.of(context).size.width), ), ), Positioned( @@ -157,7 +158,7 @@ class _MyPlanWidgetState extends State { final box = context.findRenderObject() as RenderBox?; await SharePlus.instance.share( ShareParams( - uri: Uri.parse('$baseUrl/ru/plan/${widget.plan.id}'), + uri: Uri.parse('${sl().baseUrl}/ru/plan/${widget.plan.id}'), sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, ), ); diff --git a/lib/features/order_info/presentation/widgets/plan_info.dart b/lib/features/order_info/presentation/widgets/plan_info.dart index 6c5e387d..d8f91880 100644 --- a/lib/features/order_info/presentation/widgets/plan_info.dart +++ b/lib/features/order_info/presentation/widgets/plan_info.dart @@ -1,5 +1,6 @@ import 'package:cached_network_image/cached_network_image.dart'; -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; +import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/widgets/main_background_bouble.dart'; @@ -31,7 +32,7 @@ class PlanInfo extends StatelessWidget { ClipRRect( borderRadius: BorderRadius.circular(8), child: CachedNetworkImage( - imageUrl: '$baseUrl/storage/$imagePath', + imageUrl: '${sl().baseUrl}/storage/$imagePath', height: 220, width: MediaQuery.sizeOf(context).width, fit: BoxFit.fill, diff --git a/lib/features/plans/presentation/pages/about_plan_screen.dart b/lib/features/plans/presentation/pages/about_plan_screen.dart index b5905664..64a437c1 100644 --- a/lib/features/plans/presentation/pages/about_plan_screen.dart +++ b/lib/features/plans/presentation/pages/about_plan_screen.dart @@ -1,7 +1,7 @@ import 'dart:io'; import 'package:cached_network_image/cached_network_image.dart'; -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/enums/complained_content.dart'; import 'package:eatvio_app/core/enums/product_type.dart'; import 'package:eatvio_app/core/utils/localization_utils.dart'; @@ -401,7 +401,7 @@ class _AboutPlanScreenState extends State { Stack( children: [ CachedNetworkImage( - imageUrl: '$baseUrl/storage/${state.planModel?.image}', + imageUrl: '${sl().baseUrl}/storage/${state.planModel?.image}', height: 300, width: MediaQuery.sizeOf(context).width, fit: BoxFit.fill, diff --git a/lib/features/profile/presentation/pages/profile_screen.dart b/lib/features/profile/presentation/pages/profile_screen.dart index 11ae9b7a..5c25dfe5 100644 --- a/lib/features/profile/presentation/pages/profile_screen.dart +++ b/lib/features/profile/presentation/pages/profile_screen.dart @@ -1,6 +1,7 @@ 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_config.dart'; import 'package:eatvio_app/core/app_router.dart'; import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/enums/complained_content.dart'; @@ -655,7 +656,7 @@ class _ProfileScreenState extends State { borderRadius: BorderRadius.circular(4), boxShadow: [BoxShadow(color: Colors.black.withAlpha((255 * 0.2).toInt()), blurRadius: 32)], ), - child: SvgPicture.network('$baseUrl/storage/${state.profile.badges[i].image}', width: 40, height: 40), + child: SvgPicture.network('${sl().baseUrl}/storage/${state.profile.badges[i].image}', width: 40, height: 40), ) : Tooltip( message: state.profile.badges[i].description, @@ -667,7 +668,7 @@ class _ProfileScreenState extends State { borderRadius: BorderRadius.circular(4), boxShadow: [BoxShadow(color: Colors.black.withAlpha((255 * 0.2).toInt()), blurRadius: 32)], ), - child: CachedNetworkImage(imageUrl: '$baseUrl/storage/${state.profile.badges[i].image}', width: 40, height: 40), + child: CachedNetworkImage(imageUrl: '${sl().baseUrl}/storage/${state.profile.badges[i].image}', width: 40, height: 40), ), i < state.profile.badges.length - 1 ? const SizedBox(width: 8) : const SizedBox.shrink(), ], @@ -803,7 +804,7 @@ class _ProfileScreenState extends State { icon: 'assets/icons/eye.svg', text: 'Посмотреть', onTap: () async { - context.pushTransparentRoute(rootNavigator: true, PhotoViewScreen(imageUrls: [baseUrl + state.profile.profilePhotoUrl], index: 0)); + context.pushTransparentRoute(rootNavigator: true, PhotoViewScreen(imageUrls: [sl().baseUrl + state.profile.profilePhotoUrl], index: 0)); }, ), if (widget.author == null || widget.author!.authorIsMe) ...[ @@ -885,7 +886,7 @@ class _ProfileScreenState extends State { child: CircleAvatar( radius: 32, foregroundImage: NetworkImage( - state.profile.profilePhotoUrl.contains('https') ? state.profile.profilePhotoUrl : baseUrl + state.profile.profilePhotoUrl, + state.profile.profilePhotoUrl.contains('https') ? state.profile.profilePhotoUrl : sl().baseUrl + state.profile.profilePhotoUrl, ), backgroundImage: const AssetImage('assets/images/no_avatar.png'), ), @@ -960,7 +961,7 @@ class _ProfileScreenState extends State { onTap: () { final box = context.findRenderObject() as RenderBox?; SharePlus.instance.share( - ShareParams(uri: Uri.parse('$baseUrl/ru/profile/${state.profile.slug}'), sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size), + ShareParams(uri: Uri.parse('${sl().baseUrl}/ru/profile/${state.profile.slug}'), sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size), ); }, ), diff --git a/lib/features/profile/presentation/widgets/profile_blog_post_widget.dart b/lib/features/profile/presentation/widgets/profile_blog_post_widget.dart index 0f0408d8..7c55c420 100644 --- a/lib/features/profile/presentation/widgets/profile_blog_post_widget.dart +++ b/lib/features/profile/presentation/widgets/profile_blog_post_widget.dart @@ -1,7 +1,8 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:dismissible_page/dismissible_page.dart'; -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/enums/complained_content.dart'; +import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/utils/date_time_utils.dart'; import 'package:eatvio_app/core/widgets/custom_popup.dart'; import 'package:eatvio_app/core/widgets/feed_icon_button.dart'; @@ -69,7 +70,7 @@ class _ProfileBlogPostWidgetState extends State { foregroundImage: NetworkImage( state.blogPosts[widget.index].author.authorProfileIcon.contains('https') ? state.blogPosts[widget.index].author.authorProfileIcon - : baseUrl + state.blogPosts[widget.index].author.authorProfileIcon, + : sl().baseUrl + state.blogPosts[widget.index].author.authorProfileIcon, ), backgroundImage: const AssetImage('assets/images/no_avatar.png'), ), @@ -157,17 +158,17 @@ class _ProfileBlogPostWidgetState extends State { context.pushTransparentRoute( rootNavigator: true, PhotoViewScreen( - imageUrls: ['$baseUrl/storage/${state.blogPosts[widget.index].image}'], + imageUrls: ['${sl().baseUrl}/storage/${state.blogPosts[widget.index].image}'], index: 0, ), ); }, child: Hero( - tag: '$baseUrl/storage/${state.blogPosts[widget.index].image}', + tag: '${sl().baseUrl}/storage/${state.blogPosts[widget.index].image}', child: CachedNetworkImage( fit: BoxFit.fitWidth, width: MediaQuery.of(context).size.width, - imageUrl: '$baseUrl/storage/${state.blogPosts[widget.index].image}', + imageUrl: '${sl().baseUrl}/storage/${state.blogPosts[widget.index].image}', errorWidget: (context, str, object) => const SizedBox.shrink(), ), ), @@ -226,7 +227,7 @@ class _ProfileBlogPostWidgetState extends State { final box = context.findRenderObject() as RenderBox?; SharePlus.instance.share( ShareParams( - uri: Uri.parse('$baseUrl/ru/blog_post/${state.blogPosts[widget.index].id}'), + uri: Uri.parse('${sl().baseUrl}/ru/blog_post/${state.blogPosts[widget.index].id}'), sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, ), ); diff --git a/lib/features/profile/presentation/widgets/sub_widget.dart b/lib/features/profile/presentation/widgets/sub_widget.dart index 27365fd2..d5d3a61a 100644 --- a/lib/features/profile/presentation/widgets/sub_widget.dart +++ b/lib/features/profile/presentation/widgets/sub_widget.dart @@ -1,4 +1,5 @@ -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; +import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; import 'package:eatvio_app/features/profile/models/sub_model.dart'; import 'package:eatvio_app/features/profile/presentation/widgets/sub_action_button_widget.dart'; @@ -36,7 +37,7 @@ class _SubWidgetState extends State { child: Row( children: [ CircleAvatar( - foregroundImage: NetworkImage(baseUrl + widget.subModel.profilePhotoUrl), + foregroundImage: NetworkImage(sl().baseUrl + widget.subModel.profilePhotoUrl), backgroundImage: const AssetImage('assets/images/no_avatar.png'), radius: 28, ), diff --git a/lib/features/recipe/presentation/pages/about_recipe_screen.dart b/lib/features/recipe/presentation/pages/about_recipe_screen.dart index f23ac880..efc19a12 100644 --- a/lib/features/recipe/presentation/pages/about_recipe_screen.dart +++ b/lib/features/recipe/presentation/pages/about_recipe_screen.dart @@ -2,7 +2,7 @@ import 'dart:developer'; import 'dart:io'; import 'package:cached_network_image/cached_network_image.dart'; -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/enums/complained_content.dart'; import 'package:eatvio_app/core/enums/product_type.dart'; import 'package:eatvio_app/core/utils/localization_utils.dart'; @@ -247,7 +247,7 @@ class _AboutRecipeScreenState extends State { Stack( children: [ CachedNetworkImage( - imageUrl: '$baseUrl/storage/${recipeState.recipe.image}', + imageUrl: '${sl().baseUrl}/storage/${recipeState.recipe.image}', height: 300, width: MediaQuery.sizeOf(context).width, fit: BoxFit.fill, diff --git a/lib/features/recipe/presentation/widgets/my_recipe_widget.dart b/lib/features/recipe/presentation/widgets/my_recipe_widget.dart index 2610ca13..d791135a 100644 --- a/lib/features/recipe/presentation/widgets/my_recipe_widget.dart +++ b/lib/features/recipe/presentation/widgets/my_recipe_widget.dart @@ -1,5 +1,5 @@ import 'package:cached_network_image/cached_network_image.dart'; -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; @@ -71,7 +71,7 @@ class _MyRecipeWidgetState extends State { child: ClipRRect( borderRadius: const BorderRadius.vertical(top: Radius.circular(12)), child: CachedNetworkImage( - imageUrl: '$baseUrl/storage/${widget.recipe.image}', + imageUrl: '${sl().baseUrl}/storage/${widget.recipe.image}', fit: BoxFit.fitWidth, width: MediaQuery.of(context).size.width, ), @@ -162,7 +162,7 @@ class _MyRecipeWidgetState extends State { final box = context.findRenderObject() as RenderBox?; await SharePlus.instance.share( ShareParams( - uri: Uri.parse('$baseUrl/ru/recipe/${widget.recipe.slug}'), + uri: Uri.parse('${sl().baseUrl}/ru/recipe/${widget.recipe.slug}'), sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, ), ); diff --git a/lib/features/recipe/presentation/widgets/steps_widget.dart b/lib/features/recipe/presentation/widgets/steps_widget.dart index f4281c81..dbd45602 100644 --- a/lib/features/recipe/presentation/widgets/steps_widget.dart +++ b/lib/features/recipe/presentation/widgets/steps_widget.dart @@ -1,4 +1,5 @@ -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; +import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/widgets/main_background_bouble.dart'; @@ -44,7 +45,7 @@ class StepsWidget extends StatelessWidget { child: ClipRRect( borderRadius: BorderRadius.circular(16), child: Image.network( - '$baseUrl/storage/${recipe.steps![index].image}', + '${sl().baseUrl}/storage/${recipe.steps![index].image}', fit: BoxFit.cover, ), ), diff --git a/lib/features/registration/presentation/pages/verify_screen.dart b/lib/features/registration/presentation/pages/verify_screen.dart index 8d5df9df..867af06f 100644 --- a/lib/features/registration/presentation/pages/verify_screen.dart +++ b/lib/features/registration/presentation/pages/verify_screen.dart @@ -1,4 +1,5 @@ -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; +import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/widgets/loader.dart'; import 'package:eatvio_app/features/registration/services/registration_analytics_service.dart'; @@ -89,7 +90,7 @@ class _VerifyScreenState extends State { }, ), ) - ..loadRequest(Uri.parse('$baseUrl/login_with_pass_key?id=${widget.passKey}')); + ..loadRequest(Uri.parse('${sl().baseUrl}/login_with_pass_key?id=${widget.passKey}')); } @override diff --git a/lib/features/registration/presentation/pages/webview_registration_screen.dart b/lib/features/registration/presentation/pages/webview_registration_screen.dart index 05c0ffc1..6cc777d2 100644 --- a/lib/features/registration/presentation/pages/webview_registration_screen.dart +++ b/lib/features/registration/presentation/pages/webview_registration_screen.dart @@ -1,7 +1,7 @@ import 'dart:convert'; import 'dart:developer'; -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart'; import 'package:eatvio_app/features/registration/services/registration_analytics_service.dart'; @@ -54,7 +54,7 @@ class _WebViewRegistrationScreenState extends State { }, ), ) - ..loadRequest(Uri.parse('$baseUrl/registration/step1')); + ..loadRequest(Uri.parse('${sl().baseUrl}/registration/step1')); _injectJavaScript(); } diff --git a/lib/features/support/presentation/pages/support_screen.dart b/lib/features/support/presentation/pages/support_screen.dart index 6aa44a42..a01a88af 100644 --- a/lib/features/support/presentation/pages/support_screen.dart +++ b/lib/features/support/presentation/pages/support_screen.dart @@ -1,4 +1,6 @@ +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/utils/toasts.dart'; import 'package:eatvio_app/core/widgets/custom_divider.dart'; import 'package:eatvio_app/generated/l10n.dart'; @@ -71,7 +73,7 @@ class SupportScreen extends StatelessWidget { const SizedBox(height: 12), if (!hidePlans) ...[ InkWell( - onTap: () async => await launchUrl(Uri.parse('$baseUrl/docs/public_offer.pdf')), + onTap: () async => await launchUrl(Uri.parse('${sl().baseUrl}/docs/public_offer.pdf')), borderRadius: BorderRadius.circular(16), child: const Padding( padding: EdgeInsets.symmetric(horizontal: 16, vertical: 4), @@ -81,7 +83,7 @@ class SupportScreen extends StatelessWidget { const SizedBox(height: 8), ], InkWell( - onTap: () async => await launchUrl(Uri.parse('$baseUrl/docs/privacy.pdf')), + onTap: () async => await launchUrl(Uri.parse('${sl().baseUrl}/docs/privacy.pdf')), borderRadius: BorderRadius.circular(16), child: const Padding( padding: EdgeInsets.symmetric(horizontal: 16, vertical: 4), @@ -90,7 +92,7 @@ class SupportScreen extends StatelessWidget { ), const SizedBox(height: 8), InkWell( - onTap: () async => await launchUrl(Uri.parse('$baseUrl/company-details')), + onTap: () async => await launchUrl(Uri.parse('${sl().baseUrl}/company-details')), borderRadius: BorderRadius.circular(16), child: const Padding( padding: EdgeInsets.symmetric(horizontal: 16, vertical: 4), diff --git a/lib/main.dart b/lib/main.dart index fa5ff699..af6db638 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,7 @@ import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/app_initializer.dart'; import 'package:eatvio_app/core/app_router.dart'; +import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -8,7 +9,7 @@ import 'package:flutter_portal/flutter_portal.dart'; import 'package:keyboard_dismisser/keyboard_dismisser.dart'; Future main() async { - await AppInitializer.initialize(); + await AppInitializer.initialize(buildType: AppBuildType.prod); await AppInitializer.runAppWithSentry(() => runApp(const MyApp())); } @@ -18,15 +19,15 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MultiBlocProvider( - providers: AppConfig.globalBlocProviders, + providers: sl().globalBlocProviders, child: Portal( child: KeyboardDismisser( child: MaterialApp.router( theme: ThemeHelper().themeData(), - routerConfig: AppRouter.appRouter, + routerConfig: AppRouter.appRouter, debugShowCheckedModeBanner: false, - localizationsDelegates: AppConfig.localizationDelegates, - supportedLocales: AppConfig.supportedLocales, + localizationsDelegates: sl().localizationDelegates, + supportedLocales: sl().supportedLocales, ), ), ), From 183fa012d2f3f68b8cb155471a9b510d2b9f42b4 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Thu, 9 Oct 2025 17:24:36 +0300 Subject: [PATCH 59/78] feat(auth): Issue-691 --- lib/core/app_router.dart | 5 + lib/core/di/cubit_registrations.dart | 3 +- lib/core/routing/auth_routes.dart | 10 +- lib/features/auth/models/oauth_model.dart | 24 + .../bloc/oauth_cubit/oauth_cubit.dart | 44 ++ .../bloc/oauth_cubit/oauth_state.dart | 14 + .../auth/presentation/pages/auth_screen.dart | 33 +- .../pages/register_via_social_screen.dart | 652 +++++++++++++----- .../auth/repositories/auth_repository.dart | 52 +- lib/features/auth/sdk/social_auth_facade.dart | 12 +- lib/features/auth/services/auth_service.dart | 6 + 11 files changed, 637 insertions(+), 218 deletions(-) create mode 100644 lib/features/auth/models/oauth_model.dart create mode 100644 lib/features/auth/presentation/bloc/oauth_cubit/oauth_cubit.dart create mode 100644 lib/features/auth/presentation/bloc/oauth_cubit/oauth_state.dart diff --git a/lib/core/app_router.dart b/lib/core/app_router.dart index f3715464..26355055 100644 --- a/lib/core/app_router.dart +++ b/lib/core/app_router.dart @@ -4,6 +4,7 @@ import 'package:eatvio_app/core/routing/main_navigation_routes.dart'; import 'package:eatvio_app/core/routing/other_routes.dart'; import 'package:eatvio_app/core/routing/route_constants.dart'; import 'package:eatvio_app/core/routing/settings_routes.dart'; +import 'package:eatvio_app/features/auth/entities/auth_result_entity.dart'; import 'package:flutter/material.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:go_router/go_router.dart'; @@ -50,4 +51,8 @@ class AppRouter { return null; } + + static void navigateToRegisterViaSocial({required AuthResultEntity authResult, required String provider}) { + appRouter.goNamed('registerViaSocialScreen', extra: {'authResult': authResult, 'provider': provider}); + } } diff --git a/lib/core/di/cubit_registrations.dart b/lib/core/di/cubit_registrations.dart index ec2de9ce..bd9014e6 100644 --- a/lib/core/di/cubit_registrations.dart +++ b/lib/core/di/cubit_registrations.dart @@ -1,5 +1,6 @@ import 'package:eatvio_app/core/views/cubit/views_cubit.dart'; import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart'; +import 'package:eatvio_app/features/auth/presentation/bloc/oauth_cubit/oauth_cubit.dart'; import 'package:eatvio_app/features/auth/presentation/bloc/reset_password_cubit/reset_password_cubit.dart'; import 'package:eatvio_app/features/blog_post/presentation/cubit/blog_post_cubit.dart'; import 'package:eatvio_app/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_cubit.dart'; @@ -73,7 +74,7 @@ class CubitRegistrations { sl.registerFactory(() => RegistrationCubit(registrationRepository: sl())); sl.registerFactory(() => ResetPasswordCubit(resetPasswordRepository: sl())); sl.registerFactory(() => VerifyEmailCubit(profileRepository: sl())); - + sl.registerFactory(() => OAuthCubit(authRepository: sl())); // Feed related cubits sl.registerFactory(() => FeedCubit(feedRepository: sl())); sl.registerFactory(() => RecipesCubit(feedRepository: sl(), recipeRepository: sl(), aboutRecipeRepository: sl())); diff --git a/lib/core/routing/auth_routes.dart b/lib/core/routing/auth_routes.dart index 67ecbca6..b94cc2c8 100644 --- a/lib/core/routing/auth_routes.dart +++ b/lib/core/routing/auth_routes.dart @@ -1,4 +1,5 @@ import 'package:eatvio_app/core/routing/route_constants.dart'; +import 'package:eatvio_app/features/auth/entities/auth_result_entity.dart'; import 'package:eatvio_app/features/auth/presentation/pages/auth_screen.dart'; import 'package:eatvio_app/features/auth/presentation/pages/forget_password_screen.dart'; import 'package:eatvio_app/features/auth/presentation/pages/register_via_social_screen.dart'; @@ -33,7 +34,14 @@ class AuthRoutes { builder: (context, state) => VerifyScreen(passKey: state.extra as String), ), GoRoute(path: RouteConstants.verifyEmailScreen, name: 'verifyEmailScreen', builder: (context, state) => const VerifyEmailScreen()), - GoRoute(path: RouteConstants.registerViaSocialScreen, name: 'registerViaSocialScreen', builder: (context, state) => const RegisterViaSocialScreen()), + GoRoute( + path: RouteConstants.registerViaSocialScreen, + name: 'registerViaSocialScreen', + builder: (context, state) { + final extra = state.extra as Map?; + return RegisterViaSocialScreen(authResult: extra?['authResult'] as AuthResultEntity?, provider: extra?['provider'] as String?); + }, + ), ]; } } diff --git a/lib/features/auth/models/oauth_model.dart b/lib/features/auth/models/oauth_model.dart new file mode 100644 index 00000000..aff79b5f --- /dev/null +++ b/lib/features/auth/models/oauth_model.dart @@ -0,0 +1,24 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'oauth_model.freezed.dart'; +part 'oauth_model.g.dart'; + +@freezed +abstract class OAuthModel with _$OAuthModel { + const factory OAuthModel({ + required String provider, + required String id, + @JsonKey(name: 'access_token') required String accessToken, + @JsonKey(name: 'refresh_token') required String refreshToken, + @JsonKey(name: 'first_name') required String firstName, + @JsonKey(name: 'last_name') required String lastName, + required String email, + required String phone, + @JsonKey(name: 'date_of_birth') required String dateOfBirth, + required String gender, + required int weight, + required int height, + }) = _OAuthModel; + + factory OAuthModel.fromJson(Map json) => _$OAuthModelFromJson(json); +} diff --git a/lib/features/auth/presentation/bloc/oauth_cubit/oauth_cubit.dart b/lib/features/auth/presentation/bloc/oauth_cubit/oauth_cubit.dart new file mode 100644 index 00000000..8d73b195 --- /dev/null +++ b/lib/features/auth/presentation/bloc/oauth_cubit/oauth_cubit.dart @@ -0,0 +1,44 @@ +import 'package:eatvio_app/features/auth/presentation/bloc/oauth_cubit/oauth_state.dart'; +import 'package:eatvio_app/features/auth/repositories/auth_repository.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class OAuthCubit extends Cubit { + final AuthRepository authRepository; + + OAuthCubit({required this.authRepository}) : super(const OAuthState()); + + Future getOAuthToken({ + required String provider, + required String accessToken, + required String id, + required String refreshToken, + required String firstName, + required String lastName, + required String email, + required String phone, + required String dateOfBirth, + required String gender, + required int weight, + required int height, + }) async { + try { + final token = await authRepository.getOAuthToken( + provider: provider, + accessToken: accessToken, + id: id, + refreshToken: refreshToken, + firstName: firstName, + lastName: lastName, + email: email, + phone: phone, + dateOfBirth: dateOfBirth, + gender: gender, + weight: weight, + height: height, + ); + emit(OAuthState.loaded(token: token)); + } catch (e) { + emit(OAuthState.error(errorMessage: e.toString())); + } + } +} diff --git a/lib/features/auth/presentation/bloc/oauth_cubit/oauth_state.dart b/lib/features/auth/presentation/bloc/oauth_cubit/oauth_state.dart new file mode 100644 index 00000000..7da9ab7c --- /dev/null +++ b/lib/features/auth/presentation/bloc/oauth_cubit/oauth_state.dart @@ -0,0 +1,14 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'oauth_state.freezed.dart'; + +@freezed +abstract class OAuthState with _$OAuthState { + const factory OAuthState() = _OAuthState; + + const factory OAuthState.loading() = OAuthStateLoading; + + const factory OAuthState.loaded({required String token}) = OAuthStateLoaded; + + const factory OAuthState.error({required String errorMessage}) = OAuthStateError; +} diff --git a/lib/features/auth/presentation/pages/auth_screen.dart b/lib/features/auth/presentation/pages/auth_screen.dart index 6f49ea2a..13713922 100644 --- a/lib/features/auth/presentation/pages/auth_screen.dart +++ b/lib/features/auth/presentation/pages/auth_screen.dart @@ -391,10 +391,14 @@ class _SocialButton extends StatelessWidget { void _onVkPressed() async { try { - final accessToken = await SocialAuthFacade.signInWithVk(); - if (accessToken == null) { + final context = AppRouter.navigatorKey.currentContext; + if (context == null) return; + + final authResult = await SocialAuthFacade.signInWithVkAndFetchProfile(context: context); + if (authResult == null) { return; } + AppRouter.navigateToRegisterViaSocial(authResult: authResult, provider: 'vk'); } catch (e) { log(e.toString()); } @@ -402,10 +406,14 @@ void _onVkPressed() async { void _onOkPressed() async { try { - final accessToken = await SocialAuthFacade.signInWithOk(); - if (accessToken == null) { + final context = AppRouter.navigatorKey.currentContext; + if (context == null) return; + + final authResult = await SocialAuthFacade.signInWithOkAndFetchProfile(); + if (authResult == null) { return; } + AppRouter.navigateToRegisterViaSocial(authResult: authResult, provider: 'ok'); } catch (e) { log(e.toString()); } @@ -413,18 +421,27 @@ void _onOkPressed() async { void _onMailRuPressed() async { try { - final accessToken = await SocialAuthFacade.signInWithMailRu(); - if (accessToken == null) { + final context = AppRouter.navigatorKey.currentContext; + if (context == null) return; + + final authResult = await SocialAuthFacade.signInWithMailRuAndFetchProfile(); + if (authResult == null) { return; } + AppRouter.navigateToRegisterViaSocial(authResult: authResult, provider: 'mailru'); } catch (e) { log(e.toString()); } } void _onYandexPressed() async { - final accessToken = await SocialAuthFacade.signInWithYandex(); - if (accessToken == null) return; + try { + final authResult = await SocialAuthFacade.signInWithYandex(); + if (authResult == null) return; + AppRouter.navigateToRegisterViaSocial(authResult: authResult, provider: 'yandex'); + } catch (e) { + log(e.toString()); + } } Future _reconnectAfterAuth() async { diff --git a/lib/features/auth/presentation/pages/register_via_social_screen.dart b/lib/features/auth/presentation/pages/register_via_social_screen.dart index 211b9eae..22fa5d18 100644 --- a/lib/features/auth/presentation/pages/register_via_social_screen.dart +++ b/lib/features/auth/presentation/pages/register_via_social_screen.dart @@ -1,13 +1,29 @@ import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/widgets/custom_text_form_field.dart'; +import 'package:eatvio_app/core/di/cubit_registrations.dart'; +import 'package:eatvio_app/features/auth/presentation/bloc/oauth_cubit/oauth_cubit.dart'; +import 'package:eatvio_app/features/auth/presentation/bloc/oauth_cubit/oauth_state.dart'; +import 'package:eatvio_app/features/auth/entities/auth_result_entity.dart'; +import 'package:eatvio_app/core/app_router.dart'; +import 'package:eatvio_app/core/locator_service.dart'; +import 'package:eatvio_app/core/websockets/ws_manager.dart'; +import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; +import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart'; +import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart'; +import 'package:eatvio_app/core/firebase_api.dart'; +import 'package:eatvio_app/core/utils/toasts.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:mask_text_input_formatter/mask_text_input_formatter.dart'; import 'package:url_launcher/url_launcher.dart'; class RegisterViaSocialScreen extends StatefulWidget { - const RegisterViaSocialScreen({super.key}); + final AuthResultEntity? authResult; + final String? provider; + + const RegisterViaSocialScreen({super.key, this.authResult, this.provider}); @override State createState() => _RegisterViaSocialScreenState(); @@ -17,201 +33,421 @@ class _RegisterViaSocialScreenState extends State { final maskFormatter = MaskTextInputFormatter(mask: '+7 (###) ### ## ##', filter: {"#": RegExp(r'[0-9]')}); final maskFormatterForBirthday = MaskTextInputFormatter(mask: '##.##.####', filter: {"#": RegExp(r'[0-9]')}); + // Контроллеры для полей ввода + final firstNameController = TextEditingController(); + final lastNameController = TextEditingController(); + final emailController = TextEditingController(); + final phoneController = TextEditingController(); + final dateOfBirthController = TextEditingController(); + final weightController = TextEditingController(); + final heightController = TextEditingController(); + + // Состояние для выбора пола + bool isMaleSelected = true; + + @override + void initState() { + super.initState(); + _initializeFieldsFromAuthResult(); + } + + Future _reconnectWebSocket(BuildContext context) async { + try { + await sl().clear(); + await sl().initialize(); + } catch (e) { + rethrow; + } + if (context.mounted) { + try { + await sl().getMyProfile(); + await sl().getChatRoomsWithLoading(); + await sl().connectToWebSocket(); + await sl().getNotifications(); + await sl().connectToWebSocket(); + await sl().registerPushToken(); + showSuccessToast('Успешная регистрация'); + } catch (e) { + rethrow; + } + } + AppRouter.appRouter.go('/feed-screen', extra: {'needToInitializeWebSocket': false}); + } + + void _initializeFieldsFromAuthResult() { + if (widget.authResult != null) { + final authResult = widget.authResult!; + + // Логируем полученные данные для отладки + print('AuthResult data:'); + print('firstName: ${authResult.firstName}'); + print('lastName: ${authResult.lastName}'); + print('email: ${authResult.email}'); + print('phone: ${authResult.phone}'); + print('birthDate: ${authResult.birthDate}'); + print('gender: ${authResult.gender}'); + print('provider: ${widget.provider}'); + + // Предзаполняем поля из AuthResultEntity + if (authResult.firstName != null) { + firstNameController.text = authResult.firstName!; + } + if (authResult.lastName != null) { + lastNameController.text = authResult.lastName!; + } + if (authResult.email != null) { + emailController.text = authResult.email!; + } + if (authResult.phone != null) { + phoneController.text = authResult.phone!; + } + if (authResult.birthDate != null) { + dateOfBirthController.text = _formatBirthDate(authResult.birthDate!); + } + if (authResult.gender != null) { + isMaleSelected = authResult.gender == GenderEnum.male; + } + } + } + + String _formatBirthDate(String birthDate) { + try { + // Пытаемся распарсить различные форматы даты + DateTime? date; + + // Если дата в формате ISO (YYYY-MM-DD) + if (birthDate.contains('-')) { + date = DateTime.tryParse(birthDate); + } + // Если дата в формате DD.MM.YYYY + else if (birthDate.contains('.')) { + final parts = birthDate.split('.'); + if (parts.length == 3) { + final day = int.tryParse(parts[0]); + final month = int.tryParse(parts[1]); + final year = int.tryParse(parts[2]); + if (day != null && month != null && year != null) { + date = DateTime(year, month, day); + } + } + } + + if (date != null) { + // Форматируем в DD.MM.YYYY + final day = date.day.toString().padLeft(2, '0'); + final month = date.month.toString().padLeft(2, '0'); + final year = date.year.toString(); + return '$day.$month.$year'; + } + } catch (e) { + // Если не удалось распарсить, возвращаем исходную строку + } + + return birthDate; + } + + String _getProviderName(String? provider) { + switch (provider) { + case 'vk': + return 'ВКонтакте'; + case 'yandex': + return 'Яндекс'; + case 'ok': + return 'Одноклассники'; + case 'mailru': + return 'Mail.ru'; + default: + return 'социальной сети'; + } + } + + String _getDataStatusMessage(AuthResultEntity authResult) { + final hasEmail = authResult.email != null && authResult.email!.isNotEmpty; + final hasPhone = authResult.phone != null && authResult.phone!.isNotEmpty; + final hasBirthDate = authResult.birthDate != null && authResult.birthDate!.isNotEmpty; + + if (hasEmail && hasPhone && hasBirthDate) { + return 'Все данные успешно получены и предзаполнены.'; + } else if (hasEmail && hasPhone) { + return 'Email и номер телефона получены. Дата рождения может быть недоступна из-за настроек приватности.'; + } else if (hasEmail) { + return 'Email получен. Номер телефона и дата рождения могут быть недоступны из-за настроек приватности.'; + } else if (hasPhone) { + return 'Номер телефона получен. Email и дата рождения могут быть недоступны из-за настроек приватности.'; + } else { + // Специальное сообщение для VK + if (widget.provider == 'vk') { + return 'Основные данные получены. Email запрашивается автоматически. Номер телефона получается через VK API. Если номер замаскирован, это означает, что пользователь ограничил доступ к нему в настройках приватности.'; + } else { + return 'Основные данные получены. Email, номер телефона и дата рождения могут быть недоступны из-за настроек приватности.'; + } + } + } + + @override + void dispose() { + firstNameController.dispose(); + lastNameController.dispose(); + emailController.dispose(); + phoneController.dispose(); + dateOfBirthController.dispose(); + weightController.dispose(); + heightController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(toolbarHeight: 0), - body: SingleChildScrollView( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 24), - const Center( - child: Text( - 'Регистрация', - style: TextStyle(fontSize: 24, fontWeight: FontWeight.w700, height: 1.1, color: Color(0xFF323F4B)), - ), - ), - const SizedBox(height: 24), - Row( - children: [ - SizedBox( - width: MediaQuery.of(context).size.width / 2 - 22, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - const Text('Имя'), - const SizedBox(height: 4), - CustomTextFormField(controller: TextEditingController(), hintText: 'Введите имя', onChanged: (newValue) {}), - ], + return BlocProvider( + create: (context) => CubitRegistrations.sl(), + child: Scaffold( + appBar: AppBar(toolbarHeight: 0), + body: BlocBuilder( + builder: (context, state) { + return SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 24), + const Center( + child: Text( + 'Регистрация', + style: TextStyle(fontSize: 24, fontWeight: FontWeight.w700, height: 1.1, color: Color(0xFF323F4B)), + ), ), - ), - const SizedBox(width: 12), - SizedBox( - width: MediaQuery.of(context).size.width / 2 - 22, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + const SizedBox(height: 24), + Row( children: [ - const Text('Фамилия'), - const SizedBox(height: 4), - CustomTextFormField(controller: TextEditingController(), hintText: 'Введите фамилию', onChanged: (newValue) {}), - ], - ), - ), - ], - ), - const SizedBox(height: 12), - const Text('Почта'), - const SizedBox(height: 4), - CustomTextFormField( - controller: TextEditingController(), - textInputType: TextInputType.emailAddress, - keyboardType: TextInputType.emailAddress, - hintText: 'Введите email', - textInputAction: TextInputAction.next, - onChanged: (newValue) {}, - ), - const SizedBox(height: 12), - const Text('Номер телефона'), - const SizedBox(height: 4), - CustomTextFormField( - textInputAction: TextInputAction.done, - hintText: '+7 (999) 999 99 99', - controller: TextEditingController(), - keyboardType: TextInputType.number, - inputFormatters: [maskFormatter], - onChanged: (newValue) {}, - ), - const SizedBox(height: 12), - Row( - children: [ - SizedBox( - width: MediaQuery.of(context).size.width / 2 - 22, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - const Text('Дата рождения'), - const SizedBox(height: 4), - CustomTextFormField( - textInputAction: TextInputAction.done, - hintText: '01.01.2000', - controller: TextEditingController(), - keyboardType: TextInputType.number, - inputFormatters: [maskFormatterForBirthday], - onChanged: (newValue) {}, + SizedBox( + width: MediaQuery.of(context).size.width / 2 - 22, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + const Text('Имя'), + const SizedBox(height: 4), + CustomTextFormField(controller: firstNameController, hintText: 'Введите имя', onChanged: (newValue) {}), + ], + ), + ), + const SizedBox(width: 12), + SizedBox( + width: MediaQuery.of(context).size.width / 2 - 22, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Фамилия'), + const SizedBox(height: 4), + CustomTextFormField(controller: lastNameController, hintText: 'Введите фамилию', onChanged: (newValue) {}), + ], + ), ), ], ), - ), - const SizedBox(width: 12), - SizedBox( - width: MediaQuery.of(context).size.width / 2 - 22, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + const SizedBox(height: 12), + const Text('Почта'), + const SizedBox(height: 4), + CustomTextFormField( + controller: emailController, + textInputType: TextInputType.emailAddress, + keyboardType: TextInputType.emailAddress, + hintText: 'Введите email', + textInputAction: TextInputAction.next, + onChanged: (newValue) {}, + ), + const SizedBox(height: 12), + const Text('Номер телефона'), + const SizedBox(height: 4), + CustomTextFormField( + textInputAction: TextInputAction.done, + hintText: '+7 (999) 999 99 99', + controller: phoneController, + keyboardType: TextInputType.number, + inputFormatters: [maskFormatter], + onChanged: (newValue) {}, + ), + const SizedBox(height: 12), + Row( children: [ - const Text('Пол'), - const SizedBox(height: 4), - Row( + SizedBox( + width: MediaQuery.of(context).size.width / 2 - 22, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + const Text('Дата рождения'), + const SizedBox(height: 4), + CustomTextFormField( + textInputAction: TextInputAction.done, + hintText: '01.01.2000', + controller: dateOfBirthController, + keyboardType: TextInputType.number, + inputFormatters: [maskFormatterForBirthday], + onChanged: (newValue) {}, + ), + ], + ), + ), + const SizedBox(width: 12), + SizedBox( + width: MediaQuery.of(context).size.width / 2 - 22, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Пол'), + const SizedBox(height: 4), + Row( + children: [ + Expanded(child: _genderButton('М', isMaleSelected)), + const SizedBox(width: 4), + Expanded(child: _genderButton('Ж', !isMaleSelected)), + ], + ), + ], + ), + ), + ], + ), + const SizedBox(height: 12), + Row( + children: [ + SizedBox( + width: MediaQuery.of(context).size.width / 2 - 22, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + const Text('Вес'), + const SizedBox(height: 4), + CustomTextFormField(controller: weightController, hintText: 'Введите вес', onChanged: (newValue) {}), + ], + ), + ), + const SizedBox(width: 12), + SizedBox( + width: MediaQuery.of(context).size.width / 2 - 22, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Рост'), + const SizedBox(height: 4), + CustomTextFormField(controller: heightController, hintText: 'Введите рост', onChanged: (newValue) {}), + ], + ), + ), + ], + ), + const SizedBox(height: 28), + const Center( + child: Text( + 'Проверьте правильность заполненных\nданных. Пол, рост и вес требуются для\nрасчета вашей дневной нормы калорий', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF323F4B)), + textAlign: TextAlign.center, + ), + ), + const SizedBox(height: 28), + InkWell( + borderRadius: BorderRadius.circular(20), + onTap: () => _handleRegistration(context), + child: Ink( + width: MediaQuery.of(context).size.width - 32, + height: 52, + decoration: BoxDecoration(color: state is OAuthStateLoading ? Colors.grey : appTheme.blueA400, borderRadius: BorderRadius.circular(20)), + child: Center( + child: state is OAuthStateLoading + ? const CircularProgressIndicator(color: Colors.white) + : const Text( + 'Зарегистрироваться', + style: TextStyle(fontWeight: FontWeight.w600, fontSize: 18, color: Colors.white), + ), + ), + ), + ), + const SizedBox(height: 16), + Center( + child: RichText( + textAlign: TextAlign.center, + text: TextSpan( + text: 'Нажимая "Зарегистрироваться", Вы соглашаетесь\n', + style: const TextStyle(fontSize: 16, color: Colors.black), children: [ - Expanded(child: _genderButton('М', true)), - const SizedBox(width: 4), - Expanded(child: _genderButton('Ж', false)), + const TextSpan( + text: 'c ', + style: TextStyle(fontSize: 16, color: Colors.black), + ), + if (!hidePlans) ...[ + TextSpan( + text: ' Публичной офертой', + style: TextStyle( + fontSize: 16, + color: appTheme.blueA400, + decorationColor: appTheme.blueA400, + decoration: TextDecoration.underline, + ), + recognizer: TapGestureRecognizer()..onTap = () async => await launchUrl(Uri.parse('$baseUrl/oferta_bs.pdf')), + ), + const TextSpan( + text: ' и ', + style: TextStyle(fontSize: 16, color: Colors.black), + ), + ], + TextSpan( + text: 'Политикой конфиденциальности и защитой персональных данных', + style: TextStyle(fontSize: 16, color: appTheme.blueA400, decorationColor: appTheme.blueA400, decoration: TextDecoration.underline), + recognizer: TapGestureRecognizer()..onTap = () async => await launchUrl(Uri.parse('$baseUrl/docs/privacy.pdf')), + ), ], ), - ], - ), - ), - ], - ), - const SizedBox(height: 12), - Row( - children: [ - SizedBox( - width: MediaQuery.of(context).size.width / 2 - 22, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - const Text('Вес'), - const SizedBox(height: 4), - CustomTextFormField(controller: TextEditingController(), hintText: 'Введите вес', onChanged: (newValue) {}), - ], - ), - ), - const SizedBox(width: 12), - SizedBox( - width: MediaQuery.of(context).size.width / 2 - 22, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text('Рост'), - const SizedBox(height: 4), - CustomTextFormField(controller: TextEditingController(), hintText: 'Введите рост', onChanged: (newValue) {}), - ], - ), - ), - ], - ), - const SizedBox(height: 28), - const Center( - child: Text( - 'Проверьте правильность заполненных\nданных. Пол, рост и вес требуются для\nрасчета вашей дневной нормы калорий', - style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF323F4B)), - textAlign: TextAlign.center, - ), - ), - const SizedBox(height: 28), - InkWell( - borderRadius: BorderRadius.circular(20), - onTap: () {}, - child: Ink( - width: MediaQuery.of(context).size.width - 32, - height: 52, - decoration: BoxDecoration(color: appTheme.blueA400, borderRadius: BorderRadius.circular(20)), - child: const Center( - child: Text( - 'Зарегистрироваться', - style: TextStyle(fontWeight: FontWeight.w600, fontSize: 18, color: Colors.white), - ), - ), - ), - ), - const SizedBox(height: 16), - Center( - child: RichText( - textAlign: TextAlign.center, - text: TextSpan( - text: 'Нажимая "Зарегистрироваться", Вы соглашаетесь\n', - style: const TextStyle(fontSize: 16, color: Colors.black), - children: [ - const TextSpan( - text: 'c ', - style: TextStyle(fontSize: 16, color: Colors.black), ), - if (!hidePlans) ...[ - TextSpan( - text: ' Публичной офертой', - style: TextStyle(fontSize: 16, color: appTheme.blueA400, decorationColor: appTheme.blueA400, decoration: TextDecoration.underline), - recognizer: TapGestureRecognizer()..onTap = () async => await launchUrl(Uri.parse('$baseUrl/oferta_bs.pdf')), + ), + // Показываем ошибки если есть + if (state is OAuthStateError) ...[ + const SizedBox(height: 16), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.red.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.red.shade200), ), - const TextSpan( - text: ' и ', - style: TextStyle(fontSize: 16, color: Colors.black), + child: Text( + state.errorMessage, + style: TextStyle(color: Colors.red.shade700), + textAlign: TextAlign.center, ), - ], - TextSpan( - text: 'Политикой конфиденциальности и защитой персональных данных', - style: TextStyle(fontSize: 16, color: appTheme.blueA400, decorationColor: appTheme.blueA400, decoration: TextDecoration.underline), - recognizer: TapGestureRecognizer()..onTap = () async => await launchUrl(Uri.parse('$baseUrl/docs/privacy.pdf')), ), ], - ), + // Обработка успешной регистрации + if (state is OAuthStateLoaded) ...[ + const SizedBox(height: 16), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.green.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.green.shade200), + ), + child: const Text( + 'Регистрация успешно завершена!', + style: TextStyle(color: Colors.green), + textAlign: TextAlign.center, + ), + ), + // Автоматический переход на главный экран после успешной регистрации + Builder( + builder: (context) { + WidgetsBinding.instance.addPostFrameCallback((_) { + _reconnectWebSocket(context); + }); + return const SizedBox.shrink(); + }, + ), + ], + ], ), - ), - ], + ); + }, ), ), ); @@ -219,29 +455,67 @@ class _RegisterViaSocialScreenState extends State { Widget _genderButton(String gender, bool isActive) { return InkWell( - onTap: () {}, + onTap: () { + setState(() { + isMaleSelected = gender == 'М'; + }); + }, borderRadius: BorderRadius.circular(20), child: Ink( height: 52, decoration: BoxDecoration( color: const Color(0xFFF6F6F6), borderRadius: BorderRadius.circular(20), - border: isActive ? Border.all( - width: 2, - color: appTheme.blueA400, - ) : null, + border: isActive ? Border.all(width: 2, color: appTheme.blueA400) : null, ), child: Center( child: Text( gender, - style: TextStyle( - color: isActive ? appTheme.blueA400 : const Color(0xFF323F4B), - fontSize: 16, - fontWeight: FontWeight.w500, - ), + style: TextStyle(color: isActive ? appTheme.blueA400 : const Color(0xFF323F4B), fontSize: 16, fontWeight: FontWeight.w500), ), ), ), ); } + + void _handleRegistration(BuildContext context) { + // Валидация полей + if (firstNameController.text.isEmpty || + lastNameController.text.isEmpty || + emailController.text.isEmpty || + phoneController.text.isEmpty || + dateOfBirthController.text.isEmpty || + weightController.text.isEmpty || + heightController.text.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Пожалуйста, заполните все поля'))); + return; + } + + // Парсим числовые значения + int? weight; + int? height; + try { + weight = int.parse(weightController.text); + height = int.parse(heightController.text); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Вес и рост должны быть числами'))); + return; + } + + // Вызываем OAuth регистрацию + context.read().getOAuthToken( + provider: widget.provider ?? 'social', + accessToken: widget.authResult?.accessToken ?? 'dummy_token', + id: widget.authResult?.userId ?? 'dummy_id', + refreshToken: widget.authResult?.refreshToken ?? 'dummy_refresh_token', + firstName: firstNameController.text, + lastName: lastNameController.text, + email: emailController.text, + phone: phoneController.text, + dateOfBirth: dateOfBirthController.text, + gender: isMaleSelected ? 'М' : 'Ж', + weight: weight, + height: height, + ); + } } diff --git a/lib/features/auth/repositories/auth_repository.dart b/lib/features/auth/repositories/auth_repository.dart index 84df92f6..bddebce1 100644 --- a/lib/features/auth/repositories/auth_repository.dart +++ b/lib/features/auth/repositories/auth_repository.dart @@ -1,4 +1,5 @@ import 'package:eatvio_app/features/auth/models/credentials_model.dart'; +import 'package:eatvio_app/features/auth/models/oauth_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'; @@ -8,26 +9,17 @@ class AuthRepository { AuthRepository({required this.authService}); - Future getTokenByCredentials( - String email, - String password, - ) async { + Future getTokenByCredentials(String email, String password) async { try { return await authService.getTokenByCredentials( - credentials: CredentialsModel( - email: email, - password: password, - ), + credentials: CredentialsModel(email: email, password: password), ); } catch (e) { rethrow; } } - Future getTokenBySocial({ - required String provider, - required String accessToken, - }) async { + Future getTokenBySocial({required String provider, required String accessToken}) async { try { return await authService.getTokenBySocial( body: SocialExchangeRequest(provider: provider, accessToken: accessToken), @@ -36,4 +28,40 @@ class AuthRepository { rethrow; } } + + Future getOAuthToken({ + required String provider, + required String accessToken, + required String id, + required String refreshToken, + required String firstName, + required String lastName, + required String email, + required String phone, + required String dateOfBirth, + required String gender, + required int weight, + required int height, + }) async { + try { + return (await authService.getOAuthToken( + body: OAuthModel( + provider: provider, + accessToken: accessToken, + id: id, + refreshToken: refreshToken, + firstName: firstName, + lastName: lastName, + email: email, + phone: phone, + dateOfBirth: dateOfBirth, + gender: gender == 'М' ? 'male' : 'female', + weight: weight, + height: height, + ), + ))['token']! as String; + } catch (e) { + rethrow; + } + } } diff --git a/lib/features/auth/sdk/social_auth_facade.dart b/lib/features/auth/sdk/social_auth_facade.dart index a235f03d..ada242a9 100644 --- a/lib/features/auth/sdk/social_auth_facade.dart +++ b/lib/features/auth/sdk/social_auth_facade.dart @@ -27,7 +27,7 @@ class SocialAuthFacade { try { final controller = VkIDController(clID: int.parse(clientIdStr)); - final authUri = controller.generateAuthorizeLinkWithCodeVerifier(); + final authUri = controller.generateAuthorizeLinkWithCodeVerifier(provider: VkProvider.vkID, scopes: [VkScope.email, VkScope.phone]); final authUrl = authUri.value.toString(); final codeVerifier = authUri.key; @@ -105,18 +105,16 @@ class SocialAuthFacade { return null; } - static Future signInWithOk() async { + static Future signInWithOkAndFetchProfile() async { final context = AppRouter.navigatorKey.currentContext; if (context == null) return null; - final vkAuthRes = await _signInWithVkIdProvider(context: context, provider: VkProvider.okRu); - return vkAuthRes?.accessToken; + return await _signInWithVkIdProvider(context: context, provider: VkProvider.okRu); } - static Future signInWithMailRu() async { + static Future signInWithMailRuAndFetchProfile() async { final context = AppRouter.navigatorKey.currentContext; if (context == null) return null; - final vkAuthRes = await _signInWithVkIdProvider(context: context, provider: VkProvider.mailRu); - return vkAuthRes?.accessToken; + return await _signInWithVkIdProvider(context: context, provider: VkProvider.mailRu); } static Future signInWithYandex() async { diff --git a/lib/features/auth/services/auth_service.dart b/lib/features/auth/services/auth_service.dart index 7dabb65b..3d8d43e2 100644 --- a/lib/features/auth/services/auth_service.dart +++ b/lib/features/auth/services/auth_service.dart @@ -1,5 +1,6 @@ import 'package:dio/dio.dart'; import 'package:eatvio_app/features/auth/models/credentials_model.dart'; +import 'package:eatvio_app/features/auth/models/oauth_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'; @@ -19,4 +20,9 @@ abstract class AuthService { Future getTokenBySocial({ @Body() required SocialExchangeRequest body, }); + + @POST('/api/oauth/callback') + Future> getOAuthToken({ + @Body() required OAuthModel body, + }); } From b2433b87b6d281066dfc0af394dcad09b31fb8fd Mon Sep 17 00:00:00 2001 From: misha Date: Fri, 10 Oct 2025 14:08:21 +0300 Subject: [PATCH 60/78] feat(diary): added navigation to detailed diet screen form diary --- lib/core/routing/content_routes.dart | 9 +- lib/features/auth/sdk/social_auth_facade.dart | 13 +- .../widgets/chosen_diet_widget.dart | 110 ++++++++------- .../presentation/cubit/about_diet_cubit.dart | 129 ++++++++---------- .../presentation/pages/about_diet_screen.dart | 11 +- 5 files changed, 139 insertions(+), 133 deletions(-) diff --git a/lib/core/routing/content_routes.dart b/lib/core/routing/content_routes.dart index 69d7ebf4..0556fab6 100644 --- a/lib/core/routing/content_routes.dart +++ b/lib/core/routing/content_routes.dart @@ -44,10 +44,15 @@ class ContentRoutes { builder: (context, state) { final slug = state.uri.queryParameters['slug']; if (slug != null) { - return AboutDietScreen(slug: slug, index: null, navigateToComments: false); + return AboutDietScreen(id: null, slug: slug, index: null, navigateToComments: false); } final data = state.extra as Map; - return AboutDietScreen(slug: data['slug'] as String, index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool); + return AboutDietScreen( + id: data['id'] as int?, + slug: data['slug'] as String?, + index: data['index'] as int?, + navigateToComments: data['navigateToComments'] as bool, + ); }, ), GoRoute( diff --git a/lib/features/auth/sdk/social_auth_facade.dart b/lib/features/auth/sdk/social_auth_facade.dart index a235f03d..8ea57b48 100644 --- a/lib/features/auth/sdk/social_auth_facade.dart +++ b/lib/features/auth/sdk/social_auth_facade.dart @@ -27,7 +27,7 @@ class SocialAuthFacade { try { final controller = VkIDController(clID: int.parse(clientIdStr)); - final authUri = controller.generateAuthorizeLinkWithCodeVerifier(); + final authUri = controller.generateAuthorizeLinkWithCodeVerifier(scopes: [VkScope.phone, VkScope.email, VkScope.personalInfo]); final authUrl = authUri.value.toString(); final codeVerifier = authUri.key; @@ -76,7 +76,16 @@ class SocialAuthFacade { log(msg); return null; } + final response = await http.get(Uri.parse('https://id.vk.com/openapi/v1/phone_number'), headers: {'Authorization': 'Bearer ${auth.accessToken}'}); + if (response.statusCode == 200) { + final json = jsonDecode(response.body); + final phone = json['phone_number']; + log(json.toString()); + log('Phone: $phone'); + } else { + log('VK phone request failed: ${response.statusCode} ${response.body}'); + } return AuthResultEntity( accessToken: auth.accessToken, refreshToken: auth.refreshToken, @@ -204,7 +213,7 @@ class SocialAuthFacade { try { final controller = VkIDController(clID: int.parse(clientIdStr)); - final authUri = controller.generateAuthorizeLinkWithCodeVerifier(provider: provider); + final authUri = controller.generateAuthorizeLinkWithCodeVerifier(provider: provider, scopes: [VkScope.phone, VkScope.email, VkScope.personalInfo]); final codeVerifier = authUri.key; final authUrl = authUri.value.toString(); final redirectScheme = 'vk$clientIdStr://'; diff --git a/lib/features/diary/presentation/widgets/chosen_diet_widget.dart b/lib/features/diary/presentation/widgets/chosen_diet_widget.dart index 3b5f23fd..622e0e5e 100644 --- a/lib/features/diary/presentation/widgets/chosen_diet_widget.dart +++ b/lib/features/diary/presentation/widgets/chosen_diet_widget.dart @@ -1,3 +1,4 @@ +import 'package:eatvio_app/core/routing/route_constants.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'; @@ -6,77 +7,84 @@ import 'package:eatvio_app/features/diary/presentation/bloc/diary_state.dart'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; class ChosenDietWidget extends StatelessWidget { const ChosenDietWidget({super.key}); @override Widget build(BuildContext context) { - return BlocBuilder(builder: (context, state) { - if (state is DiaryStateLoaded) { - if (state.diary.typeOfDiary.hasChosenDiet) { - return ChosenDietWidgetContent( - currentDietDay: state.diary.dayOfChosenDiet ?? 0, - dietName: state.diary.diet?.name ?? '', - dietDuration: state.diary.diet?.duration ?? 0, - ); + return BlocBuilder( + builder: (context, state) { + if (state is DiaryStateLoaded) { + if (state.diary.typeOfDiary.hasChosenDiet) { + return ChosenDietWidgetContent( + currentDietDay: state.diary.dayOfChosenDiet ?? 0, + dietName: state.diary.diet?.name ?? '', + dietDuration: state.diary.diet?.duration ?? 0, + dietId: state.diary.diet?.id ?? 0, + ); + } } - } - if (state is DiaryStateLoadedTomorrowWithDiet) { - if (state.diary.typeOfDiary.hasChosenDiet) { - return Column( - children: [ - const CustomDivider(), - const SizedBox(height: 16), - ChosenDietWidgetContent( - currentDietDay: state.diary.dayOfChosenDiet ?? 0, - dietName: state.diary.diet?.name ?? '', - dietDuration: state.diary.diet?.duration ?? 0, - ), - ], - ); + if (state is DiaryStateLoadedTomorrowWithDiet) { + if (state.diary.typeOfDiary.hasChosenDiet) { + return Column( + children: [ + const CustomDivider(), + const SizedBox(height: 16), + ChosenDietWidgetContent( + currentDietDay: state.diary.dayOfChosenDiet ?? 0, + dietName: state.diary.diet?.name ?? '', + dietDuration: state.diary.diet?.duration ?? 0, + dietId: state.diary.diet?.id ?? 0, + ), + ], + ); + } } - } - return const SizedBox.shrink(); - }); + return const SizedBox.shrink(); + }, + ); } } class ChosenDietWidgetContent extends StatelessWidget { - const ChosenDietWidgetContent({ - super.key, - required this.currentDietDay, - required this.dietName, - required this.dietDuration, - }); + const ChosenDietWidgetContent({super.key, required this.currentDietDay, required this.dietName, required this.dietDuration, required this.dietId}); final int currentDietDay; final String dietName; final int dietDuration; + final int dietId; @override Widget build(BuildContext context) { final size = MediaQuery.sizeOf(context); final s = S.of(context); - return Column( - children: [ - Row( - children: [ - SizedBox( - width: size.width * 0.4, - child: Text(dietName, style: TextStyles.textLMedium.copyWith(color: appTheme.black)), - ), - const Spacer(), - Container( - decoration: BoxDecoration(color: appTheme.dividerColor, borderRadius: BorderRadius.circular(12)), - padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 12), - child: Text('${s.day} $currentDietDay из $dietDuration', style: TextStyles.textMSemibold.copyWith(color: appTheme.gray)), - ) - ], - ), - const SizedBox( - height: 16, - ) - ], + return GestureDetector( + onTap: () { + GoRouter.of(context).push(RouteConstants.aboutDietScreen, extra: {'id': dietId, 'navigateToComments': false}); + }, + child: Column( + children: [ + Row( + children: [ + SizedBox( + width: size.width * 0.4, + child: Text( + dietName, + style: TextStyles.textLMedium.copyWith(color: appTheme.black, decoration: TextDecoration.underline), + ), + ), + const Spacer(), + Container( + decoration: BoxDecoration(color: appTheme.dividerColor, borderRadius: BorderRadius.circular(12)), + padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 12), + child: Text('${s.day} $currentDietDay из $dietDuration', style: TextStyles.textMSemibold.copyWith(color: appTheme.gray)), + ), + ], + ), + const SizedBox(height: 16), + ], + ), ); } } diff --git a/lib/features/diets/presentation/cubit/about_diet_cubit.dart b/lib/features/diets/presentation/cubit/about_diet_cubit.dart index 9d02534e..6ba7980f 100644 --- a/lib/features/diets/presentation/cubit/about_diet_cubit.dart +++ b/lib/features/diets/presentation/cubit/about_diet_cubit.dart @@ -19,9 +19,14 @@ class AboutDietCubit extends Cubit { String? userStartedDietName; List _selectedIndexes = []; - Future getDiet(String slug, {int? index}) async { + Future getDiet({String? slug, int? id, int? index}) async { try { - final diet = await dietsRepository.getDietBySlug(slug); + final Diet diet; + if (slug != null) { + diet = await dietsRepository.getDietBySlug(slug); + } else { + diet = await dietsRepository.getDiet(id!); + } if (index != null) { dietsRepository.emitCommentsStateToFeed(CommentsState(index: index, totalComments: diet.totalComments)); } @@ -32,10 +37,16 @@ class AboutDietCubit extends Cubit { } } - Future initDietPage(String slug) async { + Future initDietPage({String? slug, int? id}) async { try { emit(const AboutDietState.loading()); - final diet = await dietsRepository.getDietBySlug(slug); + final Diet diet; + if (slug != null) { + diet = await dietsRepository.getDietBySlug(slug); + } else { + diet = await dietsRepository.getDiet(id!); + } + final chosenDiets = await profileRepository.getChosenDiets(); final lastChosenDiet = chosenDiets.firstOrNull; if (lastChosenDiet != null && ['inited', 'started'].contains(lastChosenDiet.state)) { @@ -50,12 +61,7 @@ class AboutDietCubit extends Cubit { } } _selectedIndexes = List.generate(diet.dailySchedule.length, (index) => 0); - emit(AboutDietState.loaded( - diet: diet, - isStarted: isCurrentDietStarted, - hasStartedDiet: hasStartedDiet, - selectedIndexes: _selectedIndexes, - )); + emit(AboutDietState.loaded(diet: diet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet, selectedIndexes: _selectedIndexes)); } catch (e) { log(e.toString()); emit(AboutDietState.error(message: e.toString())); @@ -68,41 +74,30 @@ class AboutDietCubit extends Cubit { final currentDiet = (state as DietStateLoaded).diet; if (currentDiet.wasLikedByMe) { - final updatedDiet = currentDiet.copyWith( - totalLikes: currentDiet.totalLikes - 1, - wasLikedByMe: false, - ); + final updatedDiet = currentDiet.copyWith(totalLikes: currentDiet.totalLikes - 1, wasLikedByMe: false); if (dietIndexInFeed != null) { dietsRepository.emitLikesStateToFeed(LikesState(wasLikedByMe: updatedDiet.wasLikedByMe, totalLikes: updatedDiet.totalLikes, index: dietIndexInFeed)); } emit(AboutDietState.loaded(diet: updatedDiet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet, selectedIndexes: _selectedIndexes)); } else { - final updatedDiet = currentDiet.copyWith( - totalLikes: currentDiet.totalLikes + 1, - wasLikedByMe: true, - ); + final updatedDiet = currentDiet.copyWith(totalLikes: currentDiet.totalLikes + 1, wasLikedByMe: true); if (dietIndexInFeed != null) { dietsRepository.emitLikesStateToFeed(LikesState(wasLikedByMe: updatedDiet.wasLikedByMe, totalLikes: updatedDiet.totalLikes, index: dietIndexInFeed)); } emit(AboutDietState.loaded(diet: updatedDiet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet, selectedIndexes: _selectedIndexes)); } - EasyDebounce.debounce( - 'like_diet', - const Duration(seconds: 1), - () async { - try { - await dietsRepository.likeDiet(id); - } catch (e) { - log(e.toString()); - if (dietIndexInFeed != null) { - dietsRepository - .emitLikesStateToFeed(LikesState(wasLikedByMe: currentDiet.wasLikedByMe, totalLikes: currentDiet.totalLikes, index: dietIndexInFeed)); - } - emit(AboutDietState.loaded(selectedIndexes: _selectedIndexes,diet: currentDiet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet)); + EasyDebounce.debounce('like_diet', const Duration(seconds: 1), () async { + try { + await dietsRepository.likeDiet(id); + } catch (e) { + log(e.toString()); + if (dietIndexInFeed != null) { + dietsRepository.emitLikesStateToFeed(LikesState(wasLikedByMe: currentDiet.wasLikedByMe, totalLikes: currentDiet.totalLikes, index: dietIndexInFeed)); } - }, - ); + emit(AboutDietState.loaded(selectedIndexes: _selectedIndexes, diet: currentDiet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet)); + } + }); } Future addToFavorite(int id, int? dietIndex) async { @@ -110,30 +105,24 @@ class AboutDietCubit extends Cubit { final currentDiet = (state as DietStateLoaded).diet; - final updatedDiet = currentDiet.copyWith( - isFavorite: true, - ); + final updatedDiet = currentDiet.copyWith(isFavorite: true); if (dietIndex != null) { dietsRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: updatedDiet.isFavorite, index: dietIndex)); } - emit(AboutDietState.loaded(selectedIndexes: _selectedIndexes,diet: updatedDiet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet)); + emit(AboutDietState.loaded(selectedIndexes: _selectedIndexes, diet: updatedDiet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet)); - EasyDebounce.debounce( - 'add_or_remove_diet_to_favorites', - const Duration(seconds: 1), - () async { - try { - await dietsRepository.addToFavorite(id); - } catch (e) { - log(e.toString()); - showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время'); - if (dietIndex != null) { - dietsRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: currentDiet.isFavorite, index: dietIndex)); - } - emit(AboutDietState.loaded(selectedIndexes: _selectedIndexes,diet: currentDiet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet)); + EasyDebounce.debounce('add_or_remove_diet_to_favorites', const Duration(seconds: 1), () async { + try { + await dietsRepository.addToFavorite(id); + } catch (e) { + log(e.toString()); + showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время'); + if (dietIndex != null) { + dietsRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: currentDiet.isFavorite, index: dietIndex)); } - }, - ); + emit(AboutDietState.loaded(selectedIndexes: _selectedIndexes, diet: currentDiet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet)); + } + }); } Future removeFromFavorite(int id, int? dietIndex) async { @@ -141,30 +130,24 @@ class AboutDietCubit extends Cubit { final currentDiet = (state as DietStateLoaded).diet; - final updatedDiet = currentDiet.copyWith( - isFavorite: false, - ); + final updatedDiet = currentDiet.copyWith(isFavorite: false); if (dietIndex != null) { dietsRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: updatedDiet.isFavorite, index: dietIndex)); } - emit(AboutDietState.loaded(selectedIndexes: _selectedIndexes,diet: updatedDiet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet)); + emit(AboutDietState.loaded(selectedIndexes: _selectedIndexes, diet: updatedDiet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet)); - EasyDebounce.debounce( - 'add_or_remove_diet_to_favorites', - const Duration(seconds: 1), - () async { - try { - await dietsRepository.removeFromFavorite(id); - } catch (e) { - log(e.toString()); - showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время'); - if (dietIndex != null) { - dietsRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: currentDiet.isFavorite, index: dietIndex)); - } - emit(AboutDietState.loaded(selectedIndexes: _selectedIndexes,diet: currentDiet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet)); + EasyDebounce.debounce('add_or_remove_diet_to_favorites', const Duration(seconds: 1), () async { + try { + await dietsRepository.removeFromFavorite(id); + } catch (e) { + log(e.toString()); + showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время'); + if (dietIndex != null) { + dietsRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: currentDiet.isFavorite, index: dietIndex)); } - }, - ); + emit(AboutDietState.loaded(selectedIndexes: _selectedIndexes, diet: currentDiet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet)); + } + }); } Future startDiet(DateTime date) async { @@ -199,13 +182,13 @@ class AboutDietCubit extends Cubit { void clearStartedDietsState() { if (state is! DietStateLoaded) return; final diet = (state as DietStateLoaded).diet; - emit(AboutDietState.loaded(selectedIndexes: _selectedIndexes,diet: diet)); + emit(AboutDietState.loaded(selectedIndexes: _selectedIndexes, diet: diet)); } void emitCurrentStartedDietsState() { if (state is! DietStateLoaded) return; final diet = (state as DietStateLoaded).diet; - emit(AboutDietState.loaded(selectedIndexes: _selectedIndexes,diet: diet, isStarted: true)); + emit(AboutDietState.loaded(selectedIndexes: _selectedIndexes, diet: diet, isStarted: true)); } Future getDietBySlug(String slug) async { diff --git a/lib/features/diets/presentation/pages/about_diet_screen.dart b/lib/features/diets/presentation/pages/about_diet_screen.dart index e4dd4a75..5b6dc8b8 100644 --- a/lib/features/diets/presentation/pages/about_diet_screen.dart +++ b/lib/features/diets/presentation/pages/about_diet_screen.dart @@ -35,11 +35,12 @@ import 'package:flutter_svg/svg.dart'; import 'package:go_router/go_router.dart'; class AboutDietScreen extends StatefulWidget { - final String slug; + final String? slug; + final int? id; final int? index; final bool navigateToComments; - const AboutDietScreen({super.key, required this.index, required this.navigateToComments, required this.slug}); + const AboutDietScreen({super.key, required this.index, required this.navigateToComments, required this.slug, required this.id}); @override State createState() => _AboutDietScreenState(); @@ -79,7 +80,7 @@ class _AboutDietScreenState extends State { final s = S.of(context); return BlocProvider( - create: (context) => dietsCubit..initDietPage(widget.slug), + create: (context) => dietsCubit..initDietPage(slug: widget.slug, id: widget.id), child: BlocProvider( create: (BuildContext context) => commentsCubit, child: BlocConsumer( @@ -283,7 +284,7 @@ class _AboutDietScreenState extends State { replyId = null; }); Future.delayed(const Duration(milliseconds: 500), () { - dietsCubit.getDiet(dietState.diet.slug, index: widget.index); + dietsCubit.getDiet(slug: dietState.diet.slug, index: widget.index); }); FocusManager.instance.primaryFocus?.unfocus(); } @@ -526,7 +527,7 @@ class _AboutDietScreenState extends State { ), ), body: DataErrorWidget( - onReload: () => context.read().getDiet(widget.slug, index: widget.index), + onReload: () => context.read().getDiet(slug: widget.slug, id: widget.id, index: widget.index), ), ); } From 654edc54573f756294c9c4b86bf7de70a8b23fd7 Mon Sep 17 00:00:00 2001 From: misha Date: Fri, 10 Oct 2025 14:37:27 +0300 Subject: [PATCH 61/78] fix: analyzer issues --- lib/core/app_initializer.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/core/app_initializer.dart b/lib/core/app_initializer.dart index 57ccbfb0..ea0b8f5d 100644 --- a/lib/core/app_initializer.dart +++ b/lib/core/app_initializer.dart @@ -6,7 +6,6 @@ import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/firebase_options.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; From f90f1d32841f011750e8457c307a0693ea5bca2b Mon Sep 17 00:00:00 2001 From: misha Date: Fri, 10 Oct 2025 14:45:15 +0300 Subject: [PATCH 62/78] feat(social): fixed baseUrl for RegisterViaSocialScreen --- .../auth/presentation/pages/register_via_social_screen.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/features/auth/presentation/pages/register_via_social_screen.dart b/lib/features/auth/presentation/pages/register_via_social_screen.dart index 22fa5d18..72fa9d91 100644 --- a/lib/features/auth/presentation/pages/register_via_social_screen.dart +++ b/lib/features/auth/presentation/pages/register_via_social_screen.dart @@ -1,3 +1,4 @@ +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/widgets/custom_text_form_field.dart'; @@ -385,7 +386,7 @@ class _RegisterViaSocialScreenState extends State { decorationColor: appTheme.blueA400, decoration: TextDecoration.underline, ), - recognizer: TapGestureRecognizer()..onTap = () async => await launchUrl(Uri.parse('$baseUrl/oferta_bs.pdf')), + recognizer: TapGestureRecognizer()..onTap = () async => await launchUrl(Uri.parse('${sl().baseUrl}oferta_bs.pdf')), ), const TextSpan( text: ' и ', @@ -395,7 +396,7 @@ class _RegisterViaSocialScreenState extends State { TextSpan( text: 'Политикой конфиденциальности и защитой персональных данных', style: TextStyle(fontSize: 16, color: appTheme.blueA400, decorationColor: appTheme.blueA400, decoration: TextDecoration.underline), - recognizer: TapGestureRecognizer()..onTap = () async => await launchUrl(Uri.parse('$baseUrl/docs/privacy.pdf')), + recognizer: TapGestureRecognizer()..onTap = () async => await launchUrl(Uri.parse('${sl().baseUrl}/docs/privacy.pdf')), ), ], ), From 239ff675dbd323734e3f73c34ccf94a1c7d32e71 Mon Sep 17 00:00:00 2001 From: misha Date: Fri, 10 Oct 2025 14:58:24 +0300 Subject: [PATCH 63/78] fix: fixed author card image url handling --- .../presentation/widgets/author_card.dart | 48 ++++--------------- 1 file changed, 9 insertions(+), 39 deletions(-) diff --git a/lib/features/diets/presentation/widgets/author_card.dart b/lib/features/diets/presentation/widgets/author_card.dart index 548b9263..34131dac 100644 --- a/lib/features/diets/presentation/widgets/author_card.dart +++ b/lib/features/diets/presentation/widgets/author_card.dart @@ -38,45 +38,25 @@ class AuthorCard extends StatelessWidget { decoration: BoxDecoration( shape: BoxShape.circle, image: DecorationImage( - image: NetworkImage(sl().baseUrl + author.authorProfileIcon), + image: NetworkImage( + author.authorProfileIcon.contains('https') ? author.authorProfileIcon : sl().baseUrl + author.authorProfileIcon, + ), fit: BoxFit.cover, ), ), ), - subtitle: Text( - author.authorName, - style: TextStyles.textLSemibold.copyWith( - color: appTheme.black, - ), - ), - title: Text( - title, - style: titleStyle ?? TextStyles.textSSemibold.copyWith( - color: appTheme.gray, - ), - ), + subtitle: Text(author.authorName, style: TextStyles.textLSemibold.copyWith(color: appTheme.black)), + title: Text(title, style: titleStyle ?? TextStyles.textSSemibold.copyWith(color: appTheme.gray)), ), Row( children: [ - Text( - 'Консультант', - style: TextStyles.textMMedium.copyWith( - color: appTheme.black, - ), - ), + Text('Консультант', style: TextStyles.textMMedium.copyWith(color: appTheme.black)), const SizedBox(width: 10), if (author.authorPlaceInRating != null) InfoBouble( radius: 12, backgroundColor: appTheme.whiteA700, - children: [ - Text( - '${author.authorPlaceInRating}-й в рейтинге', - style: TextStyles.textMMediumAuto.copyWith( - color: appTheme.blueA400, - ), - ), - ], + children: [Text('${author.authorPlaceInRating}-й в рейтинге', style: TextStyles.textMMediumAuto.copyWith(color: appTheme.blueA400))], ), ], ), @@ -89,18 +69,8 @@ class AuthorCard extends StatelessWidget { child: Container( width: 28, height: 28, - decoration: BoxDecoration( - color: appTheme.whiteA700, - borderRadius: BorderRadius.circular(8), - ), - child: Center( - child: SvgPicture.asset( - 'assets/icons/arrow.svg', - width: 12, - height: 12, - fit: BoxFit.none, - ), - ), + decoration: BoxDecoration(color: appTheme.whiteA700, borderRadius: BorderRadius.circular(8)), + child: Center(child: SvgPicture.asset('assets/icons/arrow.svg', width: 12, height: 12, fit: BoxFit.none)), ), ), ], From 21492a1ad9ad39bef7e2630cc664098286d1b86c Mon Sep 17 00:00:00 2001 From: misha Date: Fri, 10 Oct 2025 15:18:25 +0300 Subject: [PATCH 64/78] fix: removed wrong code --- lib/features/auth/sdk/social_auth_facade.dart | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/lib/features/auth/sdk/social_auth_facade.dart b/lib/features/auth/sdk/social_auth_facade.dart index 80703020..0b60f6a6 100644 --- a/lib/features/auth/sdk/social_auth_facade.dart +++ b/lib/features/auth/sdk/social_auth_facade.dart @@ -76,16 +76,7 @@ class SocialAuthFacade { log(msg); return null; } - final response = await http.get(Uri.parse('https://id.vk.com/openapi/v1/phone_number'), headers: {'Authorization': 'Bearer ${auth.accessToken}'}); - if (response.statusCode == 200) { - final json = jsonDecode(response.body); - final phone = json['phone_number']; - log(json.toString()); - log('Phone: $phone'); - } else { - log('VK phone request failed: ${response.statusCode} ${response.body}'); - } return AuthResultEntity( accessToken: auth.accessToken, refreshToken: auth.refreshToken, @@ -211,7 +202,7 @@ class SocialAuthFacade { try { final controller = VkIDController(clID: int.parse(clientIdStr)); - final authUri = controller.generateAuthorizeLinkWithCodeVerifier(provider: provider, scopes: [VkScope.phone, VkScope.email, VkScope.personalInfo]); + final authUri = controller.generateAuthorizeLinkWithCodeVerifier(provider: provider, scopes: [VkScope.phone, VkScope.email,]); final codeVerifier = authUri.key; final authUrl = authUri.value.toString(); final redirectScheme = 'vk$clientIdStr://'; From 24675714e016fc8e0ee64e202f98b60f4c44b3aa Mon Sep 17 00:00:00 2001 From: misha Date: Fri, 10 Oct 2025 17:02:08 +0300 Subject: [PATCH 65/78] feat(social): added vk client id for ios --- lib/core/env_config.dart | 3 ++- lib/features/auth/sdk/social_auth_facade.dart | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/core/env_config.dart b/lib/core/env_config.dart index 446bc65b..3d451c6f 100644 --- a/lib/core/env_config.dart +++ b/lib/core/env_config.dart @@ -45,7 +45,8 @@ class EnvConfig { static String get yandexCheckUrl => dotenv.env['YANDEX_CHECK_URL'] ?? ''; // VK ID Configuration - static String get vkClientId => dotenv.env['VK_CLIENT_ID'] ?? ''; + static String get vkClientIdIos => dotenv.env['VK_CLIENT_ID_IOS'] ?? ''; + static String get vkClientIdAndroid => dotenv.env['VK_CLIENT_ID_ANDROID'] ?? ''; // Yandex ID Configuration static String get yandexClientId => dotenv.env['YANDEX_CLIENT_ID'] ?? ''; diff --git a/lib/features/auth/sdk/social_auth_facade.dart b/lib/features/auth/sdk/social_auth_facade.dart index ada242a9..b7f4f93d 100644 --- a/lib/features/auth/sdk/social_auth_facade.dart +++ b/lib/features/auth/sdk/social_auth_facade.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:developer'; +import 'dart:io'; import 'package:eatvio_app/features/auth/entities/auth_result_entity.dart'; import 'package:eatvio_app/features/auth/models/yandex_profile_model.dart'; @@ -20,7 +21,7 @@ class SocialAuthFacade { } static Future signInWithVkAndFetchProfile({required BuildContext context}) async { - final clientIdStr = EnvConfig.vkClientId.trim(); + final clientIdStr = Platform.isIOS ? EnvConfig.vkClientIdIos.trim() : EnvConfig.vkClientIdAndroid.trim(); if (clientIdStr.isEmpty) { return null; } @@ -195,7 +196,7 @@ class SocialAuthFacade { } static Future _signInWithVkIdProvider({required BuildContext context, required VkProvider provider}) async { - final clientIdStr = EnvConfig.vkClientId.trim(); + final clientIdStr = Platform.isIOS ? EnvConfig.vkClientIdIos.trim() : EnvConfig.vkClientIdAndroid.trim(); if (clientIdStr.isEmpty) { return null; } From 09a00a41adaa887c6ed81990fa4affd064aec09f Mon Sep 17 00:00:00 2001 From: misha Date: Tue, 14 Oct 2025 12:09:26 +0300 Subject: [PATCH 66/78] added scopes for yandex id --- lib/features/auth/sdk/social_auth_facade.dart | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/lib/features/auth/sdk/social_auth_facade.dart b/lib/features/auth/sdk/social_auth_facade.dart index b7f4f93d..4cab0962 100644 --- a/lib/features/auth/sdk/social_auth_facade.dart +++ b/lib/features/auth/sdk/social_auth_facade.dart @@ -91,21 +91,6 @@ class SocialAuthFacade { ); } - Future?> exchangeCodeForToken(String code, String clientId, String clientSecret) async { - final response = await http.post( - Uri.parse('https://oauth.yandex.ru/token'), - headers: {'Content-Type': 'application/x-www-form-urlencoded'}, - body: {'grant_type': 'authorization_code', 'code': code, 'client_id': clientId, 'client_secret': clientSecret}, - ); - - if (response.statusCode == 200) { - return jsonDecode(response.body) as Map; - } else { - log('Error exchanging code: ${response.body}'); - } - return null; - } - static Future signInWithOkAndFetchProfile() async { final context = AppRouter.navigatorKey.currentContext; if (context == null) return null; @@ -134,7 +119,8 @@ class SocialAuthFacade { 'response_type': 'code', 'client_id': clientId, 'redirect_uri': redirectUri, - 'force_confirm': 'yes', + 'scope': 'login:info login:email', + 'force_confirm': 'true', }).toString(); final redirectSchemePrefix = redirectUri; From 5354c3f517c21f3af679074f8d9e28b7bfa67e66 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Tue, 14 Oct 2025 12:16:20 +0300 Subject: [PATCH 67/78] feat(auth): Issue-691 --- lib/core/app_router.dart | 1 + .../auth/models/oauth_login_request.dart | 15 ++++ .../bloc/oauth_cubit/oauth_cubit.dart | 19 ++++ .../auth/presentation/pages/auth_screen.dart | 90 +++++++++++++++++-- .../pages/register_via_social_screen.dart | 24 +++-- .../auth/repositories/auth_repository.dart | 42 +++++---- lib/features/auth/services/auth_service.dart | 16 ++-- 7 files changed, 168 insertions(+), 39 deletions(-) create mode 100644 lib/features/auth/models/oauth_login_request.dart diff --git a/lib/core/app_router.dart b/lib/core/app_router.dart index 26355055..5ce04746 100644 --- a/lib/core/app_router.dart +++ b/lib/core/app_router.dart @@ -53,6 +53,7 @@ class AppRouter { } static void navigateToRegisterViaSocial({required AuthResultEntity authResult, required String provider}) { + print('AppRouter: Navigating to register via social screen for provider: $provider'); appRouter.goNamed('registerViaSocialScreen', extra: {'authResult': authResult, 'provider': provider}); } } diff --git a/lib/features/auth/models/oauth_login_request.dart b/lib/features/auth/models/oauth_login_request.dart new file mode 100644 index 00000000..43da454b --- /dev/null +++ b/lib/features/auth/models/oauth_login_request.dart @@ -0,0 +1,15 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'oauth_login_request.freezed.dart'; +part 'oauth_login_request.g.dart'; + +@freezed +abstract class OAuthLoginRequest with _$OAuthLoginRequest { + const factory OAuthLoginRequest({ + required String provider, + @JsonKey(name: 'access_token') required String accessToken, + @JsonKey(name: 'refresh_token') required String refreshToken, + }) = _OAuthLoginRequest; + + factory OAuthLoginRequest.fromJson(Map json) => _$OAuthLoginRequestFromJson(json); +} diff --git a/lib/features/auth/presentation/bloc/oauth_cubit/oauth_cubit.dart b/lib/features/auth/presentation/bloc/oauth_cubit/oauth_cubit.dart index 8d73b195..7d45b4b1 100644 --- a/lib/features/auth/presentation/bloc/oauth_cubit/oauth_cubit.dart +++ b/lib/features/auth/presentation/bloc/oauth_cubit/oauth_cubit.dart @@ -1,3 +1,4 @@ +import 'dart:developer'; import 'package:eatvio_app/features/auth/presentation/bloc/oauth_cubit/oauth_state.dart'; import 'package:eatvio_app/features/auth/repositories/auth_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -7,6 +8,23 @@ class OAuthCubit extends Cubit { OAuthCubit({required this.authRepository}) : super(const OAuthState()); + Future getOAuthLogin({required String provider, required String accessToken, required String refreshToken}) async { + try { + log('OAuthCubit: Starting OAuth login for provider: $provider'); + emit(const OAuthState.loading()); + log('OAuthCubit: Emitted loading state'); + + final tokenModel = await authRepository.getOAuthLogin(provider: provider, accessToken: accessToken, refreshToken: refreshToken); + log('OAuthCubit: Got token model: ${tokenModel.token}'); + emit(OAuthState.loaded(token: tokenModel.token)); + log('OAuthCubit: Emitted loaded state'); + } catch (e) { + log('OAuthCubit: Error occurred: $e'); + emit(OAuthState.error(errorMessage: e.toString())); + log('OAuthCubit: Emitted error state'); + } + } + Future getOAuthToken({ required String provider, required String accessToken, @@ -22,6 +40,7 @@ class OAuthCubit extends Cubit { required int height, }) async { try { + emit(const OAuthState.loading()); final token = await authRepository.getOAuthToken( provider: provider, accessToken: accessToken, diff --git a/lib/features/auth/presentation/pages/auth_screen.dart b/lib/features/auth/presentation/pages/auth_screen.dart index 13713922..fbdbd809 100644 --- a/lib/features/auth/presentation/pages/auth_screen.dart +++ b/lib/features/auth/presentation/pages/auth_screen.dart @@ -14,6 +14,10 @@ import 'package:eatvio_app/core/widgets/custom_divider.dart'; import 'package:eatvio_app/core/widgets/custom_text_form_field.dart'; import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart'; import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_state.dart'; +import 'package:eatvio_app/features/auth/presentation/bloc/oauth_cubit/oauth_cubit.dart'; +import 'package:eatvio_app/features/auth/presentation/bloc/oauth_cubit/oauth_state.dart'; +import 'package:eatvio_app/features/auth/entities/auth_result_entity.dart'; +import 'package:eatvio_app/core/di/cubit_registrations.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart'; import 'package:eatvio_app/features/dev_tools/presentation/dev_tools_screen.dart'; @@ -391,16 +395,29 @@ class _SocialButton extends StatelessWidget { void _onVkPressed() async { try { + log('VK button pressed'); final context = AppRouter.navigatorKey.currentContext; - if (context == null) return; - - final authResult = await SocialAuthFacade.signInWithVkAndFetchProfile(context: context); - if (authResult == null) { + if (context == null) { + log('Context is null'); return; } - AppRouter.navigateToRegisterViaSocial(authResult: authResult, provider: 'vk'); + + log('Getting VK auth result'); + final authResult = await SocialAuthFacade.signInWithVkAndFetchProfile(context: context); + if (authResult == null) { + log('VK auth result is null'); + return; + } + + log('VK auth result received, trying OAuth login'); + // Пробуем сначала авторизоваться через новый API + if (context.mounted) { + await _tryOAuthLogin(context, authResult, 'vk'); + } else { + log('Context is not mounted'); + } } catch (e) { - log(e.toString()); + log('VK button error: $e'); } } @@ -413,7 +430,11 @@ void _onOkPressed() async { if (authResult == null) { return; } - AppRouter.navigateToRegisterViaSocial(authResult: authResult, provider: 'ok'); + + // Пробуем сначала авторизоваться через новый API + if (context.mounted) { + await _tryOAuthLogin(context, authResult, 'ok'); + } } catch (e) { log(e.toString()); } @@ -428,7 +449,11 @@ void _onMailRuPressed() async { if (authResult == null) { return; } - AppRouter.navigateToRegisterViaSocial(authResult: authResult, provider: 'mailru'); + + // Пробуем сначала авторизоваться через новый API + if (context.mounted) { + await _tryOAuthLogin(context, authResult, 'mailru'); + } } catch (e) { log(e.toString()); } @@ -436,14 +461,61 @@ void _onMailRuPressed() async { void _onYandexPressed() async { try { + final context = AppRouter.navigatorKey.currentContext; + if (context == null) return; + final authResult = await SocialAuthFacade.signInWithYandex(); if (authResult == null) return; - AppRouter.navigateToRegisterViaSocial(authResult: authResult, provider: 'yandex'); + + // Пробуем сначала авторизоваться через новый API + if (context.mounted) { + await _tryOAuthLogin(context, authResult, 'yandex'); + } } catch (e) { log(e.toString()); } } +/// Пробует авторизоваться через новый OAuth login API +/// Если получает ошибку (включая 422 - пользователь не найден), переходит к регистрации +Future _tryOAuthLogin(BuildContext context, AuthResultEntity authResult, String provider) async { + log('Starting OAuth login for provider: $provider'); + + try { + // Создаем OAuth cubit + final oauthCubit = CubitRegistrations.sl(); + StreamSubscription? subscription; + + // Слушаем изменения состояния + subscription = oauthCubit.stream.listen((state) { + log('OAuth state changed: ${state.runtimeType}'); + + if (state is OAuthStateLoaded) { + // Успешная авторизация - переходим на главный экран + log('OAuth login successful, navigating to main screen'); + subscription?.cancel(); + _reconnectAfterAuth(); + } else if (state is OAuthStateError) { + // Ошибка авторизации (включая 422 - пользователь не найден) - переходим к регистрации + log('OAuth login failed: ${state.errorMessage}'); + subscription?.cancel(); + AppRouter.navigateToRegisterViaSocial(authResult: authResult, provider: provider); + } + }); + + // Пробуем авторизоваться + log('Calling getOAuthLogin with provider: $provider'); + await oauthCubit.getOAuthLogin(provider: provider, accessToken: authResult.accessToken, refreshToken: authResult.refreshToken ?? ''); + + // Ждем немного, чтобы состояние успело обновиться + await Future.delayed(const Duration(milliseconds: 100)); + } catch (e) { + // В случае любой ошибки (включая 422 - пользователь не найден) переходим к регистрации + log('OAuth login error in catch block: $e'); + AppRouter.navigateToRegisterViaSocial(authResult: authResult, provider: provider); + } +} + Future _reconnectAfterAuth() async { final context = AppRouter.navigatorKey.currentContext; if (context == null) return; diff --git a/lib/features/auth/presentation/pages/register_via_social_screen.dart b/lib/features/auth/presentation/pages/register_via_social_screen.dart index 22fa5d18..a0175726 100644 --- a/lib/features/auth/presentation/pages/register_via_social_screen.dart +++ b/lib/features/auth/presentation/pages/register_via_social_screen.dart @@ -30,8 +30,8 @@ class RegisterViaSocialScreen extends StatefulWidget { } class _RegisterViaSocialScreenState extends State { - final maskFormatter = MaskTextInputFormatter(mask: '+7 (###) ### ## ##', filter: {"#": RegExp(r'[0-9]')}); - final maskFormatterForBirthday = MaskTextInputFormatter(mask: '##.##.####', filter: {"#": RegExp(r'[0-9]')}); + final maskFormatter = MaskTextInputFormatter(mask: '+7 (###) ### ## ##', filter: {"#": RegExp(r'[0-9]')}, type: MaskAutoCompletionType.lazy); + final maskFormatterForBirthday = MaskTextInputFormatter(mask: '##.##.####', filter: {"#": RegExp(r'[0-9]')}, type: MaskAutoCompletionType.lazy); // Контроллеры для полей ввода final firstNameController = TextEditingController(); @@ -99,10 +99,16 @@ class _RegisterViaSocialScreenState extends State { emailController.text = authResult.email!; } if (authResult.phone != null) { - phoneController.text = authResult.phone!; + final cleanPhone = authResult.phone!.replaceAll(RegExp(r'[^\d]'), ''); + final normalizedPhone = cleanPhone.startsWith('8') ? '7${cleanPhone.substring(1)}' : cleanPhone; + final phoneWithCountryCode = normalizedPhone.startsWith('7') ? normalizedPhone : '7$normalizedPhone'; + final formattedPhone = maskFormatter.maskText('+$phoneWithCountryCode'); + phoneController.text = formattedPhone; } if (authResult.birthDate != null) { - dateOfBirthController.text = _formatBirthDate(authResult.birthDate!); + final formattedDate = _formatBirthDate(authResult.birthDate!); + final maskedDate = maskFormatterForBirthday.maskText(formattedDate); + dateOfBirthController.text = maskedDate; } if (authResult.gender != null) { isMaleSelected = authResult.gender == GenderEnum.male; @@ -265,7 +271,10 @@ class _RegisterViaSocialScreenState extends State { controller: phoneController, keyboardType: TextInputType.number, inputFormatters: [maskFormatter], - onChanged: (newValue) {}, + onChanged: (newValue) { + // Дополнительная обработка изменений номера телефона + // Это поможет маске корректно работать при редактировании + }, ), const SizedBox(height: 12), Row( @@ -502,6 +511,9 @@ class _RegisterViaSocialScreenState extends State { return; } + // Очищаем номер телефона от маски для отправки + final cleanPhone = phoneController.text.replaceAll(RegExp(r'[^\d+]'), ''); + // Вызываем OAuth регистрацию context.read().getOAuthToken( provider: widget.provider ?? 'social', @@ -511,7 +523,7 @@ class _RegisterViaSocialScreenState extends State { firstName: firstNameController.text, lastName: lastNameController.text, email: emailController.text, - phone: phoneController.text, + phone: cleanPhone, dateOfBirth: dateOfBirthController.text, gender: isMaleSelected ? 'М' : 'Ж', weight: weight, diff --git a/lib/features/auth/repositories/auth_repository.dart b/lib/features/auth/repositories/auth_repository.dart index bddebce1..1f66c039 100644 --- a/lib/features/auth/repositories/auth_repository.dart +++ b/lib/features/auth/repositories/auth_repository.dart @@ -1,5 +1,6 @@ import 'package:eatvio_app/features/auth/models/credentials_model.dart'; import 'package:eatvio_app/features/auth/models/oauth_model.dart'; +import 'package:eatvio_app/features/auth/models/oauth_login_request.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'; @@ -29,6 +30,16 @@ class AuthRepository { } } + Future getOAuthLogin({required String provider, required String accessToken, required String refreshToken}) async { + try { + return await authService.getOAuthLogin( + body: OAuthLoginRequest(provider: provider, accessToken: accessToken, refreshToken: refreshToken), + ); + } catch (e) { + rethrow; + } + } + Future getOAuthToken({ required String provider, required String accessToken, @@ -45,21 +56,22 @@ class AuthRepository { }) async { try { return (await authService.getOAuthToken( - body: OAuthModel( - provider: provider, - accessToken: accessToken, - id: id, - refreshToken: refreshToken, - firstName: firstName, - lastName: lastName, - email: email, - phone: phone, - dateOfBirth: dateOfBirth, - gender: gender == 'М' ? 'male' : 'female', - weight: weight, - height: height, - ), - ))['token']! as String; + body: OAuthModel( + provider: provider, + accessToken: accessToken, + id: id, + refreshToken: refreshToken, + firstName: firstName, + lastName: lastName, + email: email, + phone: phone, + dateOfBirth: dateOfBirth, + gender: gender == 'М' ? 'male' : 'female', + weight: weight, + height: height, + ), + ))['token']! + as String; } catch (e) { rethrow; } diff --git a/lib/features/auth/services/auth_service.dart b/lib/features/auth/services/auth_service.dart index 3d8d43e2..02f8722a 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/oauth_model.dart'; +import 'package:eatvio_app/features/auth/models/oauth_login_request.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'; @@ -12,17 +13,14 @@ abstract class AuthService { factory AuthService(Dio dio, {String? baseUrl}) = _AuthService; @POST('/api/login') - Future getTokenByCredentials({ - @Body() required CredentialsModel credentials, - }); + Future getTokenByCredentials({@Body() required CredentialsModel credentials}); @POST('/api/social/login') - Future getTokenBySocial({ - @Body() required SocialExchangeRequest body, - }); + Future getTokenBySocial({@Body() required SocialExchangeRequest body}); @POST('/api/oauth/callback') - Future> getOAuthToken({ - @Body() required OAuthModel body, - }); + Future> getOAuthToken({@Body() required OAuthModel body}); + + @POST('/api/oauth/login') + Future getOAuthLogin({@Body() required OAuthLoginRequest body}); } From f573bb28f806b29b2991e21e969296a1153e5104 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Tue, 14 Oct 2025 19:25:26 +0300 Subject: [PATCH 68/78] feat(oauth): Issue-691 --- lib/core/app_router.dart | 3 +- .../pages/register_via_social_screen.dart | 54 +------------------ lib/features/auth/sdk/social_auth_facade.dart | 1 - lib/generated/l10n.dart | 7 ++- 4 files changed, 6 insertions(+), 59 deletions(-) diff --git a/lib/core/app_router.dart b/lib/core/app_router.dart index 5ce04746..d17019d2 100644 --- a/lib/core/app_router.dart +++ b/lib/core/app_router.dart @@ -53,7 +53,6 @@ class AppRouter { } static void navigateToRegisterViaSocial({required AuthResultEntity authResult, required String provider}) { - print('AppRouter: Navigating to register via social screen for provider: $provider'); - appRouter.goNamed('registerViaSocialScreen', extra: {'authResult': authResult, 'provider': provider}); + appRouter.pushNamed('registerViaSocialScreen', extra: {'authResult': authResult, 'provider': provider}); } } diff --git a/lib/features/auth/presentation/pages/register_via_social_screen.dart b/lib/features/auth/presentation/pages/register_via_social_screen.dart index b93f82d7..c803f687 100644 --- a/lib/features/auth/presentation/pages/register_via_social_screen.dart +++ b/lib/features/auth/presentation/pages/register_via_social_screen.dart @@ -79,17 +79,6 @@ class _RegisterViaSocialScreenState extends State { if (widget.authResult != null) { final authResult = widget.authResult!; - // Логируем полученные данные для отладки - print('AuthResult data:'); - print('firstName: ${authResult.firstName}'); - print('lastName: ${authResult.lastName}'); - print('email: ${authResult.email}'); - print('phone: ${authResult.phone}'); - print('birthDate: ${authResult.birthDate}'); - print('gender: ${authResult.gender}'); - print('provider: ${widget.provider}'); - - // Предзаполняем поля из AuthResultEntity if (authResult.firstName != null) { firstNameController.text = authResult.firstName!; } @@ -153,44 +142,6 @@ class _RegisterViaSocialScreenState extends State { return birthDate; } - String _getProviderName(String? provider) { - switch (provider) { - case 'vk': - return 'ВКонтакте'; - case 'yandex': - return 'Яндекс'; - case 'ok': - return 'Одноклассники'; - case 'mailru': - return 'Mail.ru'; - default: - return 'социальной сети'; - } - } - - String _getDataStatusMessage(AuthResultEntity authResult) { - final hasEmail = authResult.email != null && authResult.email!.isNotEmpty; - final hasPhone = authResult.phone != null && authResult.phone!.isNotEmpty; - final hasBirthDate = authResult.birthDate != null && authResult.birthDate!.isNotEmpty; - - if (hasEmail && hasPhone && hasBirthDate) { - return 'Все данные успешно получены и предзаполнены.'; - } else if (hasEmail && hasPhone) { - return 'Email и номер телефона получены. Дата рождения может быть недоступна из-за настроек приватности.'; - } else if (hasEmail) { - return 'Email получен. Номер телефона и дата рождения могут быть недоступны из-за настроек приватности.'; - } else if (hasPhone) { - return 'Номер телефона получен. Email и дата рождения могут быть недоступны из-за настроек приватности.'; - } else { - // Специальное сообщение для VK - if (widget.provider == 'vk') { - return 'Основные данные получены. Email запрашивается автоматически. Номер телефона получается через VK API. Если номер замаскирован, это означает, что пользователь ограничил доступ к нему в настройках приватности.'; - } else { - return 'Основные данные получены. Email, номер телефона и дата рождения могут быть недоступны из-за настроек приватности.'; - } - } - } - @override void dispose() { firstNameController.dispose(); @@ -208,15 +159,14 @@ class _RegisterViaSocialScreenState extends State { return BlocProvider( create: (context) => CubitRegistrations.sl(), child: Scaffold( - appBar: AppBar(toolbarHeight: 0), + appBar: AppBar(), body: BlocBuilder( builder: (context, state) { return SingleChildScrollView( - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.symmetric(horizontal: 16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const SizedBox(height: 24), const Center( child: Text( 'Регистрация', diff --git a/lib/features/auth/sdk/social_auth_facade.dart b/lib/features/auth/sdk/social_auth_facade.dart index 4cab0962..140da37b 100644 --- a/lib/features/auth/sdk/social_auth_facade.dart +++ b/lib/features/auth/sdk/social_auth_facade.dart @@ -119,7 +119,6 @@ class SocialAuthFacade { 'response_type': 'code', 'client_id': clientId, 'redirect_uri': redirectUri, - 'scope': 'login:info login:email', 'force_confirm': 'true', }).toString(); diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index 1055e664..0ccdeac3 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; From e073bd3f970e52835f1057fe1004a9946b7e3b68 Mon Sep 17 00:00:00 2001 From: misha Date: Wed, 15 Oct 2025 14:21:43 +0300 Subject: [PATCH 69/78] feat(socail): finished registration and ligin via social media --- lib/core/app_config.dart | 6 +- .../auth/entities/auth_result_entity.dart | 1 + .../auth/models/oauth_login_request.dart | 3 +- lib/features/auth/models/oauth_model.dart | 7 +- .../bloc/auth_cubit/auth_cubit.dart | 15 - .../bloc/oauth_cubit/oauth_cubit.dart | 23 +- .../auth/presentation/pages/auth_screen.dart | 275 +++++++++--------- .../pages/register_via_social_screen.dart | 5 +- .../auth/repositories/auth_repository.dart | 29 +- lib/features/auth/sdk/social_auth_facade.dart | 14 +- lib/features/auth/services/auth_service.dart | 8 +- lib/main.dart | 2 +- 12 files changed, 187 insertions(+), 201 deletions(-) diff --git a/lib/core/app_config.dart b/lib/core/app_config.dart index 03513645..45ad250b 100644 --- a/lib/core/app_config.dart +++ b/lib/core/app_config.dart @@ -15,10 +15,10 @@ import 'package:flutter_localizations/flutter_localizations.dart'; /// Конфигурация приложения class AppConfig { - AppConfig({required this.buildType}){ + AppConfig({required this.buildType}) { _baseUrl = switch (buildType) { AppBuildType.prod => EnvConfig.baseUrl, - AppBuildType.tests => EnvConfig.devBaseUrl, + AppBuildType.tests => EnvConfig.devBaseUrl, AppBuildType.dev => EnvConfig.devBaseUrl, }; } @@ -30,7 +30,7 @@ class AppConfig { void setBaseUrl(String baseUrls) { _baseUrl = baseUrls; } - + /// Список глобальных BlocProvider'ов List get globalBlocProviders => [ BlocProvider(create: (context) => sl()), diff --git a/lib/features/auth/entities/auth_result_entity.dart b/lib/features/auth/entities/auth_result_entity.dart index 2f296e5b..a8d08ff4 100644 --- a/lib/features/auth/entities/auth_result_entity.dart +++ b/lib/features/auth/entities/auth_result_entity.dart @@ -8,6 +8,7 @@ abstract class AuthResultEntity with _$AuthResultEntity { const factory AuthResultEntity({ required String accessToken, required String userId, + required String clientId, String? refreshToken, String? firstName, String? lastName, diff --git a/lib/features/auth/models/oauth_login_request.dart b/lib/features/auth/models/oauth_login_request.dart index 43da454b..97145a4d 100644 --- a/lib/features/auth/models/oauth_login_request.dart +++ b/lib/features/auth/models/oauth_login_request.dart @@ -6,9 +6,10 @@ part 'oauth_login_request.g.dart'; @freezed abstract class OAuthLoginRequest with _$OAuthLoginRequest { const factory OAuthLoginRequest({ - required String provider, + @JsonKey(name: 'client_id') required String clientId, @JsonKey(name: 'access_token') required String accessToken, @JsonKey(name: 'refresh_token') required String refreshToken, + required String provider, }) = _OAuthLoginRequest; factory OAuthLoginRequest.fromJson(Map json) => _$OAuthLoginRequestFromJson(json); diff --git a/lib/features/auth/models/oauth_model.dart b/lib/features/auth/models/oauth_model.dart index aff79b5f..0156e7aa 100644 --- a/lib/features/auth/models/oauth_model.dart +++ b/lib/features/auth/models/oauth_model.dart @@ -6,15 +6,16 @@ part 'oauth_model.g.dart'; @freezed abstract class OAuthModel with _$OAuthModel { const factory OAuthModel({ - required String provider, - required String id, @JsonKey(name: 'access_token') required String accessToken, @JsonKey(name: 'refresh_token') required String refreshToken, @JsonKey(name: 'first_name') required String firstName, @JsonKey(name: 'last_name') required String lastName, + @JsonKey(name: 'date_of_birth') required String dateOfBirth, + @JsonKey(name: 'client_id') required String clientId, + required String provider, + required String id, required String email, required String phone, - @JsonKey(name: 'date_of_birth') required String dateOfBirth, required String gender, required int weight, required int height, 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 7b58a4b0..6f1a59f5 100644 --- a/lib/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart +++ b/lib/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart @@ -25,19 +25,4 @@ 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/bloc/oauth_cubit/oauth_cubit.dart b/lib/features/auth/presentation/bloc/oauth_cubit/oauth_cubit.dart index 7d45b4b1..394abc4d 100644 --- a/lib/features/auth/presentation/bloc/oauth_cubit/oauth_cubit.dart +++ b/lib/features/auth/presentation/bloc/oauth_cubit/oauth_cubit.dart @@ -1,4 +1,5 @@ import 'dart:developer'; +import 'package:eatvio_app/core/token_storage.dart'; import 'package:eatvio_app/features/auth/presentation/bloc/oauth_cubit/oauth_state.dart'; import 'package:eatvio_app/features/auth/repositories/auth_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -8,24 +9,27 @@ class OAuthCubit extends Cubit { OAuthCubit({required this.authRepository}) : super(const OAuthState()); - Future getOAuthLogin({required String provider, required String accessToken, required String refreshToken}) async { + Future signInViaSocialMedia({required String provider, required String accessToken, required String refreshToken, required String clientId}) async { try { - log('OAuthCubit: Starting OAuth login for provider: $provider'); emit(const OAuthState.loading()); - log('OAuthCubit: Emitted loading state'); - final tokenModel = await authRepository.getOAuthLogin(provider: provider, accessToken: accessToken, refreshToken: refreshToken); + final tokenModel = await authRepository.signInViaSocialMedia( + provider: provider, + accessToken: accessToken, + refreshToken: refreshToken, + clientId: clientId, + ); + log('OAuthCubit: Got token model: ${tokenModel.token}'); + TokenStorage.saveTokenInStorage(tokenModel.token); emit(OAuthState.loaded(token: tokenModel.token)); - log('OAuthCubit: Emitted loaded state'); } catch (e) { log('OAuthCubit: Error occurred: $e'); emit(OAuthState.error(errorMessage: e.toString())); - log('OAuthCubit: Emitted error state'); } } - Future getOAuthToken({ + Future signUpViaSocialMedia({ required String provider, required String accessToken, required String id, @@ -38,10 +42,11 @@ class OAuthCubit extends Cubit { required String gender, required int weight, required int height, + required String clientId, }) async { try { emit(const OAuthState.loading()); - final token = await authRepository.getOAuthToken( + final token = await authRepository.signUpViaSocialMedia( provider: provider, accessToken: accessToken, id: id, @@ -54,7 +59,9 @@ class OAuthCubit extends Cubit { gender: gender, weight: weight, height: height, + clientId: clientId, ); + TokenStorage.saveTokenInStorage(token); emit(OAuthState.loaded(token: token)); } catch (e) { emit(OAuthState.error(errorMessage: e.toString())); diff --git a/lib/features/auth/presentation/pages/auth_screen.dart b/lib/features/auth/presentation/pages/auth_screen.dart index c21e77ea..f7484a87 100644 --- a/lib/features/auth/presentation/pages/auth_screen.dart +++ b/lib/features/auth/presentation/pages/auth_screen.dart @@ -1,5 +1,3 @@ -// ignore_for_file: unused_element, duplicate_ignore - import 'dart:async'; import 'dart:developer'; import 'package:eatvio_app/app_keys.dart'; @@ -95,6 +93,138 @@ class _AuthScreenState extends State { AppRouter.navigatorKey.currentContext?.go('/feed-screen', extra: {'needToInitializeWebSocket': false}); } + void _onVkPressed() async { + try { + log('VK button pressed'); + final context = AppRouter.navigatorKey.currentContext; + if (context == null) { + log('Context is null'); + return; + } + + log('Getting VK auth result'); + final authResult = await SocialAuthFacade.signInWithVkAndFetchProfile(context: context); + if (authResult == null) { + log('VK auth result is null'); + return; + } + + log('VK auth result received, trying OAuth login'); + if (context.mounted) { + await _tryOAuthLogin(context, authResult, 'vk'); + } else { + log('Context is not mounted'); + } + } catch (e) { + log('VK button error: $e'); + } + } + + void _onOkPressed() async { + try { + final context = AppRouter.navigatorKey.currentContext; + if (context == null) return; + + final authResult = await SocialAuthFacade.signInWithOkAndFetchProfile(); + if (authResult == null) { + return; + } + + if (context.mounted) { + await _tryOAuthLogin(context, authResult, 'vk'); + } + } catch (e) { + log(e.toString()); + } + } + + void _onMailRuPressed() async { + try { + final context = AppRouter.navigatorKey.currentContext; + if (context == null) return; + + final authResult = await SocialAuthFacade.signInWithMailRuAndFetchProfile(); + if (authResult == null) { + return; + } + + if (context.mounted) { + await _tryOAuthLogin(context, authResult, 'vk'); + } + } catch (e) { + log(e.toString()); + } + } + + void _onYandexPressed() async { + try { + final context = AppRouter.navigatorKey.currentContext; + if (context == null) return; + + final authResult = await SocialAuthFacade.signInWithYandex(); + if (authResult == null) return; + + if (context.mounted) { + await _tryOAuthLogin(context, authResult, 'yandex'); + } + } catch (e) { + log(e.toString()); + } + } + + Future _tryOAuthLogin(BuildContext context, AuthResultEntity authResult, String provider) async { + log('Starting OAuth login for provider: $provider'); + + try { + final oauthCubit = CubitRegistrations.sl(); + StreamSubscription? subscription; + + subscription = oauthCubit.stream.listen((state) { + log('OAuth state changed: ${state.runtimeType}'); + + if (state is OAuthStateLoaded) { + log('OAuth login successful, navigating to main screen'); + subscription?.cancel(); + _reconnectAfterAuth(); + } else if (state is OAuthStateError) { + log('OAuth login failed: ${state.errorMessage}'); + subscription?.cancel(); + AppRouter.navigateToRegisterViaSocial(authResult: authResult, provider: provider); + } + }); + + log('Calling getOAuthLogin with provider: $provider'); + await oauthCubit.signInViaSocialMedia( + provider: provider, + accessToken: authResult.accessToken, + refreshToken: authResult.refreshToken ?? '', + clientId: authResult.clientId, + ); + } catch (e) { + log('OAuth login error in catch block: $e'); + AppRouter.navigateToRegisterViaSocial(authResult: authResult, provider: provider); + } + } + + 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 (e) { + log(e.toString()); + } + AppRouter.navigatorKey.currentContext?.go('/feed-screen', extra: {'needToInitializeWebSocket': false}); + } + @override Widget build(BuildContext context) { var shortestSide = MediaQuery.sizeOf(context).shortestSide; @@ -129,7 +259,6 @@ class _AuthScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Social auth buttons at the top (outlined) const Center(child: Text('Авторизация через')), const SizedBox(height: 12), Row( @@ -405,143 +534,3 @@ class _SocialButton extends StatelessWidget { ); } } - -void _onVkPressed() async { - try { - log('VK button pressed'); - final context = AppRouter.navigatorKey.currentContext; - if (context == null) { - log('Context is null'); - return; - } - - log('Getting VK auth result'); - final authResult = await SocialAuthFacade.signInWithVkAndFetchProfile(context: context); - if (authResult == null) { - log('VK auth result is null'); - return; - } - - log('VK auth result received, trying OAuth login'); - // Пробуем сначала авторизоваться через новый API - if (context.mounted) { - await _tryOAuthLogin(context, authResult, 'vk'); - } else { - log('Context is not mounted'); - } - } catch (e) { - log('VK button error: $e'); - } -} - -void _onOkPressed() async { - try { - final context = AppRouter.navigatorKey.currentContext; - if (context == null) return; - - final authResult = await SocialAuthFacade.signInWithOkAndFetchProfile(); - if (authResult == null) { - return; - } - - // Пробуем сначала авторизоваться через новый API - if (context.mounted) { - await _tryOAuthLogin(context, authResult, 'ok'); - } - } catch (e) { - log(e.toString()); - } -} - -void _onMailRuPressed() async { - try { - final context = AppRouter.navigatorKey.currentContext; - if (context == null) return; - - final authResult = await SocialAuthFacade.signInWithMailRuAndFetchProfile(); - if (authResult == null) { - return; - } - - // Пробуем сначала авторизоваться через новый API - if (context.mounted) { - await _tryOAuthLogin(context, authResult, 'mailru'); - } - } catch (e) { - log(e.toString()); - } -} - -void _onYandexPressed() async { - try { - final context = AppRouter.navigatorKey.currentContext; - if (context == null) return; - - final authResult = await SocialAuthFacade.signInWithYandex(); - if (authResult == null) return; - - // Пробуем сначала авторизоваться через новый API - if (context.mounted) { - await _tryOAuthLogin(context, authResult, 'yandex'); - } - } catch (e) { - log(e.toString()); - } -} - -/// Пробует авторизоваться через новый OAuth login API -/// Если получает ошибку (включая 422 - пользователь не найден), переходит к регистрации -Future _tryOAuthLogin(BuildContext context, AuthResultEntity authResult, String provider) async { - log('Starting OAuth login for provider: $provider'); - - try { - // Создаем OAuth cubit - final oauthCubit = CubitRegistrations.sl(); - StreamSubscription? subscription; - - // Слушаем изменения состояния - subscription = oauthCubit.stream.listen((state) { - log('OAuth state changed: ${state.runtimeType}'); - - if (state is OAuthStateLoaded) { - // Успешная авторизация - переходим на главный экран - log('OAuth login successful, navigating to main screen'); - subscription?.cancel(); - _reconnectAfterAuth(); - } else if (state is OAuthStateError) { - // Ошибка авторизации (включая 422 - пользователь не найден) - переходим к регистрации - log('OAuth login failed: ${state.errorMessage}'); - subscription?.cancel(); - AppRouter.navigateToRegisterViaSocial(authResult: authResult, provider: provider); - } - }); - - // Пробуем авторизоваться - log('Calling getOAuthLogin with provider: $provider'); - await oauthCubit.getOAuthLogin(provider: provider, accessToken: authResult.accessToken, refreshToken: authResult.refreshToken ?? ''); - - // Ждем немного, чтобы состояние успело обновиться - await Future.delayed(const Duration(milliseconds: 100)); - } catch (e) { - // В случае любой ошибки (включая 422 - пользователь не найден) переходим к регистрации - log('OAuth login error in catch block: $e'); - AppRouter.navigateToRegisterViaSocial(authResult: authResult, provider: provider); - } -} - -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/presentation/pages/register_via_social_screen.dart b/lib/features/auth/presentation/pages/register_via_social_screen.dart index c803f687..84a6f2ae 100644 --- a/lib/features/auth/presentation/pages/register_via_social_screen.dart +++ b/lib/features/auth/presentation/pages/register_via_social_screen.dart @@ -439,6 +439,7 @@ class _RegisterViaSocialScreenState extends State { } void _handleRegistration(BuildContext context) { + final clientID = widget.authResult?.clientId; // Валидация полей if (firstNameController.text.isEmpty || lastNameController.text.isEmpty || @@ -465,8 +466,9 @@ class _RegisterViaSocialScreenState extends State { // Очищаем номер телефона от маски для отправки final cleanPhone = phoneController.text.replaceAll(RegExp(r'[^\d+]'), ''); + if (clientID == null) return; // Вызываем OAuth регистрацию - context.read().getOAuthToken( + context.read().signUpViaSocialMedia( provider: widget.provider ?? 'social', accessToken: widget.authResult?.accessToken ?? 'dummy_token', id: widget.authResult?.userId ?? 'dummy_id', @@ -479,6 +481,7 @@ class _RegisterViaSocialScreenState extends State { gender: isMaleSelected ? 'М' : 'Ж', weight: weight, height: height, + clientId: clientID, ); } } diff --git a/lib/features/auth/repositories/auth_repository.dart b/lib/features/auth/repositories/auth_repository.dart index 1f66c039..a65f309c 100644 --- a/lib/features/auth/repositories/auth_repository.dart +++ b/lib/features/auth/repositories/auth_repository.dart @@ -3,7 +3,6 @@ import 'package:eatvio_app/features/auth/models/oauth_model.dart'; import 'package:eatvio_app/features/auth/models/oauth_login_request.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; @@ -20,27 +19,23 @@ class AuthRepository { } } - Future getTokenBySocial({required String provider, required String accessToken}) async { + + Future signInViaSocialMedia({ + required String provider, + required String accessToken, + required String refreshToken, + required String clientId, + }) async { try { - return await authService.getTokenBySocial( - body: SocialExchangeRequest(provider: provider, accessToken: accessToken), + return await authService.signInViaSocialMedia( + body: OAuthLoginRequest(provider: provider, accessToken: accessToken, refreshToken: refreshToken, clientId: clientId), ); } catch (e) { rethrow; } } - Future getOAuthLogin({required String provider, required String accessToken, required String refreshToken}) async { - try { - return await authService.getOAuthLogin( - body: OAuthLoginRequest(provider: provider, accessToken: accessToken, refreshToken: refreshToken), - ); - } catch (e) { - rethrow; - } - } - - Future getOAuthToken({ + Future signUpViaSocialMedia({ required String provider, required String accessToken, required String id, @@ -53,9 +48,10 @@ class AuthRepository { required String gender, required int weight, required int height, + required String clientId, }) async { try { - return (await authService.getOAuthToken( + return (await authService.signUpViaSocialMedia( body: OAuthModel( provider: provider, accessToken: accessToken, @@ -69,6 +65,7 @@ class AuthRepository { gender: gender == 'М' ? 'male' : 'female', weight: weight, height: height, + clientId: clientId, ), ))['token']! as String; diff --git a/lib/features/auth/sdk/social_auth_facade.dart b/lib/features/auth/sdk/social_auth_facade.dart index b3543700..4d2e9745 100644 --- a/lib/features/auth/sdk/social_auth_facade.dart +++ b/lib/features/auth/sdk/social_auth_facade.dart @@ -8,6 +8,7 @@ import 'package:eatvio_app/features/auth/models/yandex_profile_model.dart'; import 'package:flutter/material.dart'; import 'package:eatvio_app/core/app_router.dart'; import 'package:eatvio_app/core/env_config.dart'; +import 'package:go_router/go_router.dart'; import 'package:http/http.dart' as http; import 'package:vk_id/vk_id.dart'; import 'package:webview_flutter/webview_flutter.dart'; @@ -38,7 +39,7 @@ class SocialAuthFacade { if (redirectParams == null) { return null; } - return await _parseVKAuthResult(redirectParams: redirectParams, controller: controller, codeVerifier: codeVerifier); + return await _parseVKAuthResult(redirectParams: redirectParams, controller: controller, codeVerifier: codeVerifier, clientId: clientIdStr); } catch (e) { return null; } @@ -48,6 +49,7 @@ class SocialAuthFacade { required Map redirectParams, required VkIDController controller, required String codeVerifier, + required String clientId, }) async { final code = redirectParams['code']; final deviceId = redirectParams['device_id']; @@ -88,6 +90,7 @@ class SocialAuthFacade { phone: profile.phone, gender: GenderEnum.fromVkGender(profile.parsedSex), birthDate: profile.bDayDt?.toString(), + clientId: clientId, ); } @@ -151,6 +154,7 @@ class SocialAuthFacade { phone: yandexProfileData.defaultPhone.number, gender: GenderEnum.fromString(yandexProfileData.sex), birthDate: yandexProfileData.birthday, + clientId: clientId, ); } catch (e) { log(e.toString()); @@ -188,7 +192,7 @@ class SocialAuthFacade { try { final controller = VkIDController(clID: int.parse(clientIdStr)); - final authUri = controller.generateAuthorizeLinkWithCodeVerifier(provider: provider, scopes: [VkScope.phone, VkScope.email,]); + final authUri = controller.generateAuthorizeLinkWithCodeVerifier(provider: provider, scopes: [VkScope.phone, VkScope.email]); final codeVerifier = authUri.key; final authUrl = authUri.value.toString(); final redirectScheme = 'vk$clientIdStr://'; @@ -199,7 +203,7 @@ class SocialAuthFacade { final deviceId = redirectParams['device_id']; if (code == null || deviceId == null) return null; - return await _parseVKAuthResult(redirectParams: redirectParams, controller: controller, codeVerifier: codeVerifier); + return await _parseVKAuthResult(redirectParams: redirectParams, controller: controller, codeVerifier: codeVerifier, clientId: clientIdStr); } catch (e) { return null; } @@ -230,7 +234,9 @@ class SocialAuthFacade { } } } - Navigator.of(context, rootNavigator: true).pop(params); + if (GoRouter.of(context).canPop()) { + GoRouter.of(context).pop(params); + } return NavigationDecision.prevent; } return NavigationDecision.navigate; diff --git a/lib/features/auth/services/auth_service.dart b/lib/features/auth/services/auth_service.dart index 02f8722a..41d35da7 100644 --- a/lib/features/auth/services/auth_service.dart +++ b/lib/features/auth/services/auth_service.dart @@ -3,7 +3,6 @@ import 'package:eatvio_app/features/auth/models/credentials_model.dart'; import 'package:eatvio_app/features/auth/models/oauth_model.dart'; import 'package:eatvio_app/features/auth/models/oauth_login_request.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'; @@ -15,12 +14,9 @@ abstract class AuthService { @POST('/api/login') Future getTokenByCredentials({@Body() required CredentialsModel credentials}); - @POST('/api/social/login') - Future getTokenBySocial({@Body() required SocialExchangeRequest body}); - @POST('/api/oauth/callback') - Future> getOAuthToken({@Body() required OAuthModel body}); + Future> signUpViaSocialMedia({@Body() required OAuthModel body}); @POST('/api/oauth/login') - Future getOAuthLogin({@Body() required OAuthLoginRequest body}); + Future signInViaSocialMedia({@Body() required OAuthLoginRequest body}); } diff --git a/lib/main.dart b/lib/main.dart index af6db638..b6171864 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -9,7 +9,7 @@ import 'package:flutter_portal/flutter_portal.dart'; import 'package:keyboard_dismisser/keyboard_dismisser.dart'; Future main() async { - await AppInitializer.initialize(buildType: AppBuildType.prod); + await AppInitializer.initialize(buildType: AppBuildType.dev); await AppInitializer.runAppWithSentry(() => runApp(const MyApp())); } From b242eaa027bf998730e38b987fac2b3c44002a5d Mon Sep 17 00:00:00 2001 From: misha Date: Wed, 15 Oct 2025 14:22:45 +0300 Subject: [PATCH 70/78] fix: fixed base url --- lib/main.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/main.dart b/lib/main.dart index b6171864..af6db638 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -9,7 +9,7 @@ import 'package:flutter_portal/flutter_portal.dart'; import 'package:keyboard_dismisser/keyboard_dismisser.dart'; Future main() async { - await AppInitializer.initialize(buildType: AppBuildType.dev); + await AppInitializer.initialize(buildType: AppBuildType.prod); await AppInitializer.runAppWithSentry(() => runApp(const MyApp())); } From e865a11d149a688eea8143082d06c45629408ce5 Mon Sep 17 00:00:00 2001 From: misha Date: Wed, 15 Oct 2025 14:25:27 +0300 Subject: [PATCH 71/78] fix: fixed proj version --- ios/Runner.xcodeproj/project.pbxproj | 6 +++--- ios/Runner/Info.plist | 2 +- lib/generated/l10n.dart | 7 ++++--- pubspec.yaml | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 8e9edd22..2fa79910 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 = 74; + CURRENT_PROJECT_VERSION = 79; 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 = 74; + CURRENT_PROJECT_VERSION = 79; 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 = 74; + CURRENT_PROJECT_VERSION = 79; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 3275dbdb..2b41fbe1 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -21,7 +21,7 @@ CFBundleSignature ???? CFBundleVersion - 74 + 79 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index 0ccdeac3..1055e664 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -28,9 +28,10 @@ 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; diff --git a/pubspec.yaml b/pubspec.yaml index 0891053c..1a4b1db0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: eatvio_app description: A new Flutter project. -version: 1.2.14+74 +version: 1.3.0+79 publish_to: 'none' environment: From feee814bbb8630bc148a0b44a031da99c1736e37 Mon Sep 17 00:00:00 2001 From: misha Date: Wed, 15 Oct 2025 14:43:21 +0300 Subject: [PATCH 72/78] pod repod update for ci cd --- .gitea/workflows/test-flight-cd.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitea/workflows/test-flight-cd.yaml b/.gitea/workflows/test-flight-cd.yaml index c9649f0e..e9d5163b 100644 --- a/.gitea/workflows/test-flight-cd.yaml +++ b/.gitea/workflows/test-flight-cd.yaml @@ -61,6 +61,7 @@ jobs: # Configure Flutter for release mode flutter build ios --config-only --release cd ios + pod repo update pod update fastlane beta env: From 05bf90a3cf11d3abeaab7da844507a8177ee641d Mon Sep 17 00:00:00 2001 From: misha Date: Wed, 15 Oct 2025 14:47:46 +0300 Subject: [PATCH 73/78] fixed pipline --- .gitea/workflows/test-flight-cd.yaml | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/.gitea/workflows/test-flight-cd.yaml b/.gitea/workflows/test-flight-cd.yaml index e9d5163b..4736a47f 100644 --- a/.gitea/workflows/test-flight-cd.yaml +++ b/.gitea/workflows/test-flight-cd.yaml @@ -1,4 +1,4 @@ -name: "Build Test Flight" +name: 'Build Test Flight' on: pull_request: branches: @@ -10,7 +10,7 @@ jobs: steps: - name: Checkout uses: actions/checkout@v2 - + - name: Checkout env repository uses: actions/checkout@v2 with: @@ -39,8 +39,8 @@ jobs: run: | security unlock-keychain -p "$KEYCHAIN_PASSWORD" /Users/gitea_runner_ci/Library/Keychains/login.keychain-db env: - KEYCHAIN_PASSWORD: ${{ vars.IOS_RUNNER_CI_PASSWORD }} - + KEYCHAIN_PASSWORD: ${{ vars.IOS_RUNNER_CI_PASSWORD }} + - name: Setup API key run: | echo "${API_APPSTORE_KEY}" | tr -d '\n' | base64 --decode > ios/AuthKey.p8 @@ -58,18 +58,19 @@ jobs: flutter pub get flutter pub upgrade flutter pub run build_runner build --delete-conflicting-outputs - # Configure Flutter for release mode - flutter build ios --config-only --release cd ios pod repo update pod update + # Configure Flutter for release mode + flutter build ios --config-only --release + fastlane beta env: LANG: en_US.UTF-8 LC_ALL: en_US.UTF-8 - MATCH_PASSWORD: ${{ vars.IOS_RUNNER_CI_PASSWORD }} - FASTLANE_PASSWORD: ${{ vars.FASTLANE_PASSWORD }} + MATCH_PASSWORD: ${{ vars.IOS_RUNNER_CI_PASSWORD }} + FASTLANE_PASSWORD: ${{ vars.FASTLANE_PASSWORD }} MATCH_GIT_URL: http://gitea.office.local:3000/mso7nueh/fastlane FASTLANE_USER: eatvio2023@icloud.com - FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: ${{ vars.FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD }} + FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: ${{ vars.FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD }} API_APPSTORE_KEY: ${{ vars.API_APPSTORE_KEY }} From fa3cf297a3c45114ef76e4559594d9876538ceea Mon Sep 17 00:00:00 2001 From: Anatoliy Poloz Date: Thu, 16 Oct 2025 11:01:37 +0300 Subject: [PATCH 74/78] doc(project): project brief docs added --- .kilocode/rules/memory-bank-instructions.md | 167 +++++++++ .kilocode/rules/memory-bank/architecture.md | 374 ++++++++++++++++++++ .kilocode/rules/memory-bank/brief.md | 35 ++ .kilocode/rules/memory-bank/context.md | 29 ++ .kilocode/rules/memory-bank/product.md | 73 ++++ .kilocode/rules/memory-bank/tech.md | 152 ++++++++ 6 files changed, 830 insertions(+) create mode 100644 .kilocode/rules/memory-bank-instructions.md create mode 100644 .kilocode/rules/memory-bank/architecture.md create mode 100644 .kilocode/rules/memory-bank/brief.md create mode 100644 .kilocode/rules/memory-bank/context.md create mode 100644 .kilocode/rules/memory-bank/product.md create mode 100644 .kilocode/rules/memory-bank/tech.md diff --git a/.kilocode/rules/memory-bank-instructions.md b/.kilocode/rules/memory-bank-instructions.md new file mode 100644 index 00000000..6482c0b3 --- /dev/null +++ b/.kilocode/rules/memory-bank-instructions.md @@ -0,0 +1,167 @@ +# Memory Bank + +I am an expert software engineer with a unique characteristic: my memory resets completely between sessions. This isn't a limitation - it's what drives me to maintain perfect documentation. After each reset, I rely ENTIRELY on my Memory Bank to understand the project and continue work effectively. I MUST read ALL memory bank files at the start of EVERY task - this is not optional. The memory bank files are located in `.kilocode/rules/memory-bank` folder. + +When I start a task, I will include `[Memory Bank: Active]` at the beginning of my response if I successfully read the memory bank files, or `[Memory Bank: Missing]` if the folder doesn't exist or is empty. If memory bank is missing, I will warn the user about potential issues and suggest initialization. + +## Memory Bank Structure + +The Memory Bank consists of core files and optional context files, all in Markdown format. + +### Core Files (Required) +1. `brief.md` + This file is created and maintained manually by the developer. Don't edit this file directly but suggest to user to update it if it can be improved. + - Foundation document that shapes all other files + - Created at project start if it doesn't exist + - Defines core requirements and goals + - Source of truth for project scope + +2. `product.md` + - Why this project exists + - Problems it solves + - How it should work + - User experience goals + +3. `context.md` + This file should be short and factual, not creative or speculative. + - Current work focus + - Recent changes + - Next steps + +4. `architecture.md` + - System architecture + - Source Code paths + - Key technical decisions + - Design patterns in use + - Component relationships + - Critical implementation paths + +5. `tech.md` + - Technologies used + - Development setup + - Technical constraints + - Dependencies + - Tool usage patterns + +### Additional Files +Create additional files/folders within memory-bank/ when they help organize: +- `tasks.md` - Documentation of repetitive tasks and their workflows +- Complex feature documentation +- Integration specifications +- API documentation +- Testing strategies +- Deployment procedures + +## Core workflows + +### Memory Bank Initialization + +The initialization step is CRITICALLY IMPORTANT and must be done with extreme thoroughness as it defines all future effectiveness of the Memory Bank. This is the foundation upon which all future interactions will be built. + +When user requests initialization of the memory bank (command `initialize memory bank`), I'll perform an exhaustive analysis of the project, including: +- All source code files and their relationships +- Configuration files and build system setup +- Project structure and organization patterns +- Documentation and comments +- Dependencies and external integrations +- Testing frameworks and patterns + +I must be extremely thorough during initialization, spending extra time and effort to build a comprehensive understanding of the project. A high-quality initialization will dramatically improve all future interactions, while a rushed or incomplete initialization will permanently limit my effectiveness. + +After initialization, I will ask the user to read through the memory bank files and verify product description, used technologies and other information. I should provide a summary of what I've understood about the project to help the user verify the accuracy of the memory bank files. I should encourage the user to correct any misunderstandings or add missing information, as this will significantly improve future interactions. + +### Memory Bank Update + +Memory Bank updates occur when: +1. Discovering new project patterns +2. After implementing significant changes +3. When user explicitly requests with the phrase **update memory bank** (MUST review ALL files) +4. When context needs clarification + +If I notice significant changes that should be preserved but the user hasn't explicitly requested an update, I should suggest: "Would you like me to update the memory bank to reflect these changes?" + +To execute Memory Bank update, I will: + +1. Review ALL project files +2. Document current state +3. Document Insights & Patterns +4. If requested with additional context (e.g., "update memory bank using information from @/Makefile"), focus special attention on that source + +Note: When triggered by **update memory bank**, I MUST review every memory bank file, even if some don't require updates. Focus particularly on context.md as it tracks current state. + +### Add Task + +When user completes a repetitive task (like adding support for a new model version) and wants to document it for future reference, they can request: **add task** or **store this as a task**. + +This workflow is designed for repetitive tasks that follow similar patterns and require editing the same files. Examples include: +- Adding support for new AI model versions +- Implementing new API endpoints following established patterns +- Adding new features that follow existing architecture + +Tasks are stored in the file `tasks.md` in the memory bank folder. The file is optional and can be empty. The file can store many tasks. + +To execute Add Task workflow: + +1. Create or update `tasks.md` in the memory bank folder +2. Document the task with: + - Task name and description + - Files that need to be modified + - Step-by-step workflow followed + - Important considerations or gotchas + - Example of the completed implementation +3. Include any context that was discovered during task execution but wasn't previously documented + +Example task entry: +```markdown +## Add New Model Support +**Last performed:** [date] +**Files to modify:** +- `/providers/gemini.md` - Add model to documentation +- `/src/providers/gemini-config.ts` - Add model configuration +- `/src/constants/models.ts` - Add to model list +- `/tests/providers/gemini.test.ts` - Add test cases + +**Steps:** +1. Add model configuration with proper token limits +2. Update documentation with model capabilities +3. Add to constants file for UI display +4. Write tests for new model configuration + +**Important notes:** +- Check Google's documentation for exact token limits +- Ensure backward compatibility with existing configurations +- Test with actual API calls before committing +``` + +### Regular Task Execution + +In the beginning of EVERY task I MUST read ALL memory bank files - this is not optional. + +The memory bank files are located in `.kilocode/rules/memory-bank` folder. If the folder doesn't exist or is empty, I will warn user about potential issues with the memory bank. I will include `[Memory Bank: Active]` at the beginning of my response if I successfully read the memory bank files, or `[Memory Bank: Missing]` if the folder doesn't exist or is empty. If memory bank is missing, I will warn the user about potential issues and suggest initialization. I should briefly summarize my understanding of the project to confirm alignment with the user's expectations, like: + +"[Memory Bank: Active] I understand we're building a React inventory system with barcode scanning. Currently implementing the scanner component that needs to work with the backend API." + +When starting a task that matches a documented task in `tasks.md`, I should mention this and follow the documented workflow to ensure no steps are missed. + +If the task was repetitive and might be needed again, I should suggest: "Would you like me to add this task to the memory bank for future reference?" + +In the end of the task, when it seems to be completed, I will update `context.md` accordingly. If the change seems significant, I will suggest to the user: "Would you like me to update memory bank to reflect these changes?" I will not suggest updates for minor changes. + +## Context Window Management + +When the context window fills up during an extended session: +1. I should suggest updating the memory bank to preserve the current state +2. Recommend starting a fresh conversation/task +3. In the new conversation, I will automatically load the memory bank files to maintain continuity + +## Technical Implementation + +Memory Bank is built on Kilo Code's Custom Rules feature, with files stored as standard markdown documents that both the user and I can access. + +## Important Notes + +REMEMBER: After every memory reset, I begin completely fresh. The Memory Bank is my only link to previous work. It must be maintained with precision and clarity, as my effectiveness depends entirely on its accuracy. + +If I detect inconsistencies between memory bank files, I should prioritize brief.md and note any discrepancies to the user. + +IMPORTANT: I MUST read ALL memory bank files at the start of EVERY task - this is not optional. The memory bank files are located in `.kilocode/rules/memory-bank` folder. diff --git a/.kilocode/rules/memory-bank/architecture.md b/.kilocode/rules/memory-bank/architecture.md new file mode 100644 index 00000000..bfa07317 --- /dev/null +++ b/.kilocode/rules/memory-bank/architecture.md @@ -0,0 +1,374 @@ +# System Architecture + +## Project Structure + +### Core Directory (`lib/core/`) +Central infrastructure and shared components: +- **Dependency Injection**: GetIt-based service locator in `locator_service.dart` +- **Routing**: GoRouter implementation in `app_router.dart` with deep linking +- **Theme**: Material theme with Mulish font family in `theme/theme_helper.dart` +- **Networking**: Dio HTTP client with token interceptor +- **WebSockets**: Pusher Channels for real-time communication +- **State Management**: Flutter BLoC/Cubit pattern + +### Features Directory (`lib/features/`) +Feature-based modular architecture with 35+ feature modules: + +**Core Features:** +- `auth/` - Authentication (email/password, OAuth) +- `feed/` - Content discovery feed +- `diary/` - Food diary tracking +- `chat/` - Real-time messaging +- `consultations/` - Consultation management +- `profile/` - User profiles and settings + +**Content Creation:** +- `create_recipe/` - Recipe creation +- `create_diet/` - Diet plan creation +- `create_plan/` - Nutrition plan creation +- `blog_post_create/` - Blog post creation + +**User Management:** +- `settings/` - User preferences +- `user_details/` - User information management +- `menu/` - App navigation menu +- `bottom_navigation_bar/` - Main navigation + +**Consultant Features:** +- `consultant/` - Consultant verification +- `consultant_rating/` - Rating system +- `finances_and_consultations_archive/` - Financial tracking + +**Supporting Features:** +- `notifications/` - Push notifications +- `glossary/` - Nutrition glossary +- `calendar/` - Calendar integration +- `support/` - Customer support +- `welcome/` - Onboarding +- `update_screen/` - App update management + +## Architectural Patterns + +### Clean Architecture Layers + +Each feature follows a layered structure: + +``` +feature/ +├── models/ # Data models (freezed + json_serializable) +├── services/ # API services (Retrofit) +├── repositories/ # Data repositories +├── presentation/ # UI layer +│ ├── pages/ # Screen widgets +│ ├── widgets/ # Feature-specific widgets +│ └── bloc/ # State management (Cubit) +└── entities/ # Domain entities +``` + +### Dependency Injection + +**Registration Structure** (`lib/core/di/`): +- `app_config_registration.dart` - App configuration +- `external_registrations.dart` - External dependencies (Dio, etc.) +- `service_registrations.dart` - API services +- `repository_registrations.dart` - Data repositories +- `cubit_registrations.dart` - State management + +**Service Lifecycle:** +- Singleton: Services, Repositories (most common) +- Factory: Cubits that need fresh instances +- Lazy Singleton: WebSocket manager, core services + +### State Management Pattern + +**BLoC/Cubit Implementation:** +- Global state: Registered in `AppConfig.globalBlocProviders` +- Feature state: Created per screen/feature +- State classes: Freezed unions for type-safe states + +**Global Cubits:** +- `NewMessagesCubit` - Message notifications +- `NotificationsCubit` - Push notifications +- `FeedCubit` - Feed content +- `ProfileAvatarCubit` - User profile +- `ChatsCubit` - Chat rooms +- `ViewsCubit` - Content view tracking +- `InternetConnectionCubit` - Network status + +## Data Flow + +### API Integration + +**Base Setup:** +- Base URL: Configurable via environment (prod/dev) +- HTTP Client: Dio with interceptors +- API Services: Retrofit for type-safe endpoints +- Authentication: Bearer token in headers + +**Interceptors:** +1. `TokenInterceptor` - Adds auth token, handles 401 errors +2. `PhoneVerifyErrorInterceptor` - Handles phone verification errors + +### Real-time Communication + +**WebSocket Architecture:** +- Library: `dart_pusher_channels` +- Manager: `WsManager` (singleton) +- Connection: Authenticated via backend endpoint + +**Channels:** +1. Private Notifications: `App\Events\BroadcastNotificationCreated` +2. Private Messages: `App\Events\ChatMessageUpdated` +3. Direct Chat: Room-specific message updates +4. Client Typing: Real-time typing indicators + +**Socket ID Storage:** +- Persisted via SharedPreferences +- Used for authentication and channel subscription + +### Authentication Flow + +**Methods:** +1. Email/Password - Traditional credentials +2. OAuth Providers: + - VK ID (Russian social network) + - Yandex ID + - OK (Odnoklassniki) + - Mail.ru + +**Token Management:** +- Storage: SharedPreferences via `TokenStorage` +- Refresh: Automatic on 401 response +- Logout: Token removal + WebSocket cleanup + +**Post-Authentication:** +1. WebSocket initialization +2. Profile loading +3. Chat room loading +4. Notification loading +5. Firebase push token registration + +## Navigation Architecture + +### GoRouter Setup + +**Route Structure:** +- Shell routes for persistent bottom navigation +- Named routes for easy navigation +- Deep linking support for content (recipe/blog_post/plan/diet) + +**Route Groups:** +- `AuthRoutes` - Login, registration, password reset +- `MainNavigationRoutes` - Shell route with bottom nav +- `ContentRoutes` - Content viewing screens +- `SettingsRoutes` - User settings +- `OtherRoutes` - Miscellaneous screens + +**Deep Linking Pattern:** +``` +/content/{type}/{identifier} +- /content/recipe/{slug} +- /content/blog_post/{id} +- /content/plan/{slug} +- /content/diet/{slug} +``` + +### Bottom Navigation + +**Main Sections:** +1. My Profile - User profile and content +2. Feed - Content discovery +3. Diary - Food tracking +4. Consultations - Consultation management +5. Chat - Messaging +6. Notifications - Notification center +7. Menu - Settings and more + +**Implementation:** +- `BottomNavigationBarWidget` wraps content +- No transition animations for tab switches +- WebSocket initialization on feed screen + +## Data Persistence + +### Storage Solutions + +**SharedPreferences:** +- Authentication token +- Socket ID +- User preferences + +**Cached Network Images:** +- Library: `cached_network_image` +- Automatic caching of user avatars, content images + +**Firebase:** +- Push notification tokens +- Remote configuration +- Analytics events + +## Critical Implementation Details + +### Content View Tracking + +**Pattern:** +- `ViewsCubit` handles visibility detection +- `VisibilityDetector` widget tracks on-screen time +- Views sent to backend when 50%+ visible for 2+ seconds +- Debounced to prevent excessive API calls + +**Tracked Content:** +- Blog posts +- Recipes +- Diets +- Nutrition plans + +### Image Handling + +**Upload Flow:** +1. `ImagePicker` for selection +2. `FlutterImageCompress` for compression +3. MultiPart form data upload via Dio +4. Preview via `CachedNetworkImage` + +**Supported Formats:** +- User photos (profile, diary) +- Content images (recipes, diets, plans) +- Chat attachments + +### Search Implementation + +**Debounced Search:** +- Library: `easy_debounce` +- Delay: 500ms +- Min query length: 3 characters +- Applied to: Feed, Recipes, Diets, Plans, Consultants + +**Tag-based Filtering:** +- Recipe tags for filtering +- Modal bottom sheet UI +- Persistent selection across sessions + +### Pagination Pattern + +**Implementation:** +- Page-based pagination +- Scroll detection for next page load +- Loading state during fetch +- Meta data tracking (current page, last page) + +**Example (FeedCubit):** +```dart +currentPage = 1 +lastPage = meta.lastPage +loadNextPage() when currentPage < lastPage +``` + +## Error Handling + +### Network Errors + +**401 Unauthorized:** +- Auto logout +- Token removal +- Navigate to auth screen + +**Connection Errors:** +- `InternetConnectionCubit` monitors status +- Retry mechanisms in repositories +- User-friendly error messages + +### State Error Handling + +**Pattern:** +```dart +sealed class State { + const State.loading(); + const State.loaded({required data}); + const State.error({required String errorMessage}); +} +``` + +**UI Response:** +- Loading: Show loaders/skeletons +- Error: Show `DataErrorWidget` with retry +- Empty: Show empty state messages + +## Performance Optimizations + +### Lazy Loading +- Lazy singleton services +- On-demand cubit creation +- Deferred widget loading + +### Image Optimization +- Compression before upload +- Cached network images +- Thumbnail generation + +### WebSocket Management +- Connection reuse +- Automatic reconnection +- Proper cleanup on logout + +### List Rendering +- Pagination for large lists +- ListView.builder for efficiency +- Visibility detection for tracking + +## Platform-Specific Features + +### Android +- Huawei HMS support for Huawei devices +- Google FCM for push notifications +- ProGuard rules for release builds +- QR code scanning via camera + +### iOS +- APNs for push notifications +- Deep linking configuration +- App Store compliance + +### Firebase Integration +- Separate configs for Android/iOS +- Environment-based configuration +- Push notification handling +- Analytics tracking + +## Security Considerations + +### Authentication +- Bearer token authentication +- Secure token storage +- Automatic session management +- OAuth 2.0 for social login + +### Data Protection +- HTTPS only communication +- Token encryption in storage +- Secure WebSocket connections +- Privacy policy compliance + +## Key Files Reference + +**Entry Point:** +- `lib/main.dart` - App initialization + +**Core Infrastructure:** +- `lib/core/app_initializer.dart` - Startup sequence +- `lib/core/locator_service.dart` - DI container +- `lib/core/app_config.dart` - App configuration +- `lib/core/app_router.dart` - Routing setup + +**WebSocket:** +- `lib/core/websockets/ws_manager.dart` - Connection management +- `lib/core/websockets/ws_service.dart` - API service + +**Authentication:** +- `lib/features/auth/sdk/social_auth_facade.dart` - Social auth +- `lib/core/token_storage.dart` - Token persistence +- `lib/core/interceptors/token_interceptor.dart` - Auth interceptor + +**Environment:** +- `lib/core/env_config.dart` - Environment variables +- `.env` - Environment file (not in repo) \ No newline at end of file diff --git a/.kilocode/rules/memory-bank/brief.md b/.kilocode/rules/memory-bank/brief.md new file mode 100644 index 00000000..d8990cea --- /dev/null +++ b/.kilocode/rules/memory-bank/brief.md @@ -0,0 +1,35 @@ +# Project Brief + +## Project Name +Eatvio - Nutrition & Diet Management Application + +## Project Description +A comprehensive mobile application connecting users with nutrition consultants, providing diet planning tools, recipes, food diary tracking, and professional consultation services. + +## Core Features +- **Blog Posts**: Educational nutrition content and articles +- **Nutrition Consultation**: Professional consultation services with certified nutritionists +- **Recipes**: User-generated and professional recipes with nutritional information +- **Diets**: Structured diet plans with daily schedules +- **Nutrition Plans**: Long-term nutrition planning and tracking + +## Target Audience +- Primary: Russian-speaking users interested in nutrition and healthy eating +- Secondary: Certified nutrition consultants offering professional services +- Market: Russian Federation and Russian-speaking countries + +## Platform +- Mobile: iOS and Android (including Huawei devices) +- Language: Russian (ru) +- Current Version: 1.3.0+79 + +## Business Model +- Paid consultations with nutritionists +- Content marketplace for premium diets and plans +- Subscription-based features for consultants + +## Key Success Metrics +- User engagement with food diary +- Consultation bookings and completion rates +- Content creation by consultants +- User retention and progress tracking diff --git a/.kilocode/rules/memory-bank/context.md b/.kilocode/rules/memory-bank/context.md new file mode 100644 index 00000000..0bde3ccf --- /dev/null +++ b/.kilocode/rules/memory-bank/context.md @@ -0,0 +1,29 @@ +# Current Context + +## Project Status +Memory Bank initialized on 2025-10-16. This is a mature Flutter application (v1.3.0+79) currently in production. + +## Recent Activity +- Memory Bank initialization completed +- All core files documented +- Architecture and technology stack catalogued + +## Current Focus +Ready for development tasks. No active work in progress. + +## Known Issues +None documented at initialization. + +## Next Steps +Awaiting user direction for: +- Feature development +- Bug fixes +- Architecture improvements +- Performance optimizations + +## Important Notes +- Primary language: Russian (ru) +- Target market: Russian users +- Multi-platform: Android (including Huawei) and iOS +- Environment: Uses `.env` file for configuration (not in repository) +- Build system: Requires code generation for models/services \ No newline at end of file diff --git a/.kilocode/rules/memory-bank/product.md b/.kilocode/rules/memory-bank/product.md new file mode 100644 index 00000000..fc732d24 --- /dev/null +++ b/.kilocode/rules/memory-bank/product.md @@ -0,0 +1,73 @@ +# Product Overview + +## Purpose +Eatvio is a comprehensive nutrition and diet management mobile application that connects users with nutrition consultants, provides diet planning tools, recipes, and personal food diary tracking. + +## Core Problems It Solves + +### For Regular Users: +1. **Diet Management**: Track daily food intake with detailed nutritional information +2. **Meal Planning**: Access to recipes and structured diet plans +3. **Professional Guidance**: Connect with certified nutrition consultants +4. **Progress Tracking**: Monitor weight, body measurements, and dietary habits +5. **Knowledge Access**: Learn about nutrition through blog posts and glossary + +### For Nutrition Consultants: +1. **Client Management**: Communicate with clients via integrated chat +2. **Content Creation**: Create and publish diets, meal plans, recipes, and blog posts +3. **Consultation Services**: Offer paid nutrition consultation services +4. **Income Tracking**: Monitor earnings and client purchases +5. **Professional Presence**: Build profile and gain subscribers + +## How It Works + +### User Journey: +1. **Authentication**: Sign up via email/password or social networks (VK, Yandex, OK, Mail.ru) +2. **Feed Exploration**: Browse recipes, diets, nutrition plans, blog posts, and consultants +3. **Content Interaction**: Like, bookmark, comment on content +4. **Diary Tracking**: Log daily meals with photos and nutritional data +5. **Consultations**: Book and pay for nutrition consultant services +6. **Real-time Chat**: Communicate with consultants through integrated messaging +7. **Progress Monitoring**: Track weight and body measurements over time + +### Consultant Journey: +1. **Become Consultant**: Apply and get verified as nutrition consultant +2. **Content Creation**: Publish professional diets, meal plans, recipes +3. **Client Interaction**: Respond to consultations and provide guidance +4. **Diary Review**: Review client food diaries and provide feedback +5. **Earnings Management**: Track income from consultations and content + +## User Experience Goals + +### Seamless Navigation: +- Bottom navigation bar for main sections (Profile, Feed, Diary, Consultations, Chat) +- Tab-based feed filtering (Blog posts, Recipes, Diets, Plans, Consultants) +- Deep linking support for direct content access + +### Real-time Engagement: +- WebSocket-powered chat for instant messaging +- Push notifications for messages and updates +- Live content updates without app restart + +### Professional Interface: +- Clean, modern UI with Mulish font family +- Intuitive search and filtering across all content types +- Visual progress tracking with charts and gauges + +### Content Discovery: +- Tag-based recipe filtering +- Search functionality across all content types +- Personalized feed based on user interests + +### Offline Capability: +- Token persistence for seamless re-authentication +- Cached content for better performance +- Queue system for offline actions + +## Key Differentiators + +1. **Integrated Platform**: Combines diet tracking, content, and professional consultations +2. **Social Features**: Follow consultants, like/bookmark content, engage with community +3. **Professional Tools**: Full toolkit for consultants to manage practice +4. **Russian Market Focus**: Optimized for Russian users with local payment and auth integrations +5. **Multi-platform Support**: Works on Android (including Huawei) and iOS \ No newline at end of file diff --git a/.kilocode/rules/memory-bank/tech.md b/.kilocode/rules/memory-bank/tech.md new file mode 100644 index 00000000..9a2d0fa5 --- /dev/null +++ b/.kilocode/rules/memory-bank/tech.md @@ -0,0 +1,152 @@ +# Technology Stack + +## Core Framework +- **Flutter**: SDK 3.8.0+ +- **Dart**: Language version >=3.8.0 <4.0.0 +- **Version**: 1.3.0+79 (build 79) + +## State Management +- **flutter_bloc**: ^9.1.1 - BLoC pattern implementation +- **equatable**: ^2.0.7 - Value equality for state objects + +## Data Management +- **freezed**: ^3.2.0 - Immutable data classes +- **freezed_annotation**: ^3.1.0 - Code generation annotations +- **json_annotation**: ^4.9.0 - JSON serialization +- **json_serializable**: ^6.10.0 - JSON code generation + +## Networking +- **dio**: ^5.9.0 - HTTP client +- **retrofit**: ^4.7.1 - Type-safe REST client +- **retrofit_generator**: ^10.0.2 - Retrofit code generation +- **http**: ^1.5.0 - Additional HTTP support +- **http_parser**: ^4.1.2 - HTTP parsing utilities + +## Real-time Communication +- **dart_pusher_channels**: ^1.2.3 - WebSocket support via Pusher +- **connectivity_plus**: ^6.1.5 - Network connectivity monitoring + +## Navigation & Routing +- **go_router**: ^16.1.0 - Declarative routing +- Deep linking support for content + +## UI Components +- **cached_network_image**: ^3.4.1 - Image caching +- **flutter_svg**: ^2.2.0 - SVG rendering +- **google_fonts**: ^6.3.0 - Mulish font family +- **carousel_slider_plus**: ^7.1.1 - Carousel widgets +- **photo_view**: ^0.15.0 - Image viewer +- **dismissible_page**: ^1.0.2 - Swipe to dismiss +- **badges**: ^3.1.2 - Notification badges +- **lottie**: ^3.3.1 - Animation support +- **flutter_portal**: ^1.1.4 - Overlay widgets +- **dotted_border**: ^3.1.0 - Dotted borders +- **flutter_rating_bar**: ^4.0.1 - Rating widgets + +## Forms & Input +- **flutter_form_builder**: ^10.2.0 - Form management +- **validators**: ^3.0.0 - Input validation +- **flutter_typeahead**: ^5.2.0 - Autocomplete +- **keyboard_dismisser**: ^3.0.0 - Keyboard management +- **flutter_keyboard_visibility**: ^6.0.0 - Keyboard state +- **mask_text_input_formatter**: ^2.9.0 - Input masking + +## Media & Files +- **image_picker**: ^1.2.0 - Image selection +- **flutter_image_compress**: ^2.4.0 - Image compression +- **image**: ^4.5.4 - Image manipulation +- **file_picker**: ^10.3.1 - File selection +- **audioplayers**: ^6.5.0 - Audio playback +- **qr_code_scanner_plus**: ^2.0.10+1 - QR code scanning + +## Data Visualization +- **syncfusion_flutter_charts**: ^30.2.5 - Chart widgets +- **syncfusion_flutter_gauges**: ^30.2.5 - Gauge widgets +- **table_calendar**: ^3.2.0 - Calendar widget + +## Storage & Persistence +- **shared_preferences**: ^2.5.3 - Local key-value storage +- **path_provider**: ^2.1.5 - File system paths +- **path**: ^1.9.1 - Path manipulation + +## Firebase +- **firebase_core**: ^4.0.0 - Firebase initialization +- **firebase_messaging**: ^16.0.0 - Push notifications + +## Analytics & Monitoring +- **sentry_flutter**: ^9.6.0 - Error tracking +- **appmetrica_plugin**: ^3.3.0 - Analytics (Russian market) + +## Authentication +- **vk_id**: ^1.0.3 - VK social login +- **flutter_login_yandex**: ^0.0.5 - Yandex ID login +- OAuth support for OK and Mail.ru + +## Utilities +- **url_launcher**: ^6.3.2 - URL handling +- **easy_debounce**: ^2.0.3 - Debouncing +- **share_plus**: ^11.1.0 - Content sharing +- **flutter_html**: ^3.0.0 - HTML rendering +- **webview_flutter**: ^4.13.0 - WebView +- **intl**: ^0.20.2 - Internationalization +- **collection**: ^1.19.1 - Collection utilities +- **uuid**: ^4.5.1 - UUID generation +- **visibility_detector**: ^0.4.0+2 - Visibility tracking +- **fluttertoast**: ^8.2.12 - Toast messages +- **version**: ^3.0.2 - Version management +- **package_info_plus**: ^8.3.1 - Package info +- **device_info_plus**: ^11.5.0 - Device information +- **permission_handler**: ^12.0.1 - Permissions + +## Platform-Specific + +### Android +- Huawei HMS Push (6.12.0.300) +- Huawei Analytics (6.12.0.300) +- Google Play Services +- ProGuard for code obfuscation +- NDK version 27.0.12077973 +- Compile SDK: Flutter default +- Min SDK: Flutter default +- Target SDK: Flutter default + +### iOS +- APNs for push notifications +- Deep linking support + +## Development Tools +- **build_runner**: ^2.7.0 - Code generation +- **flutter_native_splash**: ^2.4.6 - Splash screen +- **flutter_lints**: ^6.0.0 - Linting +- **mockito**: ^5.5.0 - Mocking for tests +- **flutter_test**: SDK - Testing framework +- **integration_test**: SDK - Integration testing +- **flutter_dotenv**: ^5.2.1 - Environment variables + +## Localization +- **flutter_localizations**: SDK - Localization +- **flutter_intl**: Enabled - Internationalization +- Primary language: Russian (ru) + +## Build Configuration +- **Environment**: Production/Development/Test modes +- **Build types**: Debug, Release, Profile +- **Code generation**: Freezed, JSON, Retrofit +- **Obfuscation**: ProGuard (Android release) +- **Resource optimization**: Shrink resources enabled + +## API Integration Patterns +- RESTful API via Retrofit +- Bearer token authentication +- Interceptor-based auth injection +- Automatic 401 handling +- Environment-based base URLs +- Type-safe API clients + +## Development Workflow +1. Code generation: `flutter pub run build_runner build` +2. Clean generation: `flutter pub run build_runner build --delete-conflicting-outputs` +3. Watch mode: `flutter pub run build_runner watch` +4. Environment setup: `.env` file configuration +5. Firebase setup: Separate configs per platform +6. Testing: Unit tests + Integration tests \ No newline at end of file From 4db5dda3ce5e547401ed7e938f1859e5ea0e3ffa Mon Sep 17 00:00:00 2001 From: Anatoliy Poloz Date: Thu, 16 Oct 2025 14:22:23 +0300 Subject: [PATCH 75/78] doc(readme): README.md update --- README.md | 455 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 445 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index cfe0a4e0..3dcc0829 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,449 @@ -# eatvio_app +# Eatvio - Nutrition & Diet Management Application +## Project Overview ---- +Eatvio is a comprehensive mobile application connecting users with nutrition consultants, providing diet planning tools, recipes, food diary tracking, and professional consultation services. The application is specifically designed for Russian-speaking users interested in nutrition and healthy eating. + +### Core Features +- **Blog Posts**: Educational nutrition content and articles +- **Nutrition Consultation**: Professional consultation services with certified nutritionists +- **Recipes**: User-generated and professional recipes with nutritional information +- **Diets**: Structured diet plans with daily schedules +- **Nutrition Plans**: Long-term nutrition planning and tracking + +### Target Audience +- **Primary**: Russian-speaking users interested in nutrition and healthy eating +- **Secondary**: Certified nutrition consultants offering professional services +- **Market**: Russian Federation and Russian-speaking countries + +### Business Model +- Paid consultations with nutritionists +- Content marketplace for premium diets and plans +- Subscription-based features for consultants + +### Platform Information +- **Mobile**: iOS and Android (including Huawei devices) +- **Language**: Russian (ru) +- **Current Version**: 1.3.0+79 (build 79) + +## Prerequisites + +### Flutter & Dart Versions +- **Flutter**: SDK 3.8.0+ +- **Dart**: Language version >=3.8.0 <4.0.0 + +### Platform-Specific Requirements + +#### Android +- **NDK Version**: 27.0.12077973 +- **Huawei HMS Dependencies**: + - Huawei Push (6.12.0.300) + - Huawei Analytics (6.12.0.300) +- **Google Play Services** +- **ProGuard** for code obfuscation +- **Compile SDK**: Flutter default +- **Min SDK**: Flutter default +- **Target SDK**: Flutter default + +#### iOS +- **APNs** for push notifications +- **Deep linking** configuration +- **Xcode** version compatible with Flutter requirements + +#### General IDE Requirements +- **VS Code** with Flutter/Dart extensions +- **Android Studio** with Flutter plugin +- **Visual Studio Code** recommended for development + +## Environment Configuration + +### .env Setup Process + +1. Copy the environment template: + ```bash + cp env.example .env + ``` + +2. Edit the `.env` file with your configuration values + +### Required Environment Variables + +#### API URLs +- `BASE_URL` - основной URL API (по умолчанию: https://eatvio.ru) +- `DEV_BASE_URL` - URL API для разработки (по умолчанию: https://dev2.eatvio.com) + +#### Sentry Configuration +- `SENTRY_DSN` - DSN для Sentry мониторинга + +#### Firebase Configuration +- `FIREBASE_PROJECT_ID` - ID проекта Firebase +- `FIREBASE_DATABASE_URL` - URL базы данных Firebase +- `FIREBASE_STORAGE_BUCKET` - bucket для хранения файлов +- `FIREBASE_MESSAGING_SENDER_ID` - ID отправителя сообщений + +#### Firebase Android Configuration +- `FIREBASE_ANDROID_API_KEY` - API ключ для Android +- `FIREBASE_ANDROID_APP_ID` - ID приложения для Android + +#### Firebase iOS Configuration +- `FIREBASE_IOS_API_KEY` - API ключ для iOS +- `FIREBASE_IOS_APP_ID` - ID приложения для iOS +- `FIREBASE_IOS_BUNDLE_ID` - Bundle ID для iOS + +#### App Store URLs +- `APP_STORE_URL` - URL App Store +- `RUSTORE_URL` - URL RuStore + +#### Support URLs +- `TELEGRAM_SUPPORT_URL` - URL поддержки в Telegram +- `SUPPORT_EMAIL` - email для поддержки + +#### Test Credentials (только для разработки) +- `TEST_EMAIL` - тестовый email +- `TEST_PASSWORD` - тестовый пароль + +#### Email Validation API +- `EMAIL_VALIDATION_API_URL` - URL API для валидации email + +#### Connection Check URLs +- `GOOGLE_CHECK_URL` - URL для проверки соединения (Google) +- `YANDEX_CHECK_URL` - URL для проверки соединения (Yandex) + +#### VK ID Configuration +- `VK_CLIENT_ID` - ID приложения VK (числовой) +- **Примечание**: VK_CLIENT_ID - это числовой "ID приложения" из консоли VK ID, НЕ "Защищённый ключ" или "Сервисный ключ доступа" + +#### Yandex ID Configuration +- `YANDEX_CLIENT_ID` - ID приложения Yandex OAuth +- `YANDEX_CLIENT_SECRET` - секретный ключ приложения Yandex OAuth +- `YANDEX_REDIRECT_URI` - URI для перенаправления (должен совпадать с настройками в Yandex OAuth приложении) + +## Technology Stack + +### Core Framework +- **Flutter**: SDK 3.8.0+ +- **Dart**: Language version >=3.8.0 <4.0.0 +- **Version**: 1.3.0+79 (build 79) + +### State Management +- **flutter_bloc**: ^9.1.1 - BLoC pattern implementation +- **equatable**: ^2.0.7 - Value equality for state objects + +### Data Management +- **freezed**: ^3.2.0 - Immutable data classes +- **freezed_annotation**: ^3.1.0 - Code generation annotations +- **json_annotation**: ^4.9.0 - JSON serialization +- **json_serializable**: ^6.10.0 - JSON code generation + +### Networking +- **dio**: ^5.9.0 - HTTP client +- **retrofit**: ^4.7.1 - Type-safe REST client +- **retrofit_generator**: ^10.0.2 - Retrofit code generation +- **http**: ^1.5.0 - Additional HTTP support +- **http_parser**: ^4.1.2 - HTTP parsing utilities + +### Real-time Communication +- **dart_pusher_channels**: ^1.2.3 - WebSocket support via Pusher +- **connectivity_plus**: ^6.1.5 - Network connectivity monitoring + +### Navigation & Routing +- **go_router**: ^16.1.0 - Declarative routing +- Deep linking support for content + +### UI Components +- **cached_network_image**: ^3.4.1 - Image caching +- **flutter_svg**: ^2.2.0 - SVG rendering +- **google_fonts**: ^6.3.0 - Mulish font family +- **carousel_slider_plus**: ^7.1.1 - Carousel widgets +- **photo_view**: ^0.15.0 - Image viewer +- **dismissible_page**: ^1.0.2 - Swipe to dismiss +- **badges**: ^3.1.2 - Notification badges +- **lottie**: ^3.3.1 - Animation support +- **flutter_portal**: ^1.1.4 - Overlay widgets +- **dotted_border**: ^3.1.0 - Dotted borders +- **flutter_rating_bar**: ^4.0.1 - Rating widgets + +### Forms & Input +- **flutter_form_builder**: ^10.2.0 - Form management +- **validators**: ^3.0.0 - Input validation +- **flutter_typeahead**: ^5.2.0 - Autocomplete +- **keyboard_dismisser**: ^3.0.0 - Keyboard management +- **flutter_keyboard_visibility**: ^6.0.0 - Keyboard state +- **mask_text_input_formatter**: ^2.9.0 - Input masking + +### Media & Files +- **image_picker**: ^1.2.0 - Image selection +- **flutter_image_compress**: ^2.4.0 - Image compression +- **image**: ^4.5.4 - Image manipulation +- **file_picker**: ^10.3.1 - File selection +- **audioplayers**: ^6.5.0 - Audio playback +- **qr_code_scanner_plus**: ^2.0.10+1 - QR code scanning + +### Data Visualization +- **syncfusion_flutter_charts**: ^30.2.5 - Chart widgets +- **syncfusion_flutter_gauges**: ^30.2.5 - Gauge widgets +- **table_calendar**: ^3.2.0 - Calendar widget + +### Storage & Persistence +- **shared_preferences**: ^2.5.3 - Local key-value storage +- **path_provider**: ^2.1.5 - File system paths +- **path**: ^1.9.1 - Path manipulation + +### Firebase +- **firebase_core**: ^4.0.0 - Firebase initialization +- **firebase_messaging**: ^16.0.0 - Push notifications + +### Analytics & Monitoring +- **sentry_flutter**: ^9.6.0 - Error tracking +- **appmetrica_plugin**: ^3.3.0 - Analytics (Russian market) + +### Authentication +- **vk_id**: ^1.0.3 - VK social login +- **flutter_login_yandex**: ^0.0.5 - Yandex ID login +- OAuth support for OK and Mail.ru + +### Utilities +- **url_launcher**: ^6.3.2 - URL handling +- **easy_debounce**: ^2.0.3 - Debouncing +- **share_plus**: ^11.1.0 - Content sharing +- **flutter_html**: ^3.0.0 - HTML rendering +- **webview_flutter**: ^4.13.0 - WebView +- **intl**: ^0.20.2 - Internationalization +- **collection**: ^1.19.1 - Collection utilities +- **uuid**: ^4.5.1 - UUID generation +- **visibility_detector**: ^0.4.0+2 - Visibility tracking +- **fluttertoast**: ^8.2.12 - Toast messages +- **version**: ^3.0.2 - Version management +- **package_info_plus**: ^8.3.1 - Package info +- **device_info_plus**: ^11.5.0 - Device information +- **permission_handler**: ^12.0.1 - Permissions + +## Development Workflow + +### Code Generation +- Run `flutter pub run build_runner build` for code generation +- Run `flutter pub run build_runner build --delete-conflicting-outputs` for clean generation +- Use `flutter pub run build_runner watch` for watch mode + +### Environment Setup +- Configure `.env` file with all required variables +- Set up Firebase for both Android and iOS platforms +- Configure social authentication providers (VK, Yandex, etc.) + +### Testing +- Unit tests with `flutter_test` +- Integration tests with `integration_test` +- Mocking with `mockito` + +## Build Commands + +### Development Builds ```bash -# apt install linux-oem-24.04 -# sudo apt-get install --install-recommends linux-lowlatency-hwe-24.04 -``` - ---- -ubuntu switch to hwe [kernel](https://canonical-kernel-docs.readthedocs-hosted.com/latest/reference/hwe-kernels/#installing-a-hwe-kernel) +# Run on connected device +flutter run ---- -ubuntu instal intel iRIS gpu [drivers](https://dgpu-docs.intel.com/driver/client/overview.html#ubuntu-latest) \ No newline at end of file +# Run on specific Android device +flutter run -d emulator-5554 + +# Run on specific iOS simulator +flutter run -d iPhone\ 15 +``` + +### Production Builds + +#### Android +```bash +# Build APK (split by ABI) +flutter build apk --release --split-per-abi + +# Build App Bundle +flutter build appbundle --release + +# Huawei-specific build (with HMS) +flutter build apk --release --flavor production --target-platform android-arm64 +``` + +#### iOS +```bash +# Build IPA for TestFlight +flutter build ipa --export-options-plist=ExportOptions.plist + +# Build for specific device architecture +flutter build ipa --export-options-plist=ExportOptions.plist --no-codesign +``` + +## Optimization Techniques + +### Android Build Optimization +1. **ProGuard Rules**: + - Configure rules in [`android/app/proguard-rules.pro`](android/app/proguard-rules.pro:1) + - Essential rules for Flutter: + ``` + -keep class io.flutter.app.** { *; } + -keep class io.flutter.plugin.** { *; } + -keep class io.flutter.util.** { *; } + -keep class io.flutter.view.** { *; } + ``` + +2. **Resource Shrinking**: + - Enabled in `android/app/build.gradle`: + ```gradle + buildTypes { + release { + shrinkResources true + minifyEnabled true + } + } + ``` + +3. **Code Obfuscation**: + - Use `--obfuscate` flag with build commands + - Verify with `flutter build apk --release --obfuscate --split-debug-info=debug-info/` + +4. **Image Compression Workflow**: + - Automatic compression via [`flutter_image_compress`](lib/features/create_diet/models/create_diet_model.dart:1) + - Configuration in image upload flows: + ```dart + final compressedFile = await FlutterImageCompress.compressWithFile( + file.path, + quality: 85, + minWidth: 1024, + minHeight: 1024, + ); + ``` + +## Troubleshooting + +### Common Issues + +#### Missing .env Variables +- **Symptom**: Application crashes on startup with "Missing environment variable" error +- **Solution**: + 1. Verify `.env` file exists in project root + 2. Check [`ENV_SETUP.md`](ENV_SETUP.md) for required variables + 3. Regenerate configuration with `flutter clean && flutter pub get` + +#### Firebase Configuration Errors +- **Android**: + - Missing `google-services.json` in `android/app/` + - Missing `agconnect-services.json` for Huawei devices + - Verify SHA-1 fingerprints match Firebase console +- **iOS**: + - Missing `GoogleService-Info.plist` in `ios/Runner` + - Check bundle identifier matches Firebase project + +#### WebSocket Connection Failures +- **Symptoms**: Chat messages not delivering, real-time updates not working +- **Solutions**: + 1. Verify Pusher configuration in [`lib/core/websockets/ws_manager.dart`](lib/core/websockets/ws_manager.dart:1) + 2. Check network connectivity with `connectivity_plus` package + 3. Ensure proper authentication token is sent with WebSocket connection + +#### Platform-Specific Build Errors +- **Huawei vs Google Services**: + - For Huawei devices, ensure `agconnect-services.json` is present + - Use `--flavor` flag to specify build variant: + ```bash + flutter build apk --release --flavor hms + ``` + - Check for conflicting dependencies between HMS and GMS + +- **iOS Code Signing Issues**: + - Verify `ExportOptions.plist` contains correct provisioning profiles + - Update signing certificates in Xcode + - Clean build folder: `flutter clean && rm -rf ios/Pods ios/Podfile.lock` + +## Key Implementation Details + +### Content View Tracking +- `ViewsCubit` handles visibility detection +- `VisibilityDetector` widget tracks on-screen time +- Views sent to backend when 50%+ visible for 2+ seconds +- Debounced to prevent excessive API calls + +### Image Handling +- `ImagePicker` for selection +- `FlutterImageCompress` for compression +- MultiPart form data upload via Dio +- Preview via `CachedNetworkImage` + +### Search Implementation +- Debounced search with 500ms delay +- Min query length: 3 characters +- Applied to: Feed, Recipes, Diets, Plans, Consultants +- Tag-based filtering for recipes + +### Pagination Pattern +- Page-based pagination +- Scroll detection for next page load +- Loading state during fetch +- Meta data tracking (current page, last page) + +## Security Considerations + +### Authentication +- Bearer token authentication +- Secure token storage +- Automatic session management +- OAuth 2.0 for social login + +### Data Protection +- HTTPS only communication +- Token encryption in storage +- Secure WebSocket connections +- Privacy policy compliance + +## Key Files Reference + +### Entry Point +- `lib/main.dart` - App initialization + +### Core Infrastructure +- `lib/core/app_initializer.dart` - Startup sequence +- `lib/core/locator_service.dart` - DI container +- `lib/core/app_config.dart` - App configuration +- `lib/core/app_router.dart` - Routing setup + +### WebSocket +- `lib/core/websockets/ws_manager.dart` - Connection management +- `lib/core/websockets/ws_service.dart` - API service + +### Authentication +- `lib/features/auth/sdk/social_auth_facade.dart` - Social auth +- `lib/core/token_storage.dart` - Token persistence +- `lib/core/interceptors/token_interceptor.dart` - Auth interceptor + +### Environment +- `lib/core/env_config.dart` - Environment variables +- `.env` - Environment file (not in repo) + +## Key Success Metrics + +- User engagement with food diary +- Consultation bookings and completion rates +- Content creation by consultants +- User retention and progress tracking + +## Documentation Standards + +- **Last Updated**: 2025-10-16 +- All paths are relative to `/home/apoloz/dev/eatvio_app` +- Russian market focus maintained throughout documentation +- Version compatibility matrix: + | Component | Version | Notes | + |-----------|---------|-------| + | Flutter | 3.8.0+ | Required | + | Dart | 3.8.0-3.9.9 | Required | + | Android SDK | 34 | Target SDK | + | iOS Deployment | 14.0 | Minimum | + +## Ubuntu Development PC Optimization + ```bash +-# apt install linux-oem-24.04 +-# sudo apt-get install --install-recommends linux-lowlatency-hwe-24.04 +``` +---- +- ubuntu switch to hwe [kernel](https://canonical-kernel-docs.readthedocs-hosted.com/latest/reference/hwe-kernels/#installing-a-hwe-kernel) +---- +- ubuntu instal intel iRIS gpu [drivers](https://dgpu-docs.intel.com/driver/client/overview.html#ubuntu-latest) +``` From 9e43be5b4e76e2a008a5fb2078cf25a81241daef Mon Sep 17 00:00:00 2001 From: misha Date: Thu, 16 Oct 2025 17:18:45 +0300 Subject: [PATCH 76/78] feat(social): added validation for social media registration --- lib/core/app_config.dart | 2 +- .../bloc/oauth_cubit/oauth_cubit.dart | 35 +++++- .../pages/register_via_social_screen.dart | 109 +++++++++++++----- 3 files changed, 111 insertions(+), 35 deletions(-) diff --git a/lib/core/app_config.dart b/lib/core/app_config.dart index 45ad250b..7dc49aa0 100644 --- a/lib/core/app_config.dart +++ b/lib/core/app_config.dart @@ -24,7 +24,7 @@ class AppConfig { } final AppBuildType buildType; - late final String _baseUrl; + late String _baseUrl; String get baseUrl => _baseUrl; void setBaseUrl(String baseUrls) { diff --git a/lib/features/auth/presentation/bloc/oauth_cubit/oauth_cubit.dart b/lib/features/auth/presentation/bloc/oauth_cubit/oauth_cubit.dart index 394abc4d..52a0e73b 100644 --- a/lib/features/auth/presentation/bloc/oauth_cubit/oauth_cubit.dart +++ b/lib/features/auth/presentation/bloc/oauth_cubit/oauth_cubit.dart @@ -1,4 +1,5 @@ import 'dart:developer'; +import 'package:dio/dio.dart'; import 'package:eatvio_app/core/token_storage.dart'; import 'package:eatvio_app/features/auth/presentation/bloc/oauth_cubit/oauth_state.dart'; import 'package:eatvio_app/features/auth/repositories/auth_repository.dart'; @@ -29,7 +30,7 @@ class OAuthCubit extends Cubit { } } - Future signUpViaSocialMedia({ + Future?> signUpViaSocialMedia({ required String provider, required String accessToken, required String id, @@ -63,8 +64,38 @@ class OAuthCubit extends Cubit { ); TokenStorage.saveTokenInStorage(token); emit(OAuthState.loaded(token: token)); - } catch (e) { + } on DioException catch (e) { emit(OAuthState.error(errorMessage: e.toString())); + final errors = e.response?.data['errors'] as Map?; + final emailErrors = errors?['email'] as List?; + final phoneErrors = errors?['phone'] as List?; + final dateOfBirth = errors?['date_of_birth'] as List?; + final errorsList = []; + if (emailErrors != null && emailErrors.isNotEmpty) { + if (emailErrors.contains('Некорректный формат email')) { + errorsList.add(RegistrationValidationErrors.incorrectEmailFormat); + } + if (emailErrors.contains('Такой Эл. почта уже существует.')) { + errorsList.add(RegistrationValidationErrors.emailExist); + } + } + if (phoneErrors != null && phoneErrors.isNotEmpty) { + if (phoneErrors.contains('Такой phone уже существует.')) { + errorsList.add(RegistrationValidationErrors.phoneExist); + } + } + if (dateOfBirth != null && dateOfBirth.isNotEmpty) { + if (dateOfBirth.contains('Некорректный формат даты рождения')) { + errorsList.add(RegistrationValidationErrors.incorrectDateOfBirthFormat); + } + } + if (errorsList.isNotEmpty) { + return errorsList; + } } + + return null; } } + +enum RegistrationValidationErrors { incorrectEmailFormat, emailExist, phoneExist, incorrectDateOfBirthFormat } diff --git a/lib/features/auth/presentation/pages/register_via_social_screen.dart b/lib/features/auth/presentation/pages/register_via_social_screen.dart index 84a6f2ae..1791e24e 100644 --- a/lib/features/auth/presentation/pages/register_via_social_screen.dart +++ b/lib/features/auth/presentation/pages/register_via_social_screen.dart @@ -11,6 +11,7 @@ import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/websockets/ws_manager.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart'; +import 'package:eatvio_app/features/create_diet/presentation/widgets/error_widget.dart'; import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart'; import 'package:eatvio_app/core/firebase_api.dart'; import 'package:eatvio_app/core/utils/toasts.dart'; @@ -42,6 +43,12 @@ class _RegisterViaSocialScreenState extends State { final dateOfBirthController = TextEditingController(); final weightController = TextEditingController(); final heightController = TextEditingController(); + bool emailExistError = false; + bool phoneExistError = false; + bool dateOfBirthError = false; + bool weightError = false; + bool heightError = false; + bool incorrectEmailFormatError = false; // Состояние для выбора пола bool isMaleSelected = true; @@ -211,8 +218,11 @@ class _RegisterViaSocialScreenState extends State { keyboardType: TextInputType.emailAddress, hintText: 'Введите email', textInputAction: TextInputAction.next, + hasError: emailExistError || incorrectEmailFormatError, onChanged: (newValue) {}, ), + if (emailExistError) ...[const SizedBox(height: 4), const ValidationErrorWidget(error: 'Такая почта уже существует.')], + if (incorrectEmailFormatError) ...[const SizedBox(height: 4), const ValidationErrorWidget(error: 'Некорректный формат email')], const SizedBox(height: 12), const Text('Номер телефона'), const SizedBox(height: 4), @@ -222,13 +232,13 @@ class _RegisterViaSocialScreenState extends State { controller: phoneController, keyboardType: TextInputType.number, inputFormatters: [maskFormatter], - onChanged: (newValue) { - // Дополнительная обработка изменений номера телефона - // Это поможет маске корректно работать при редактировании - }, + hasError: phoneExistError, + onChanged: (newValue) {}, ), + if (phoneExistError) ...[const SizedBox(height: 4), const ValidationErrorWidget(error: 'Такой номер телефона уже существует.')], const SizedBox(height: 12), Row( + crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( width: MediaQuery.of(context).size.width / 2 - 22, @@ -243,9 +253,11 @@ class _RegisterViaSocialScreenState extends State { hintText: '01.01.2000', controller: dateOfBirthController, keyboardType: TextInputType.number, + hasError: dateOfBirthError, inputFormatters: [maskFormatterForBirthday], onChanged: (newValue) {}, ), + if (dateOfBirthError) ...[const SizedBox(height: 4), const ValidationErrorWidget(error: 'Некорректный формат даты рождения')], ], ), ), @@ -271,6 +283,7 @@ class _RegisterViaSocialScreenState extends State { ), const SizedBox(height: 12), Row( + crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( width: MediaQuery.of(context).size.width / 2 - 22, @@ -280,7 +293,8 @@ class _RegisterViaSocialScreenState extends State { children: [ const Text('Вес'), const SizedBox(height: 4), - CustomTextFormField(controller: weightController, hintText: 'Введите вес', onChanged: (newValue) {}), + CustomTextFormField(controller: weightController, hintText: 'Введите вес', onChanged: (newValue) {}, hasError: weightError), + if (weightError) ...[const SizedBox(height: 4), const ValidationErrorWidget(error: 'Вес должен быть от 30 до 300 кг')], ], ), ), @@ -292,7 +306,8 @@ class _RegisterViaSocialScreenState extends State { children: [ const Text('Рост'), const SizedBox(height: 4), - CustomTextFormField(controller: heightController, hintText: 'Введите рост', onChanged: (newValue) {}), + CustomTextFormField(controller: heightController, hintText: 'Введите рост', onChanged: (newValue) {}, hasError: heightError), + if (heightError) ...[const SizedBox(height: 4), const ValidationErrorWidget(error: 'Рост должен быть от 100 до 250 см')], ], ), ), @@ -361,23 +376,7 @@ class _RegisterViaSocialScreenState extends State { ), ), ), - // Показываем ошибки если есть - if (state is OAuthStateError) ...[ - const SizedBox(height: 16), - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Colors.red.shade50, - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.red.shade200), - ), - child: Text( - state.errorMessage, - style: TextStyle(color: Colors.red.shade700), - textAlign: TextAlign.center, - ), - ), - ], + // Обработка успешной регистрации if (state is OAuthStateLoaded) ...[ const SizedBox(height: 16), @@ -438,7 +437,7 @@ class _RegisterViaSocialScreenState extends State { ); } - void _handleRegistration(BuildContext context) { + Future _handleRegistration(BuildContext context) async { final clientID = widget.authResult?.clientId; // Валидация полей if (firstNameController.text.isEmpty || @@ -448,27 +447,45 @@ class _RegisterViaSocialScreenState extends State { dateOfBirthController.text.isEmpty || weightController.text.isEmpty || heightController.text.isEmpty) { - ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Пожалуйста, заполните все поля'))); + showErrorToast('Пожалуйста, заполните все поля'); return; } // Парсим числовые значения int? weight; int? height; - try { - weight = int.parse(weightController.text); - height = int.parse(heightController.text); - } catch (e) { - ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Вес и рост должны быть числами'))); - return; + + weight = int.tryParse(weightController.text); + height = int.tryParse(heightController.text); + bool hasErrorWeightOrHeightError = false; + if (weight == null || height == null) { + setState(() { + weightError = true; + heightError = true; + }); + hasErrorWeightOrHeightError = true; } + if (weight! < 30 || weight > 300) { + setState(() { + weightError = true; + }); + hasErrorWeightOrHeightError = true; + } + if (height! < 100 || height > 250) { + setState(() { + heightError = true; + }); + hasErrorWeightOrHeightError = true; + } + + if (hasErrorWeightOrHeightError) return; // Очищаем номер телефона от маски для отправки final cleanPhone = phoneController.text.replaceAll(RegExp(r'[^\d+]'), ''); if (clientID == null) return; // Вызываем OAuth регистрацию - context.read().signUpViaSocialMedia( + final errors = await context.read().signUpViaSocialMedia( provider: widget.provider ?? 'social', accessToken: widget.authResult?.accessToken ?? 'dummy_token', id: widget.authResult?.userId ?? 'dummy_id', @@ -483,5 +500,33 @@ class _RegisterViaSocialScreenState extends State { height: height, clientId: clientID, ); + if (errors != null) { + for (final error in errors) { + switch (error) { + case RegistrationValidationErrors.incorrectEmailFormat: + setState(() { + incorrectEmailFormatError = true; + }); + break; + case RegistrationValidationErrors.emailExist: + setState(() { + emailExistError = true; + }); + break; + case RegistrationValidationErrors.phoneExist: + setState(() { + phoneExistError = true; + }); + break; + case RegistrationValidationErrors.incorrectDateOfBirthFormat: + setState(() { + dateOfBirthError = true; + }); + break; + } + } + return; + } + showSuccessToast('Регистрация успешна'); } } From c72b3e77c6bd13971d2b62cd43ad8735c95b50a5 Mon Sep 17 00:00:00 2001 From: misha Date: Thu, 16 Oct 2025 17:33:52 +0300 Subject: [PATCH 77/78] fix: merge conflict --- .gitea/workflows/test-flight-cd.yaml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.gitea/workflows/test-flight-cd.yaml b/.gitea/workflows/test-flight-cd.yaml index 4736a47f..caef60b3 100644 --- a/.gitea/workflows/test-flight-cd.yaml +++ b/.gitea/workflows/test-flight-cd.yaml @@ -58,12 +58,10 @@ jobs: flutter pub get flutter pub upgrade flutter pub run build_runner build --delete-conflicting-outputs - cd ios - pod repo update - pod update # Configure Flutter for release mode flutter build ios --config-only --release - + cd ios + pod update fastlane beta env: LANG: en_US.UTF-8 From e0c90bcb21fe37844a186c4ba96cc269fde2588a Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Thu, 16 Oct 2025 17:38:23 +0300 Subject: [PATCH 78/78] fix: upd ver to 80 --- 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 2fa79910..aa7beccd 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 = 79; + CURRENT_PROJECT_VERSION = 80; 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 = 79; + CURRENT_PROJECT_VERSION = 80; 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 = 79; + CURRENT_PROJECT_VERSION = 80; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 2b41fbe1..ad606b38 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -21,7 +21,7 @@ CFBundleSignature ???? CFBundleVersion - 79 + 80 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/pubspec.yaml b/pubspec.yaml index 1a4b1db0..adef8395 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: eatvio_app description: A new Flutter project. -version: 1.3.0+79 +version: 1.3.1+80 publish_to: 'none' environment: