From 96e80ced38eb26db4aaa8e1724a2d4f56319a515 Mon Sep 17 00:00:00 2001 From: Username1548 Date: Tue, 17 Jun 2025 13:33:41 +0700 Subject: [PATCH 001/134] feat(consultantRating): added api requests and state to consultant rating page --- lib/core/const.dart | 1 + lib/core/locator_service.dart | 8 +- .../models/rating_model.dart | 36 +++++- .../models/statistics_model.dart | 19 ++++ .../consultant_rating_screen.dart | 103 ++++++++++-------- .../cubit/consultant_rating_cubit.dart | 32 ++++++ .../cubit/consultant_rating_state.dart | 16 +++ .../widgets/period_filter_widget.dart | 72 +++++++----- .../widgets/rating_history_item.dart | 3 +- .../widgets/rating_history_widget.dart | 98 +++++++++-------- .../widgets/statistic_widget.dart | 25 +++-- .../consultant_rating_repository.dart | 26 +++++ .../services/consultant_rating_service.dart | 18 +++ .../menu/presentation/pages/menu_screen.dart | 10 ++ 14 files changed, 335 insertions(+), 132 deletions(-) create mode 100644 lib/features/consultant_rating/models/statistics_model.dart create mode 100644 lib/features/consultant_rating/presentation/cubit/consultant_rating_cubit.dart create mode 100644 lib/features/consultant_rating/presentation/cubit/consultant_rating_state.dart create mode 100644 lib/features/consultant_rating/repositories/consultant_rating_repository.dart create mode 100644 lib/features/consultant_rating/services/consultant_rating_service.dart diff --git a/lib/core/const.dart b/lib/core/const.dart index 618a9866..31c47e9c 100644 --- a/lib/core/const.dart +++ b/lib/core/const.dart @@ -1,4 +1,5 @@ String baseUrl = 'https://eatvio.ru'; +// String baseUrl = 'https://dev2.eatvio.com'; const List durationValues = [7, 14, 21]; diff --git a/lib/core/locator_service.dart b/lib/core/locator_service.dart index accb729d..82eac777 100644 --- a/lib/core/locator_service.dart +++ b/lib/core/locator_service.dart @@ -14,6 +14,9 @@ import 'package:eatvio_app/features/cancel_plan_bottom_sheet/service/cancel_plan 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'; @@ -158,6 +161,7 @@ Future init() async { sl.registerFactory(() => ConsultationsArchiveCubit(consultationsRepository: sl())); sl.registerFactory(() => ConsultantFinancesCubit(consultationsRepository: sl())); sl.registerFactory(() => UserPurchasesCubit(consultationsRepository: sl())); + sl.registerFactory(() => ConsultantRatingCubit(sl())); // Repository sl.registerLazySingleton(() => ChatRepository(chatService: sl())); @@ -186,6 +190,7 @@ Future init() async { sl.registerLazySingleton(() => CreatePlanRepository(createPlanService: sl())); sl.registerLazySingleton(() => NotificationsRepository(notificationsService: sl())); sl.registerFactory(() => MyRecipesRepository(myRecipesService: sl())); + sl.registerFactory(() => ConsultantRatingRepository(sl())); // Services sl.registerLazySingleton(() => ChatService(sl())); @@ -214,8 +219,9 @@ Future init() async { sl.registerLazySingleton(() => CancelPlanService(sl())); sl.registerLazySingleton(() => CreatePlanService(sl())); sl.registerLazySingleton(() => NotificationsService(sl())); - sl.registerLazySingleton(() => MyRecipesService(sl())); sl.registerLazySingleton(() => FirebaseApi()); + sl.registerLazySingleton(() => MyRecipesService(sl())); + sl.registerLazySingleton(() => ConsultantRatingService(sl())); // External sl.registerLazySingleton( diff --git a/lib/features/consultant_rating/models/rating_model.dart b/lib/features/consultant_rating/models/rating_model.dart index 164008dd..870d1c82 100644 --- a/lib/features/consultant_rating/models/rating_model.dart +++ b/lib/features/consultant_rating/models/rating_model.dart @@ -1,16 +1,42 @@ +import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; part 'rating_model.freezed.dart'; part 'rating_model.g.dart'; @freezed -abstract class RatingModel with _$RatingModel { +abstract class RatingPaginationModel with _$RatingPaginationModel { + factory RatingPaginationModel({ + required List data, + }) = _RatingPaginationModel; + factory RatingPaginationModel.fromJson(Map json) => _$RatingPaginationModelFromJson(json); +} + +@freezed +abstract class RatingModel with _$RatingModel { factory RatingModel({ - required DateTime date, - required String title, - required int amount, + @JsonKey(name: 'created_at', fromJson: parseDate) required DateTime date, + @JsonKey(name: 'log_data') required RatingEnum type, + @JsonKey(name: 'log_scores') required int amount, }) = _RatingModel; factory RatingModel.fromJson(Map json) => _$RatingModelFromJson(json); -} \ No newline at end of file +} + +enum RatingEnum { + @JsonValue('ADDED_RECIPE') + addedRecipe, + @JsonValue('ADDED_PUBLIC_DIET') + addedPublicDiet, + @JsonValue('ADDED_BLOG_POST') + addedBlogPost, + @JsonValue('INVITED_A_NEW_USER') + invitedUser, + @JsonValue('HAS_MEDICAL_DEGREE') + hasMedicalDegree, + @JsonValue('CONSULTED') + consulted, + @JsonValue('PRODUCT_NUTRITINAL_VALUE_APPROVED') + addedGlossary; +} diff --git a/lib/features/consultant_rating/models/statistics_model.dart b/lib/features/consultant_rating/models/statistics_model.dart new file mode 100644 index 00000000..bbac675b --- /dev/null +++ b/lib/features/consultant_rating/models/statistics_model.dart @@ -0,0 +1,19 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'statistics_model.freezed.dart'; +part 'statistics_model.g.dart'; + +@freezed +abstract class StatisticsModel with _$StatisticsModel { + + factory StatisticsModel({ + @JsonKey(name: 'count_referal') int? countReferal, + @JsonKey(name: 'count_blog_posts') int? blogPostsCount, + @JsonKey(name: 'count_recipes') int? recipesCount, + @JsonKey(name: 'count_diets') int? dietsCount, + @JsonKey(name: 'count_consultants') int? consultationsCount, + @JsonKey(name: 'count_products_nutritional_values') int? qwe, + }) = _StatisticsModel; + + factory StatisticsModel.fromJson(Map json) => _$StatisticsModelFromJson(json); +} \ No newline at end of file diff --git a/lib/features/consultant_rating/presentation/consultant_rating_screen.dart b/lib/features/consultant_rating/presentation/consultant_rating_screen.dart index 285e5d63..8c6cb3d4 100644 --- a/lib/features/consultant_rating/presentation/consultant_rating_screen.dart +++ b/lib/features/consultant_rating/presentation/consultant_rating_screen.dart @@ -1,12 +1,16 @@ +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/custom_outlined_button.dart'; -import 'package:eatvio_app/features/consultant_rating/models/rating_model.dart'; +import 'package:eatvio_app/core/widgets/data_error_widget.dart'; +import 'package:eatvio_app/features/consultant_rating/presentation/cubit/consultant_rating_cubit.dart'; +import 'package:eatvio_app/features/consultant_rating/presentation/cubit/consultant_rating_state.dart'; import 'package:eatvio_app/features/consultant_rating/presentation/widgets/rating_history_widget.dart'; import 'package:eatvio_app/features/consultant_rating/presentation/widgets/rating_widget.dart'; import 'package:eatvio_app/features/consultant_rating/presentation/widgets/statistic_widget.dart'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; class ConsultantRatingScreen extends StatelessWidget { static const name = 'consultantRatingScreen'; @@ -15,7 +19,10 @@ class ConsultantRatingScreen extends StatelessWidget { @override Widget build(BuildContext context) { - return const ConsultantRatingPageContent(); + return BlocProvider( + create: (context) => sl()..getConsultantStatistics()..getScoreLogs(), + child: const ConsultantRatingPageContent(), + ); } } @@ -25,52 +32,56 @@ class ConsultantRatingPageContent extends StatelessWidget { @override Widget build(BuildContext context) { final s = S.of(context); - return Scaffold( - backgroundColor: appTheme.gray100, - appBar: AppBar( - centerTitle: true, - title: Text( - s.rating_and_statistics, - style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), - ), - ), - body: SingleChildScrollView( - child: Column( - children: [ - const SizedBox(height: 16), - const RatingWidget( - rating: 1058, + return BlocBuilder( + builder: (context, state) { + return Scaffold( + backgroundColor: appTheme.gray100, + appBar: AppBar( + centerTitle: true, + title: Text( + s.rating_and_statistics, + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), ), - const SizedBox(height: 16), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: CustomOutlinedButton( - text: s.copy_referral_link, - height: 52, - buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), - buttonStyle: ButtonStyle( - side: const WidgetStatePropertyAll(BorderSide.none), - backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), + ), + body: state.error != null + ? DataErrorWidget(onReload: () async { + await context.read().getConsultantStatistics(); + if (!context.mounted) return; + await context.read().getScoreLogs(); + }) + : SingleChildScrollView( + child: Column( + children: [ + const SizedBox(height: 16), + const RatingWidget( + rating: 1058, + ), + const SizedBox(height: 16), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: CustomOutlinedButton( + text: s.copy_referral_link, + height: 52, + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), + buttonStyle: ButtonStyle( + side: const WidgetStatePropertyAll(BorderSide.none), + backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), + ), + onPressed: () {}, + ), + ), + const SizedBox(height: 16), + StatisticWidget(isLoading: state.isStatisticsLoading, statistics: state.statistics,), + const SizedBox(height: 16), + RatingHistoryWidget( + isLoading: state.isRatingLoading, + ratingHistory: state.ratings, + ), + ], + ), ), - onPressed: () {}, - ), - ), - const SizedBox(height: 16), - const StatisticWidget(), - const SizedBox(height: 16), - RatingHistoryWidget( - ratingHistory: [ - RatingModel(date: DateTime.now(), title: ' djkasjdasjkdkjasjkdjkaskjdjklasjkld', amount: 201300), - RatingModel(date: DateTime.now(), title: 'djkasjdasjkdkjasjkdjkaskjdjklasjkld', amount: 89323), - RatingModel(date: DateTime.now(), title: 'djkasjdasjkdkjasjkdjkaskjdjklasjkld', amount: 20), - RatingModel(date: DateTime.now(), title: 'djkasjdasjkdkjasjkdjkaskjdjklasjkld', amount: 1212), - RatingModel(date: DateTime.now(), title: 'qweqweeq', amount: 323), - RatingModel(date: DateTime.now(), title: 'ee', amount: 1), - ], - ), - ], - ), - ), + ); + }, ); } } diff --git a/lib/features/consultant_rating/presentation/cubit/consultant_rating_cubit.dart b/lib/features/consultant_rating/presentation/cubit/consultant_rating_cubit.dart new file mode 100644 index 00000000..291f450c --- /dev/null +++ b/lib/features/consultant_rating/presentation/cubit/consultant_rating_cubit.dart @@ -0,0 +1,32 @@ +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:flutter_bloc/flutter_bloc.dart'; + +class ConsultantRatingCubit extends Cubit { + final ConsultantRatingRepository repository; + ConsultantRatingCubit(this.repository) : 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)); + } catch (e, st) { + emit(state.copyWith(isStatisticsLoading: true, error: e.toString())); + log(e.toString(), stackTrace: st); + } + } + + Future getScoreLogs({String? date}) async { + try { + emit(state.copyWith(isRatingLoading: true)); + final scoreLogs = await repository.getScoreLogs(date: date); + emit(state.copyWith(isRatingLoading: false, ratings: scoreLogs, error: null)); + } catch (e, st) { + emit(state.copyWith(isRatingLoading: 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 new file mode 100644 index 00000000..20cc458f --- /dev/null +++ b/lib/features/consultant_rating/presentation/cubit/consultant_rating_state.dart @@ -0,0 +1,16 @@ +import 'package:eatvio_app/features/consultant_rating/models/rating_model.dart'; +import 'package:eatvio_app/features/consultant_rating/models/statistics_model.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'consultant_rating_state.freezed.dart'; + +@freezed +abstract class ConsultantRatingState with _$ConsultantRatingState { + factory ConsultantRatingState({ + @Default(false) bool isStatisticsLoading, + @Default(false) bool isRatingLoading, + @Default(null) StatisticsModel? statistics, + @Default(null) String? error, + @Default([]) List ratings, + }) = _ConsultantRatingState; +} diff --git a/lib/features/consultant_rating/presentation/widgets/period_filter_widget.dart b/lib/features/consultant_rating/presentation/widgets/period_filter_widget.dart index 7ad0fa7e..3f15be8e 100644 --- a/lib/features/consultant_rating/presentation/widgets/period_filter_widget.dart +++ b/lib/features/consultant_rating/presentation/widgets/period_filter_widget.dart @@ -1,42 +1,29 @@ import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/core/utils/date_time_utils.dart'; import 'package:eatvio_app/core/widgets/custom_drop_down_button.dart'; +import 'package:eatvio_app/features/consultant_rating/presentation/cubit/consultant_rating_cubit.dart'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/material.dart'; import 'package:collection/collection.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; class PeriodFilterWidget extends StatefulWidget { - const PeriodFilterWidget({super.key}); - + const PeriodFilterWidget({super.key, required this.isStatistic}); + final bool isStatistic; @override State createState() => _PeriodFilterWidgetState(); } class _PeriodFilterWidgetState extends State { - final List periodDropDownTitles = []; - int periodDropDownIndex = 0; - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - final s = S.of(context); - if (periodDropDownTitles.isEmpty) { - periodDropDownTitles.addAll([ - s.for_whole_period_of_time, - s.for_a_year, - s.for_a_month, - s.for_a_week, - ]); - } - } - + PeriodFilterStatus _currentState = PeriodFilterStatus.all; @override Widget build(BuildContext context) { final s = S.of(context); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - CustomDropDownButton( + CustomDropDownButton( hint: Text( s.choose_diet_duration, style: TextStyles.textSMedium.copyWith(color: appTheme.black), @@ -44,24 +31,59 @@ class _PeriodFilterWidgetState extends State { onChanged: (value) { if (value != null) { setState(() { - periodDropDownIndex = periodDropDownTitles.indexOf(value); + _currentState = value; }); + if (widget.isStatistic) { + if (value == PeriodFilterStatus.all) { + context.read().getConsultantStatistics(); + return; + } + context.read().getConsultantStatistics(date: value.value); + return; + } + if (value == PeriodFilterStatus.all) { + context.read().getScoreLogs(); + return; + } + context.read().getScoreLogs(date: value.value); + return; } }, - items: periodDropDownTitles + items: PeriodFilterStatus.values .mapIndexed( (index, value) => DropdownMenuItem( - value: periodDropDownTitles[index], + value: PeriodFilterStatus.values[index], child: Text( - periodDropDownTitles[index], + PeriodFilterStatus.values[index].name, style: TextStyles.textMMedium.copyWith(color: appTheme.black), ), ), ) .toList(), - value: periodDropDownTitles[periodDropDownIndex], + value: _currentState, ), ], ); } } + +enum PeriodFilterStatus { + all, + month, + week, + year; + + String get name => switch (this) { + PeriodFilterStatus.all => 'Все периоды', + PeriodFilterStatus.month => 'За месяц', + PeriodFilterStatus.week => 'За неделю', + PeriodFilterStatus.year => 'За год', + }; + + String? get value => switch (this) { + PeriodFilterStatus.all => null, + PeriodFilterStatus.month => DateTime.now().subtract(const Duration(days: 30)).format('yyyy-MM-dd'), + PeriodFilterStatus.week => DateTime.now().subtract(const Duration(days: 7)).format('yyyy-MM-dd'), + PeriodFilterStatus.year => DateTime.now().subtract(const Duration(days: 365)).format('yyyy-MM-dd'), + }; +} diff --git a/lib/features/consultant_rating/presentation/widgets/rating_history_item.dart b/lib/features/consultant_rating/presentation/widgets/rating_history_item.dart index 3a3cff6e..4bafd1b3 100644 --- a/lib/features/consultant_rating/presentation/widgets/rating_history_item.dart +++ b/lib/features/consultant_rating/presentation/widgets/rating_history_item.dart @@ -11,7 +11,6 @@ class RatingHistoryItem extends StatelessWidget { @override Widget build(BuildContext context) { return Container( - height: 80, padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: appTheme.gray100, @@ -29,7 +28,7 @@ class RatingHistoryItem extends StatelessWidget { date.format('dd.MM.yyyy'), style: TextStyles.textXSSemibold.copyWith(color: appTheme.gray), ), - const Spacer(), + const SizedBox(height: 12), Text( title, style: TextStyles.textMMedium.copyWith(color: appTheme.black), diff --git a/lib/features/consultant_rating/presentation/widgets/rating_history_widget.dart b/lib/features/consultant_rating/presentation/widgets/rating_history_widget.dart index 0413dda5..d6f8969b 100644 --- a/lib/features/consultant_rating/presentation/widgets/rating_history_widget.dart +++ b/lib/features/consultant_rating/presentation/widgets/rating_history_widget.dart @@ -1,5 +1,6 @@ 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/loader.dart'; import 'package:eatvio_app/core/widgets/main_background_bouble.dart'; import 'package:eatvio_app/features/consultant_rating/models/rating_model.dart'; import 'package:eatvio_app/features/consultant_rating/presentation/widgets/period_filter_widget.dart'; @@ -9,7 +10,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; class RatingHistoryWidget extends StatelessWidget { - const RatingHistoryWidget({super.key, required this.ratingHistory}); + const RatingHistoryWidget({super.key, required this.ratingHistory, required this.isLoading}); + final bool isLoading; final List ratingHistory; @override Widget build(BuildContext context) { @@ -20,50 +22,60 @@ class RatingHistoryWidget extends StatelessWidget { right: 16, top: 16, ), - child: Column( - children: [ - Row( - children: [ - Text( - s.rating_history, - style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), - ), - const Spacer(), - SvgPicture.asset( - 'assets/icons/question_mark.svg', - height: 22, - width: 22, - colorFilter: ColorFilter.mode( - appTheme.blueA400, - BlendMode.srcIn, + child: isLoading + ? const Center(child: Loader()) + : Column( + children: [ + Row( + children: [ + Text( + s.rating_history, + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), + ), + const Spacer(), + SvgPicture.asset( + 'assets/icons/question_mark.svg', + height: 22, + width: 22, + colorFilter: ColorFilter.mode( + appTheme.blueA400, + BlendMode.srcIn, + ), + ), + ], ), - ), - ], - ), - const SizedBox( - height: 16, - ), - const PeriodFilterWidget(), - const SizedBox( - height: 12, - ), - ListView.builder( - physics: const NeverScrollableScrollPhysics(), - itemBuilder: (context, index) { - return Padding( - padding: index == ratingHistory.length - 1 ? EdgeInsets.zero : const EdgeInsets.only(bottom: 12), - child: RatingHistoryItem( - title: ratingHistory[index].title, - amount: ratingHistory[index].amount, - date: ratingHistory[index].date, + const SizedBox( + height: 16, ), - ); - }, - itemCount: ratingHistory.length, - shrinkWrap: true, - ), - ], - ), + const PeriodFilterWidget(isStatistic: false), + const SizedBox( + height: 12, + ), + ListView.builder( + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) { + return Padding( + padding: index == ratingHistory.length - 1 ? EdgeInsets.zero : const EdgeInsets.only(bottom: 12), + child: RatingHistoryItem( + title: switch (ratingHistory[index].type) { + RatingEnum.addedRecipe => 'Добавление рецепта', + RatingEnum.addedPublicDiet => 'Добавление публичной диеты', + RatingEnum.addedBlogPost => 'Добавление поста', + RatingEnum.invitedUser => 'Регистрация по реф. ссылке', + RatingEnum.hasMedicalDegree => 'Получение мед. степени', + RatingEnum.consulted => 'Консультация', + RatingEnum.addedGlossary => 'Добавление продукта в глоссарий', + }, + amount: ratingHistory[index].amount, + date: ratingHistory[index].date, + ), + ); + }, + itemCount: ratingHistory.length, + shrinkWrap: true, + ), + ], + ), ); } } diff --git a/lib/features/consultant_rating/presentation/widgets/statistic_widget.dart b/lib/features/consultant_rating/presentation/widgets/statistic_widget.dart index b7f97e2b..3be44129 100644 --- a/lib/features/consultant_rating/presentation/widgets/statistic_widget.dart +++ b/lib/features/consultant_rating/presentation/widgets/statistic_widget.dart @@ -1,6 +1,8 @@ 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/loader.dart'; import 'package:eatvio_app/core/widgets/main_background_bouble.dart'; +import 'package:eatvio_app/features/consultant_rating/models/statistics_model.dart'; import 'package:eatvio_app/features/consultant_rating/presentation/widgets/period_filter_widget.dart'; import 'package:eatvio_app/features/consultant_rating/presentation/widgets/statistic_bouble.dart'; import 'package:eatvio_app/generated/l10n.dart'; @@ -8,14 +10,15 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; class StatisticWidget extends StatelessWidget { - const StatisticWidget({super.key}); - + const StatisticWidget({super.key, required this.isLoading, required this.statistics}); + final bool isLoading; + final StatisticsModel? statistics; @override Widget build(BuildContext context) { final s = S.of(context); return MainBackgroundBouble( padding: const EdgeInsets.all(16), - child: Column( + child: isLoading ? const Center(child: Loader()) : Column( children: [ Row( children: [ @@ -38,15 +41,17 @@ class StatisticWidget extends StatelessWidget { const SizedBox( height: 16, ), - const PeriodFilterWidget(), + const PeriodFilterWidget( + isStatistic: true, + ), const SizedBox( height: 12, ), Row( children: [ - Expanded(child: StatisticBouble(number: 12, title: s.invited_people_count)), + Expanded(child: StatisticBouble(number: statistics?.countReferal ?? 0, title: s.invited_people_count)), const SizedBox(width: 8), - Expanded(child: StatisticBouble(number: 12, title: s.published_posts_count)), + Expanded(child: StatisticBouble(number: statistics?.blogPostsCount ?? 0, title: s.published_posts_count)), ], ), const SizedBox( @@ -54,9 +59,9 @@ class StatisticWidget extends StatelessWidget { ), Row( children: [ - Expanded(child: StatisticBouble(number: 12, title: s.published_recipes_count)), + Expanded(child: StatisticBouble(number: statistics?.recipesCount ?? 0, title: s.published_recipes_count)), const SizedBox(width: 8), - Expanded(child: StatisticBouble(number: 12, title: s.published_diets_count)), + Expanded(child: StatisticBouble(number: statistics?.dietsCount ?? 0, title: s.published_diets_count)), ], ), const SizedBox( @@ -64,9 +69,9 @@ class StatisticWidget extends StatelessWidget { ), Row( children: [ - Expanded(child: StatisticBouble(number: 12, title: s.consultations_count)), + Expanded(child: StatisticBouble(number: statistics?.consultationsCount ?? 0, title: s.consultations_count)), const SizedBox(width: 8), - Expanded(child: StatisticBouble(number: 12, title: s.active_consultations_count)), + Expanded(child: StatisticBouble(number: statistics?.qwe ?? 0, title: s.active_consultations_count)), ], ), const SizedBox( diff --git a/lib/features/consultant_rating/repositories/consultant_rating_repository.dart b/lib/features/consultant_rating/repositories/consultant_rating_repository.dart new file mode 100644 index 00000000..b4ac3770 --- /dev/null +++ b/lib/features/consultant_rating/repositories/consultant_rating_repository.dart @@ -0,0 +1,26 @@ +import 'package:eatvio_app/features/consultant_rating/models/rating_model.dart'; +import 'package:eatvio_app/features/consultant_rating/models/statistics_model.dart'; +import 'package:eatvio_app/features/consultant_rating/services/consultant_rating_service.dart'; + +class ConsultantRatingRepository { + final ConsultantRatingService service; + + ConsultantRatingRepository(this.service); + + Future getConsultantStatistics({String? date}) async { + try { + return await service.getConsultantStatistics(date: date); + } catch (e) { + rethrow; + } + } + + Future> getScoreLogs({String? date}) async { + try { + final data = (await service.getScoreLogs(date: date)).data; + return data; + } catch (e) { + rethrow; + } + } +} diff --git a/lib/features/consultant_rating/services/consultant_rating_service.dart b/lib/features/consultant_rating/services/consultant_rating_service.dart new file mode 100644 index 00000000..c4412afb --- /dev/null +++ b/lib/features/consultant_rating/services/consultant_rating_service.dart @@ -0,0 +1,18 @@ +import 'package:dio/dio.dart'; +import 'package:eatvio_app/features/consultant_rating/models/rating_model.dart'; +import 'package:eatvio_app/features/consultant_rating/models/statistics_model.dart'; + +import 'package:retrofit/retrofit.dart'; + +part 'consultant_rating_service.g.dart'; + +@RestApi() +abstract class ConsultantRatingService { + factory ConsultantRatingService(Dio dio, {String baseUrl}) = _ConsultantRatingService; + + @GET('/api/consultants/rating/statistics') + Future getConsultantStatistics({@Query('filters[duration][\$gte]') String? date}); + + @GET('/api/consultants/score/logs') + Future getScoreLogs({@Query('filters[duration][\$gte]') String? date}); +} diff --git a/lib/features/menu/presentation/pages/menu_screen.dart b/lib/features/menu/presentation/pages/menu_screen.dart index f657c6a7..7409a3a5 100644 --- a/lib/features/menu/presentation/pages/menu_screen.dart +++ b/lib/features/menu/presentation/pages/menu_screen.dart @@ -4,6 +4,7 @@ import 'package:eatvio_app/core/token_storage.dart'; import 'package:eatvio_app/core/utils/image_constant.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/features/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/my_diets/presentation/my_diets_screen.dart'; @@ -49,6 +50,15 @@ class MenuScreen extends StatelessWidget { 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: () => GoRouter.of(context).push('/glossary-screen')), _divider(context), From 49827e979e81b37cbf6e7a5fd9bb0379547e7fda Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Wed, 2 Jul 2025 16:41:49 +0300 Subject: [PATCH 002/134] feat(qrcode): Issue-515 --- ios/Runner.xcodeproj/project.pbxproj | 6 +- ios/Runner/Info.plist | 2 +- .../auth/presentation/pages/auth_screen.dart | 73 ++++++++++++++++++- pubspec.yaml | 6 +- 4 files changed, 78 insertions(+), 9 deletions(-) diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 65f4f4f7..046c0614 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 = 38; + CURRENT_PROJECT_VERSION = 39; 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 = 38; + CURRENT_PROJECT_VERSION = 39; 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 = 38; + CURRENT_PROJECT_VERSION = 39; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 72d42aff..d7a769f2 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -21,7 +21,7 @@ CFBundleSignature ???? CFBundleVersion - 38 + 39 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/lib/features/auth/presentation/pages/auth_screen.dart b/lib/features/auth/presentation/pages/auth_screen.dart index 31028059..c0847e0e 100644 --- a/lib/features/auth/presentation/pages/auth_screen.dart +++ b/lib/features/auth/presentation/pages/auth_screen.dart @@ -258,8 +258,77 @@ class _AuthScreenState extends State { backgroundColor: state is AuthStateLoading ? Colors.black.withAlpha(50) : appTheme.blueA400, onPressed: state is AuthStateLoading ? null - : () { - GoRouter.of(context).pushNamed('scanQRCodeScreen'); + : () async { + await showDialog( + context: context, + builder: (BuildContext context) { + return Dialog( + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + 'Где взять QR-код?\n', + style: TextStyle( + fontSize: 18, + ), + ), + Text( + '1. Войдите на сайте $baseUrl в свой аккаунт\n\n2. Перейдите в настройки профиля\n\n3. В разделе "Доп информация" перейдите на "QR Login"'), + const SizedBox(height: 12), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + InkWell( + borderRadius: BorderRadius.circular(8), + onTap: () { + AppRouter.navigatorKey.currentContext?.pop(); + }, + child: Ink( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + ), + child: const Text( + 'Отмена', + style: TextStyle( + color: Colors.black, + fontWeight: FontWeight.w400, + ), + ), + ), + ), + const SizedBox(width: 8), + InkWell( + borderRadius: BorderRadius.circular(8), + onTap: () { + AppRouter.navigatorKey.currentContext?.pop(); + AppRouter.navigatorKey.currentContext?.pushNamed('scanQRCodeScreen'); + }, + child: Ink( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + ), + child: Text( + 'Перейти', + style: TextStyle( + color: appTheme.blueA400, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ], + ), + ], + ), + ), + ); + }, + ); }, label: state is AuthStateLoading ? const CupertinoActivityIndicator() diff --git a/pubspec.yaml b/pubspec.yaml index cdc5881d..d5c928e7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: eatvio_app description: A new Flutter project. -version: 1.1.15+45 +version: 1.1.16+46 publish_to: 'none' environment: @@ -58,7 +58,7 @@ dependencies: http: ^1.4.0 flutter_native_splash: ^2.4.6 webview_flutter: ^4.13.0 - sentry_flutter: ^9.2.0 + sentry_flutter: ^8.14.2 connectivity_plus: ^6.1.4 visibility_detector: ^0.4.0+2 firebase_core: ^3.15.0 @@ -83,7 +83,7 @@ dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^5.0.0 + flutter_lints: ^6.0.0 flutter: uses-material-design: true From ebe28136afb72b4b069602e70a95e0bb54f6fa89 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Fri, 4 Jul 2025 09:37:37 +0300 Subject: [PATCH 003/134] fix(password+recipe): Issue-221 and Issue-418 --- lib/core/app_router.dart | 2 +- ...success_published_recipe_bottom_sheet.dart | 10 +- .../auth/presentation/pages/auth_screen.dart | 2 +- .../pages/forget_password_screen.dart | 13 +- .../presentation/widgets/expandable_text.dart | 116 ++++++++++++------ .../pages/about_recipe_screen.dart | 33 ++--- .../presentation/pages/settings_screen.dart | 8 +- 7 files changed, 112 insertions(+), 72 deletions(-) diff --git a/lib/core/app_router.dart b/lib/core/app_router.dart index 70107e0a..223a8913 100644 --- a/lib/core/app_router.dart +++ b/lib/core/app_router.dart @@ -121,7 +121,7 @@ class AppRouter { GoRoute( path: 'forget-password-screen', name: 'forgetPasswordScreen', - builder: (context, state) => const ForgetPasswordScreen(), + builder: (context, state) => ForgetPasswordScreen(isAuth: state.extra as bool,), ), GoRoute( path: 'scan-qr-code-screen', diff --git a/lib/core/widgets/success_published_recipe_bottom_sheet.dart b/lib/core/widgets/success_published_recipe_bottom_sheet.dart index 9fb1ddeb..796e7d91 100644 --- a/lib/core/widgets/success_published_recipe_bottom_sheet.dart +++ b/lib/core/widgets/success_published_recipe_bottom_sheet.dart @@ -26,9 +26,13 @@ class SuccessPublishedRecipeBottomSheet extends StatelessWidget { s.recipe_publishing, style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), ), - Text( - '"$title" ${s.successfully_published}', - style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Text( + '"$title" ${s.successfully_published}', + textAlign: TextAlign.center, + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), + ), ), const SizedBox( height: 20, diff --git a/lib/features/auth/presentation/pages/auth_screen.dart b/lib/features/auth/presentation/pages/auth_screen.dart index c0847e0e..6b4ec260 100644 --- a/lib/features/auth/presentation/pages/auth_screen.dart +++ b/lib/features/auth/presentation/pages/auth_screen.dart @@ -141,7 +141,7 @@ class _AuthScreenState extends State { const SizedBox(height: 16), Center( child: GestureDetector( - onTap: () => GoRouter.of(context).goNamed('forgetPasswordScreen'), + onTap: () => GoRouter.of(context).goNamed('forgetPasswordScreen', extra: false), child: Text( 'Забыли пароль?', style: TextStyle( diff --git a/lib/features/auth/presentation/pages/forget_password_screen.dart b/lib/features/auth/presentation/pages/forget_password_screen.dart index 78313a15..d842fcfc 100644 --- a/lib/features/auth/presentation/pages/forget_password_screen.dart +++ b/lib/features/auth/presentation/pages/forget_password_screen.dart @@ -9,7 +9,8 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; class ForgetPasswordScreen extends StatefulWidget { - const ForgetPasswordScreen({super.key}); + final bool isAuth; + const ForgetPasswordScreen({super.key, required this.isAuth}); @override State createState() => _ForgetPasswordScreenState(); @@ -29,7 +30,7 @@ class _ForgetPasswordScreenState extends State { builder: (BuildContext context, ResetPasswordState state) { return Scaffold( appBar: AppBar( - title: const Text('Смена пароля'), + title: widget.isAuth ? const Text('Смена пароля') : const Text('Восстановление пароля'), centerTitle: false, ), body: state is ResetPasswordStateLoadingResetPassword || state is ResetPasswordStateLoadingSendEmailCode @@ -42,9 +43,9 @@ class _ForgetPasswordScreenState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ if (state is ResetPasswordStateCodeSendError || state is ResetPasswordStateEmpty) ...[ - const Text( - 'Забыли пароль? Укажите свой email,\nи мы пришлём код для сброса.', - style: TextStyle( + Text( + '${widget.isAuth ? '' : 'Забыли пароль? '}Укажите свой email,\nи мы пришлём код для сброса.', + style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w500, ), @@ -156,7 +157,7 @@ class _ForgetPasswordScreenState extends State { } if (state is ResetPasswordStateResetPassowrdSuccess) { GoRouter.of(context).pop(); - showSuccessToast('Пароль успешно сменён. Войдите в аккаунт'); + showSuccessToast('Пароль успешно сменён. ${widget.isAuth ? '' : 'Войдите в аккаунт'}'); } }, ), diff --git a/lib/features/diets/presentation/widgets/expandable_text.dart b/lib/features/diets/presentation/widgets/expandable_text.dart index 18de6ae5..5832ec86 100644 --- a/lib/features/diets/presentation/widgets/expandable_text.dart +++ b/lib/features/diets/presentation/widgets/expandable_text.dart @@ -10,7 +10,11 @@ class ExpandableText extends StatefulWidget { final String text; final int maxLines; - const ExpandableText({super.key, required this.text, this.maxLines = 3}); + const ExpandableText({ + super.key, + required this.text, + this.maxLines = 5, // по умолчанию 5 строк + }); @override State createState() => _ExpandableTextState(); @@ -18,6 +22,29 @@ class ExpandableText extends StatefulWidget { class _ExpandableTextState extends State { bool isExpanded = false; + bool showExpandButton = false; + + final GlobalKey _textKey = GlobalKey(); + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + WidgetsBinding.instance.addPostFrameCallback((_) => _checkTextOverflow()); + } + + void _checkTextOverflow() { + final textRenderObject = _textKey.currentContext?.findRenderObject(); + if (textRenderObject is RenderBox) { + final maxLinesHeight = widget.maxLines * TextStyles.textMMediumAuto.fontSize! * 1.2; + final actualHeight = textRenderObject.size.height; + + if (actualHeight > maxLinesHeight && !showExpandButton) { + setState(() { + showExpandButton = true; + }); + } + } + } void toggleExpansion() { setState(() { @@ -33,52 +60,63 @@ class _ExpandableTextState extends State { children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 16), - child: Stack( - children: [ - Text( - widget.text, - style: TextStyles.textMMediumAuto.copyWith(color: appTheme.black), - // style: { - // "*": Style( - // margin: Margins.all(0), - // color: Colors.black87, - // maxLines: isExpanded ? null : widget.maxLines, - // textOverflow: TextOverflow.clip, - // ), - // }, - ), - if (!isExpanded) - Positioned.fill( - child: Align( - alignment: Alignment.bottomCenter, - child: Container( - height: 35, - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [Colors.white.withValues(alpha: 0.0), Colors.white.withValues(alpha: 0.6), Colors.white], + child: AnimatedSize( + duration: const Duration(milliseconds: 200), + curve: Curves.easeInOut, + child: Stack( + children: [ + Text( + widget.text, + key: _textKey, + style: TextStyles.textMMediumAuto.copyWith(color: appTheme.black), + maxLines: isExpanded ? null : widget.maxLines, + overflow: TextOverflow.clip, + ), + if (!isExpanded && showExpandButton) + Positioned.fill( + child: Align( + alignment: Alignment.bottomCenter, + child: Container( + height: 35, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.white.withAlpha(0), + Colors.white.withAlpha((255 * 0.6).toInt()), + Colors.white, + ], + ), ), ), ), ), - ), - ], - ), - ), - Align( - alignment: Alignment.bottomCenter, - child: GestureDetector( - onTap: toggleExpansion, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text(isExpanded ? s.hide : s.expand, style: TextStyles.textMMedium.copyWith(color: appTheme.blueA400)), - Transform.rotate(angle: isExpanded ? math.pi : 0, child: SvgPicture.asset('assets/icons/arrow_down.svg', width: 12, height: 12)), ], ), ), ), + if (showExpandButton) + Align( + alignment: Alignment.bottomCenter, + child: GestureDetector( + onTap: toggleExpansion, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + isExpanded ? s.hide : s.expand, + style: TextStyles.textMMedium.copyWith(color: appTheme.blueA400), + ), + const SizedBox(width: 4), + Transform.rotate( + angle: isExpanded ? math.pi : 0, + child: SvgPicture.asset('assets/icons/arrow_down.svg', width: 12, height: 12), + ), + ], + ), + ), + ), ], ); } diff --git a/lib/features/recipe/presentation/pages/about_recipe_screen.dart b/lib/features/recipe/presentation/pages/about_recipe_screen.dart index b9328ba3..8c6a5e8b 100644 --- a/lib/features/recipe/presentation/pages/about_recipe_screen.dart +++ b/lib/features/recipe/presentation/pages/about_recipe_screen.dart @@ -205,14 +205,14 @@ class _AboutRecipeScreenState extends State { appBar: AppBar( centerTitle: true, leading: BackButton( - onPressed: () { - if (context.canPop()) { - GoRouter.of(context).pop(); - return; - } - GoRouter.of(context).go('/feed-screen'); - }, - ), + onPressed: () { + if (context.canPop()) { + GoRouter.of(context).pop(); + return; + } + GoRouter.of(context).go('/feed-screen'); + }, + ), title: Text( recipeState.recipe.title ?? '', maxLines: 2, @@ -356,6 +356,7 @@ class _AboutRecipeScreenState extends State { ), ), ), + const SizedBox(height: 12), ExpandableText(text: recipeState.recipe.description), const SizedBox(height: 20), Padding( @@ -400,14 +401,14 @@ class _AboutRecipeScreenState extends State { appBar: AppBar( centerTitle: true, leading: BackButton( - onPressed: () { - if (context.canPop()) { - GoRouter.of(context).pop(); - return; - } - GoRouter.of(context).go('/feed-screen'); - }, - ), + onPressed: () { + if (context.canPop()) { + GoRouter.of(context).pop(); + return; + } + GoRouter.of(context).go('/feed-screen'); + }, + ), title: Text( '', maxLines: 2, diff --git a/lib/features/settings/presentation/pages/settings_screen.dart b/lib/features/settings/presentation/pages/settings_screen.dart index c5c9dc99..67460955 100644 --- a/lib/features/settings/presentation/pages/settings_screen.dart +++ b/lib/features/settings/presentation/pages/settings_screen.dart @@ -164,12 +164,8 @@ class _SettingsScreenState extends State { _divider(context), _tile( onTap: () => GoRouter.of(context).pushNamed( - 'changePasswordScreen', - extra: SettingsEntity( - profileSettingsModel: state.profileSettings, - profileDetailsModel: state.profileDetails, - settingsCubit: settingsCubit, - ), + 'forgetPasswordScreen', + extra: true, ), title: 'Сменить пароль', ), From cfe6b50c2814502524d72f3fb0fd4ab41309da2e Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Fri, 4 Jul 2025 10:54:00 +0300 Subject: [PATCH 004/134] fix(profile): Issue-317 --- lib/features/profile/presentation/pages/profile_screen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/features/profile/presentation/pages/profile_screen.dart b/lib/features/profile/presentation/pages/profile_screen.dart index 359bd21a..7c94e537 100644 --- a/lib/features/profile/presentation/pages/profile_screen.dart +++ b/lib/features/profile/presentation/pages/profile_screen.dart @@ -216,7 +216,7 @@ class _ProfileScreenState extends State { Scaffold _myProfileScreen(ProfileStateLoaded state) { return Scaffold( backgroundColor: Colors.grey.shade200, - appBar: AppBar(toolbarHeight: 0), + appBar: AppBar(), body: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, From 478ac139ddc458c4cc26cee6c97a02e8b89a9467 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Fri, 4 Jul 2025 16:27:18 +0300 Subject: [PATCH 005/134] feat(chat): WIP: Issue-1594 --- lib/core/app_router.dart | 4 ++-- lib/core/const.dart | 2 +- lib/core/token_storage.dart | 22 +++++++++++++++++++ lib/core/websockets/ws_manager.dart | 21 +++++++++++++++++- lib/features/chat/models/chat_room_model.dart | 2 +- .../models/chat_room_pagination_model.dart | 14 +----------- .../chat/models/chat_room_settings_model.dart | 5 +---- .../bloc/chat_room_cubit/chat_room_cubit.dart | 16 +++++++------- .../bloc/chats_cubit/chats_cubit.dart | 10 ++++----- .../chat/presentation/pages/chat_screen.dart | 10 ++++----- .../presentation/pages/empty_chat_screen.dart | 8 +++---- .../widgets/chat_entry_widget.dart | 2 +- .../chat/repositories/chat_repository.dart | 18 ++++++++------- lib/features/chat/services/chat_service.dart | 19 +++++----------- .../presentation/pages/about_plan_screen.dart | 2 +- .../bloc/profile_cubit/profile_cubit.dart | 2 +- .../widgets/sub_action_button_widget.dart | 2 +- 17 files changed, 90 insertions(+), 69 deletions(-) diff --git a/lib/core/app_router.dart b/lib/core/app_router.dart index 223a8913..9dc8612f 100644 --- a/lib/core/app_router.dart +++ b/lib/core/app_router.dart @@ -373,10 +373,10 @@ class AppRouter { name: 'chatScreen', builder: (context, state) { final data = state.extra as Map; - final chatId = data['chatId'] as String; + final uuid = data['uuid'] as String; final sendInitialMessage = data['sendInitialMessage'] as bool?; return ChatScreen( - chatId: chatId, + uuid: uuid, sendInitialMessage: sendInitialMessage, ); }, diff --git a/lib/core/const.dart b/lib/core/const.dart index 618a9866..8f5ddb59 100644 --- a/lib/core/const.dart +++ b/lib/core/const.dart @@ -1,4 +1,4 @@ -String baseUrl = 'https://eatvio.ru'; +String baseUrl = 'https://mobile-stage.eatvio.ru'; const List durationValues = [7, 14, 21]; diff --git a/lib/core/token_storage.dart b/lib/core/token_storage.dart index cbfb8abb..30906264 100644 --- a/lib/core/token_storage.dart +++ b/lib/core/token_storage.dart @@ -21,3 +21,25 @@ abstract class TokenStorage { static Future removeTokenInStorage() async => (await const FlutterSecureStorage().delete(key: 'token')); } + +abstract class SocketIdStorage { + static Future haveSocketIdInStorage() async { + try { + if (await const FlutterSecureStorage().read(key: 'socketId') == null) { + return false; + } else { + return true; + } + } catch (e) { + return false; + } + } + + static Future getSocketIdFromStorage() async => await const FlutterSecureStorage().read(key: 'socketId'); + + static Future saveSocketIdInStorage(String socketId) async { + await const FlutterSecureStorage().write(key: 'socketId', value: socketId); + } + + static Future removeSocketIdInStorage() async => (await const FlutterSecureStorage().delete(key: 'socketId')); +} diff --git a/lib/core/websockets/ws_manager.dart b/lib/core/websockets/ws_manager.dart index 73431542..0a5f7241 100644 --- a/lib/core/websockets/ws_manager.dart +++ b/lib/core/websockets/ws_manager.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'dart:developer'; import 'package:dart_pusher_channels/dart_pusher_channels.dart'; @@ -59,6 +60,24 @@ class WsManager { ), ); + String? extractSocketId(String? data) { + if (data == null) return null; + try { + final decoded = jsonDecode(data); + return decoded['socket_id'] as String?; + } catch (_) { + return null; + } + } + + client?.eventStream.listen((event) async { + if (event.name == 'pusher:connection_established') { + final data = event.data; + final socketId = extractSocketId(data); + await SocketIdStorage.saveSocketIdInStorage(socketId!); + } + }); + await client?.connect(); client = client; @@ -77,7 +96,7 @@ class WsManager { EasyDebounce.debounce( 'searchChatRoom', const Duration(milliseconds: 300), - () => sl().getChatRooms(), + () => sl().getChatRooms(), ); log(event.rootObject.toString()); }); diff --git a/lib/features/chat/models/chat_room_model.dart b/lib/features/chat/models/chat_room_model.dart index d7bf450a..dc091ea6 100644 --- a/lib/features/chat/models/chat_room_model.dart +++ b/lib/features/chat/models/chat_room_model.dart @@ -9,7 +9,7 @@ part 'chat_room_model.g.dart'; @freezed abstract class ChatRoomModel with _$ChatRoomModel { const factory ChatRoomModel({ - required String id, + required String id, @JsonKey(name: 'chat_room_setting') required ChatRoomSettingsModel chatRoomSettings, required Users users, @JsonKey(name: 'chosen_plan_id') required int? chosenPlanId, diff --git a/lib/features/chat/models/chat_room_pagination_model.dart b/lib/features/chat/models/chat_room_pagination_model.dart index 7bb8ceb5..699df5e8 100644 --- a/lib/features/chat/models/chat_room_pagination_model.dart +++ b/lib/features/chat/models/chat_room_pagination_model.dart @@ -8,29 +8,17 @@ part 'chat_room_pagination_model.g.dart'; @freezed abstract class ChatRoomPaginationModel with _$ChatRoomPaginationModel { const factory ChatRoomPaginationModel({ - @JsonKey(name: "chatRooms") required ChatRooms chatRooms, + @JsonKey(name: "data") required List data, @JsonKey(name: "number_of_unread_chat_rooms") required int unreadChatsCount, }) = _ChatRoomPreviewModel; factory ChatRoomPaginationModel.fromJson(Map json) => _$ChatRoomPaginationModelFromJson(json); } -@freezed -abstract class ChatRooms with _$ChatRooms { - const factory ChatRooms({ - @JsonKey(name: "data") required List data, - @JsonKey(name: "last_page") required int lastPage, - @JsonKey(name: "current_page") required int currentPage, - }) = _ChatRooms; - - factory ChatRooms.fromJson(Map json) => _$ChatRoomsFromJson(json); -} - @freezed abstract class ChatRoom with _$ChatRoom { const factory ChatRoom({ @JsonKey(name: "id") required String id, - @JsonKey(name: "room_name") required String roomName, @JsonKey(name: "last_message", fromJson: parseLastMessage) required ChatMessageModel? lastMessage, @JsonKey(name: "users") required Users users, @JsonKey(name: "unread_messages_count") required int unreadMessagesCount, diff --git a/lib/features/chat/models/chat_room_settings_model.dart b/lib/features/chat/models/chat_room_settings_model.dart index 5298d3d9..fa332843 100644 --- a/lib/features/chat/models/chat_room_settings_model.dart +++ b/lib/features/chat/models/chat_room_settings_model.dart @@ -6,11 +6,8 @@ part 'chat_room_settings_model.g.dart'; @freezed abstract class ChatRoomSettingsModel with _$ChatRoomSettingsModel { const factory ChatRoomSettingsModel({ - required int id, - @JsonKey(name: 'room_name') required String roomName, + required String name, required String state, - @JsonKey(name: 'access_level') required String accessLevel, - @JsonKey(name: 'updated_at') required String updatedAt, }) = _ChatRoomSettingsModel; factory ChatRoomSettingsModel.fromJson(Map json) => _$ChatRoomSettingsModelFromJson(json); diff --git a/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart b/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart index 63bf9e19..40ee862d 100644 --- a/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart +++ b/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart @@ -47,14 +47,14 @@ class ChatRoomCubit extends Cubit { _timer?.cancel(); } - Future loadNextPage(String chatId) async { + Future loadNextPage(String uuid) async { if (currentPage < lastPage && !isLoading) { isLoading = true; emit((state as ChatRoomStateLoaded).copyWith(isLoading: isLoading)); try { - final chatMessages = await chatRepository.getChatMessages(chatId, currentPage + 1); + final chatMessages = await chatRepository.getChatMessages(uuid, currentPage + 1); isLoading = false; emit( @@ -89,17 +89,17 @@ class ChatRoomCubit extends Cubit { } } - Future initChat(String chatId, {bool sendInitialMessage = false}) async { + Future initChat(String uuid, {bool sendInitialMessage = false}) async { try { - final chatRoom = await chatRepository.getChatRoom(chatId); + final chatRoom = await chatRepository.getChatRoom(uuid); ChosenPlanModel? chosenPlan; if (chatRoom.chosenPlanId != null) { chosenPlan = await chatRepository.getChosenPlan(id: chatRoom.chosenPlanId!); } - final chatMessages = await chatRepository.getChatMessages(chatId, currentPage); + final chatMessages = await chatRepository.getChatMessages(uuid, currentPage); lastPage = chatMessages.meta.lastPage; - await _subscribeToChatChannel(chatId: chatId); + await _subscribeToChatChannel(uuid: uuid); emit( ChatRoomState.loaded( chatRoom: chatRoom.copyWith(chosenPlan: chosenPlan), @@ -158,10 +158,10 @@ class ChatRoomCubit extends Cubit { } } - Future _subscribeToChatChannel({required String chatId}) async { + Future _subscribeToChatChannel({required String uuid}) async { final client = wsManager.client; if (client == null) return; - await wsManager.initPrivateDirectChatChannel(chatId); + await wsManager.initPrivateDirectChatChannel(uuid); wsManager.subscribeDirectChatChannel(); messagesSubscription = wsManager.directChatStream?.listen((event) { AudioPlayer().play((AssetSource('audio/recive_message.wav'))); diff --git a/lib/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart b/lib/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart index 3b50c995..24028797 100644 --- a/lib/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart +++ b/lib/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart @@ -43,10 +43,10 @@ class ChatsCubit extends Cubit { try { final chatRoomsPaginationModel = await chatRepository.getChatRooms(query: query); final unreadChats = chatRoomsPaginationModel.unreadChatsCount; - lastPage = chatRoomsPaginationModel.chatRooms.lastPage + 1; + // lastPage = chatRoomsPaginationModel.chatRooms.lastPage + 1; emit( state.copyWith( - chatRooms: chatRoomsPaginationModel.chatRooms.data, + chatRooms: chatRoomsPaginationModel.data, chatsWithUnreadMessagesCount: unreadChats, state: BaseState.success, ), @@ -65,10 +65,10 @@ class ChatsCubit extends Cubit { try { final chatRoomsPaginationModel = await chatRepository.getChatRooms(query: query); final unreadChats = chatRoomsPaginationModel.unreadChatsCount; - lastPage = chatRoomsPaginationModel.chatRooms.lastPage + 1; + // lastPage = chatRoomsPaginationModel.chatRooms.lastPage + 1; emit( state.copyWith( - chatRooms: chatRoomsPaginationModel.chatRooms.data, + chatRooms: chatRoomsPaginationModel.data, chatsWithUnreadMessagesCount: unreadChats, state: BaseState.success, ), @@ -90,7 +90,7 @@ class ChatsCubit extends Cubit { final unreadChats = chatRoomsPaginationModel.unreadChatsCount; emit( state.copyWith( - chatRooms: [...state.chatRooms, ...chatRoomsPaginationModel.chatRooms.data], + chatRooms: [...state.chatRooms, ...chatRoomsPaginationModel.data], chatsWithUnreadMessagesCount: unreadChats, state: BaseState.success, isLoadingMoreChats: false, diff --git a/lib/features/chat/presentation/pages/chat_screen.dart b/lib/features/chat/presentation/pages/chat_screen.dart index 55f74d70..c017f047 100644 --- a/lib/features/chat/presentation/pages/chat_screen.dart +++ b/lib/features/chat/presentation/pages/chat_screen.dart @@ -32,10 +32,10 @@ import 'package:intl/intl.dart'; import 'package:visibility_detector/visibility_detector.dart'; class ChatScreen extends StatefulWidget { - final String chatId; + final String uuid; final bool? sendInitialMessage; - const ChatScreen({required this.chatId, super.key, required this.sendInitialMessage}); + const ChatScreen({required this.uuid, super.key, required this.sendInitialMessage}); @override State createState() => _ChatScreenState(); @@ -64,14 +64,14 @@ class _ChatScreenState extends State { void initState() { super.initState(); scrollController.addListener(_onScroll); - chatRoomCubit.initChat(widget.chatId, sendInitialMessage: widget.sendInitialMessage ?? false); + chatRoomCubit.initChat(widget.uuid, sendInitialMessage: widget.sendInitialMessage ?? false); chatRoomCubit.setTimer(); _listener = AppLifecycleListener(onHide: _onHide, onResume: _onResume); } void _onScroll() { if (scrollController.offset >= scrollController.position.maxScrollExtent && !scrollController.position.outOfRange) { - chatRoomCubit.loadNextPage(widget.chatId); + chatRoomCubit.loadNextPage(widget.uuid); } } @@ -93,7 +93,7 @@ class _ChatScreenState extends State { if (state is ChatRoomStateLoaded) { return _buildChat(context, state); } else if (state is ChatRoomStateError) { - return Scaffold(appBar: AppBar(), body: DataErrorWidget(onReload: () => chatRoomCubit.initChat(widget.chatId))); + return Scaffold(appBar: AppBar(), body: DataErrorWidget(onReload: () => chatRoomCubit.initChat(widget.uuid))); } else { return const Scaffold(body: Loader()); } diff --git a/lib/features/chat/presentation/pages/empty_chat_screen.dart b/lib/features/chat/presentation/pages/empty_chat_screen.dart index 3195888c..7b543f86 100644 --- a/lib/features/chat/presentation/pages/empty_chat_screen.dart +++ b/lib/features/chat/presentation/pages/empty_chat_screen.dart @@ -145,7 +145,7 @@ class _EmptyChatScreenState extends State { AppRouter.navigatorKey.currentContext?.pushReplacementNamed( 'chatScreen', extra: { - 'chatId': chatIdOrError, + 'uuid': chatIdOrError, }, ); } @@ -186,7 +186,7 @@ class _EmptyChatScreenState extends State { AppRouter.navigatorKey.currentContext?.pushReplacementNamed( 'chatScreen', extra: { - 'chatId': chatIdOrError, + 'uuid': chatIdOrError, }, ); } @@ -214,7 +214,7 @@ class _EmptyChatScreenState extends State { AppRouter.navigatorKey.currentContext?.pushReplacementNamed( 'chatScreen', extra: { - 'chatId': chatIdOrError, + 'uuid': chatIdOrError, }, ); } @@ -264,7 +264,7 @@ class _EmptyChatScreenState extends State { AppRouter.navigatorKey.currentContext?.pushReplacementNamed( 'chatScreen', extra: { - 'chatId': chatIdOrError, + 'uuid': chatIdOrError, }, ); } diff --git a/lib/features/chat/presentation/widgets/chat_entry_widget.dart b/lib/features/chat/presentation/widgets/chat_entry_widget.dart index 44cc67f9..dacff9cb 100644 --- a/lib/features/chat/presentation/widgets/chat_entry_widget.dart +++ b/lib/features/chat/presentation/widgets/chat_entry_widget.dart @@ -24,7 +24,7 @@ class _ChatEntryWidgetState extends State { onTap: () => GoRouter.of(context).pushNamed( 'chatScreen', extra: { - 'chatId': widget.chatRoom.id.toString(), + 'uuid': widget.chatRoom.id.toString(), }, ), child: Container( diff --git a/lib/features/chat/repositories/chat_repository.dart b/lib/features/chat/repositories/chat_repository.dart index 54924fcd..d682c9a0 100644 --- a/lib/features/chat/repositories/chat_repository.dart +++ b/lib/features/chat/repositories/chat_repository.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:developer'; import 'dart:io'; import 'package:dio/dio.dart'; +import 'package:eatvio_app/core/token_storage.dart'; import 'package:eatvio_app/features/chat/models/chat_message_model.dart'; import 'package:eatvio_app/features/chat/models/chat_messages_model.dart'; import 'package:eatvio_app/features/chat/models/chat_room_model.dart'; @@ -47,10 +48,11 @@ class ChatRepository { } } - await chatService.sendMessageByUserSlug( - userSlug: slug, + await chatService.sendMessage( + slug: slug, data: data, mediaData: multipartFiles.isEmpty ? null : multipartFiles, + socketId: (await SocketIdStorage.getSocketIdFromStorage())!, ); } catch (e, st) { log(e.toString(), stackTrace: st); @@ -58,9 +60,9 @@ class ChatRepository { } } - Future getChatRoom(String chatId) async { + Future getChatRoom(String uuid) async { try { - return (await chatService.getChatRoom(id: chatId))['chatRoom']!; + return (await chatService.getChatRoom(uuid: uuid)); } catch (e) { rethrow; } @@ -74,9 +76,9 @@ class ChatRepository { } } - Future getChatMessages(String chatId, int page) async { + Future getChatMessages(String id, int page) async { try { - return (await chatService.getChatMessages(id: chatId, page: page)); + return (await chatService.getChatMessages(id: id, page: page)); } catch (e) { rethrow; } @@ -90,9 +92,9 @@ class ChatRepository { } } - Future markMessagesAsRead(String chatId, List messageIds) async { + Future markMessagesAsRead(String id, List messageIds) async { try { - await chatService.markMessagesAsRead(id: chatId, body: {'messages_ids': messageIds}); + await chatService.markMessagesAsRead(id: id, body: {'messages_ids': messageIds}); } catch (e) { rethrow; } diff --git a/lib/features/chat/services/chat_service.dart b/lib/features/chat/services/chat_service.dart index dcdab395..dee63eab 100644 --- a/lib/features/chat/services/chat_service.dart +++ b/lib/features/chat/services/chat_service.dart @@ -19,15 +19,16 @@ abstract class ChatService { @Query('find') String? query, }); - @GET('/api/chat/show/{id}') - Future> getChatRoom({ - @Path() required String id, + @GET('/api/chat/show/{uuid}') + Future getChatRoom({ + @Path() required String uuid, }); - @POST('/api/v2/chat/message/send/{id}') + @POST('/api/chat/message/send/{slug}') @MultiPart() Future sendMessage({ - @Path() required String id, + @Header('X-Socket-ID') required String socketId, + @Path() required String slug, @Part() String? data, @Part(name: 'media_data[]', contentType: 'multipart/form-data') List? mediaData, }); @@ -85,12 +86,4 @@ abstract class ChatService { Future getChatByUserSlug({ @Path() required String slug, }); - - @POST('/api/v2/chat/message/send/{userSlug}') - @MultiPart() - Future sendMessageByUserSlug({ - @Path() required String userSlug, - @Part() String? data, - @Part(name: 'media_data[]', contentType: 'multipart/form-data') List? mediaData, - }); } diff --git a/lib/features/plans/presentation/pages/about_plan_screen.dart b/lib/features/plans/presentation/pages/about_plan_screen.dart index d2984b68..15c12ecb 100644 --- a/lib/features/plans/presentation/pages/about_plan_screen.dart +++ b/lib/features/plans/presentation/pages/about_plan_screen.dart @@ -208,7 +208,7 @@ class _AboutPlanScreenState extends State { GoRouter.of(context).pushReplacementNamed( 'chatScreen', extra: { - 'chatId': chosenPlan.chatId, + 'uuid': chosenPlan.chatId, 'sendInitialMessage': true, }, ); 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 b088a073..36a98658 100644 --- a/lib/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart +++ b/lib/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart @@ -123,7 +123,7 @@ class ProfileCubit extends Cubit { AppRouter.navigatorKey.currentContext?.pushNamed( 'chatScreen', extra: { - 'chatId': chatIdOrError, + 'uuid': chatIdOrError, }, ); return chatIdOrError; diff --git a/lib/features/profile/presentation/widgets/sub_action_button_widget.dart b/lib/features/profile/presentation/widgets/sub_action_button_widget.dart index bca5e3da..55b4a095 100644 --- a/lib/features/profile/presentation/widgets/sub_action_button_widget.dart +++ b/lib/features/profile/presentation/widgets/sub_action_button_widget.dart @@ -69,7 +69,7 @@ class _SubActionButtonWidgetState extends State { GoRouter.of(context).pushNamed( 'chatScreen', extra: { - 'chatId': await sl().getChatRoomIdBySlug( + 'uuid': await sl().getChatRoomIdBySlug( author: Author( authorSlug: widget.slug, authorName: widget.name, From 0a636e05b8d41c2c0b806b0b405b25f49cdb57f0 Mon Sep 17 00:00:00 2001 From: Username1548 Date: Tue, 8 Jul 2025 09:17:20 +0700 Subject: [PATCH 006/134] feat(comments): added actions bottom sheet with different actions for comments, added copy, reply and complain --- assets/icons/reply.svg | 3 + .../comments_cubit/comments_cubit.dart | 31 ++- .../comments_cubit/comments_state.dart | 0 .../comments_actions_bottom_sheet.dart | 141 +++++++++++++ .../repositories/comments_repository.dart | 23 +++ .../comments/services/comments_service.dart | 15 ++ lib/core/const.dart | 3 +- lib/core/enums/complained_content.dart | 8 +- lib/core/locator_service.dart | 8 +- .../presentation/pages/blog_post_screen.dart | 4 +- .../presentation/cubit/complaining_cubit.dart | 6 + .../repositories/complaining_repository.dart | 12 ++ .../services/complaining_service.dart | 7 + .../presentation/pages/about_diet_screen.dart | 4 +- .../presentation/widgets/comments_widget.dart | 191 +++++++++++------- .../presentation/pages/about_plan_screen.dart | 4 +- .../pages/about_recipe_screen.dart | 4 +- lib/generated/intl/messages_en.dart | 5 + lib/generated/l10n.dart | 20 ++ lib/l10n/intl_en.arb | 6 +- 20 files changed, 395 insertions(+), 100 deletions(-) create mode 100644 assets/icons/reply.svg rename lib/{features/feed/presentation/bloc => core/comments}/comments_cubit/comments_cubit.dart (75%) rename lib/{features/feed/presentation/bloc => core/comments}/comments_cubit/comments_state.dart (100%) create mode 100644 lib/core/comments/presentation/comments_actions_bottom_sheet.dart create mode 100644 lib/core/comments/repositories/comments_repository.dart create mode 100644 lib/core/comments/services/comments_service.dart diff --git a/assets/icons/reply.svg b/assets/icons/reply.svg new file mode 100644 index 00000000..b62c76ae --- /dev/null +++ b/assets/icons/reply.svg @@ -0,0 +1,3 @@ + + + diff --git a/lib/features/feed/presentation/bloc/comments_cubit/comments_cubit.dart b/lib/core/comments/comments_cubit/comments_cubit.dart similarity index 75% rename from lib/features/feed/presentation/bloc/comments_cubit/comments_cubit.dart rename to lib/core/comments/comments_cubit/comments_cubit.dart index 5bb62317..2bc21d14 100644 --- a/lib/features/feed/presentation/bloc/comments_cubit/comments_cubit.dart +++ b/lib/core/comments/comments_cubit/comments_cubit.dart @@ -1,20 +1,25 @@ -import 'package:eatvio_app/features/feed/presentation/bloc/comments_cubit/comments_state.dart'; +import 'dart:developer'; + +import 'package:eatvio_app/core/comments/comments_cubit/comments_state.dart'; +import 'package:eatvio_app/core/comments/repositories/comments_repository.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/comments_widget.dart'; import 'package:eatvio_app/features/feed/repositories/feed_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class CommentsCubit extends Cubit { final FeedRepository feedRepository; + final CommentsRepository commentsRepository; CommentsCubit({ required this.feedRepository, + required this.commentsRepository, }) : super(const CommentsState()); void setReplyId(int? replyId) { emit((state as CommentsStateLoaded).copyWith(replyId: replyId)); } - Future getComments(String id, CommentSourceType commentSourceType, [bool isFirstFetch = false]) async { + Future getComments(String id, CommentSourceType commentSourceType, [bool isFirstFetch = false]) async { if (isFirstFetch) emit(const CommentsState.loading()); try { switch (commentSourceType) { @@ -27,7 +32,7 @@ class CommentsCubit extends Cubit { case CommentSourceType.recipe: emit(CommentsState.loaded(comment: await feedRepository.getRecipesComments(id: id))); break; - case CommentSourceType.plan: + case CommentSourceType.plan: emit(CommentsState.loaded(comment: await feedRepository.getPlansComments(slug: id))); break; } @@ -78,4 +83,24 @@ class CommentsCubit extends Cubit { emit(CommentsState.error(errorMessage: e.toString())); } } + + Future deleteComment({required int id}) async { + try { + await commentsRepository.deleteComment(id: id); + return true; + } catch (e, st) { + log(e.toString(), stackTrace: st); + } + return false; + } + + Future updateComment({required int id, required String body}) async { + try { + await commentsRepository.updateComment(id: id, body: body); + return true; + } catch (e, st) { + log(e.toString(), stackTrace: st); + } + return false; + } } diff --git a/lib/features/feed/presentation/bloc/comments_cubit/comments_state.dart b/lib/core/comments/comments_cubit/comments_state.dart similarity index 100% rename from lib/features/feed/presentation/bloc/comments_cubit/comments_state.dart rename to lib/core/comments/comments_cubit/comments_state.dart diff --git a/lib/core/comments/presentation/comments_actions_bottom_sheet.dart b/lib/core/comments/presentation/comments_actions_bottom_sheet.dart new file mode 100644 index 00000000..2fc9498e --- /dev/null +++ b/lib/core/comments/presentation/comments_actions_bottom_sheet.dart @@ -0,0 +1,141 @@ +import 'package:eatvio_app/core/theme/custom_text_style.dart'; +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/core/widgets/bottom_sheet_handler_bar.dart'; +import 'package:eatvio_app/core/widgets/custom_divider.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; + +void showCommentsActionsBottomSheet( + BuildContext context, { + required void Function()? reply, + required void Function() copy, + required void Function() edit, + required void Function() delete, + required void Function() complain, + 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), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: appTheme.whiteA700, + ), + child: CommentsActionsBottomSheet( + isMyComment: isMyComment, + reply: reply, + copy: copy, + edit: edit, + delete: delete, + complain: complain, + ), + )); +} + +class CommentsActionsBottomSheet extends StatelessWidget { + const CommentsActionsBottomSheet({ + super.key, + required this.isMyComment, + required this.reply, + required this.copy, + required this.edit, + required this.delete, + required this.complain, + }); + final bool isMyComment; + final void Function()? reply; + final void Function() copy; + final void Function() edit; + final void Function() delete; + final void Function() complain; + @override + Widget build(BuildContext context) { + final s = S.of(context); + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox( + height: 7, + ), + const BottomSheetHandlerBar(), + if (isMyComment) + _ActionWidget( + text: s.edit, + iconPath: 'assets/icons/edit_paint.svg', + onTap: edit, + ) + else if (reply != null) + _ActionWidget( + text: s.reply, + iconPath: 'assets/icons/reply.svg', + onTap: reply!, + ), + if (reply != null) const CustomDivider(), + _ActionWidget( + text: s.copy, + iconPath: 'assets/icons/copy2.svg', + onTap: copy, + ), + if (isMyComment) ...[ + if (reply != null) ...[ + const CustomDivider(), + _ActionWidget( + text: s.reply, + iconPath: 'assets/icons/reply.svg', + onTap: reply!, + ), + ], + const CustomDivider(), + _ActionWidget( + text: s.deleteComment, + iconPath: 'assets/icons/red_trash.svg', + onTap: delete, + ), + ] else ...[ + const CustomDivider(), + _ActionWidget( + text: s.complain, + iconPath: 'assets/icons/complain.svg', + onTap: complain, + ), + ] + ], + ), + ); + } +} + +class _ActionWidget extends StatelessWidget { + const _ActionWidget({required this.text, required this.iconPath, required this.onTap}); + final String text; + final String iconPath; + final void Function() onTap; + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + Navigator.pop(context); + onTap(); + }, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: Row( + children: [ + Text( + text, + style: TextStyles.textLMedium.copyWith(color: appTheme.black), + ), + const Spacer(), + SvgPicture.asset(iconPath) + ], + ), + ), + ); + } +} diff --git a/lib/core/comments/repositories/comments_repository.dart b/lib/core/comments/repositories/comments_repository.dart new file mode 100644 index 00000000..bc6e654b --- /dev/null +++ b/lib/core/comments/repositories/comments_repository.dart @@ -0,0 +1,23 @@ +import 'package:eatvio_app/core/comments/services/comments_service.dart'; + +class CommentsRepository { + final CommentsService service; + + CommentsRepository({required this.service}); + + Future deleteComment({required int id}) async { + try { + return service.deleteComment(id); + } catch (e) { + rethrow; + } + } + + Future updateComment({required int id, required String body}) async { + try { + return service.updateComment(id, body); + } catch (e) { + rethrow; + } + } +} diff --git a/lib/core/comments/services/comments_service.dart b/lib/core/comments/services/comments_service.dart new file mode 100644 index 00000000..d2db4218 --- /dev/null +++ b/lib/core/comments/services/comments_service.dart @@ -0,0 +1,15 @@ +import 'package:dio/dio.dart'; +import 'package:retrofit/retrofit.dart'; + +part 'comments_service.g.dart'; + +@RestApi() +abstract class CommentsService { + factory CommentsService(Dio dio, {String baseUrl}) = _CommentsService; + + @DELETE('/api/comments/{comment}') + Future deleteComment(@Path('id') int id); + + @PATCH('/api/comments/{comment}') + Future updateComment(@Path('id') int id, @Body() String body); +} diff --git a/lib/core/const.dart b/lib/core/const.dart index 618a9866..4f38a54e 100644 --- a/lib/core/const.dart +++ b/lib/core/const.dart @@ -1,4 +1,5 @@ -String baseUrl = 'https://eatvio.ru'; +// String baseUrl = 'https://eatvio.ru'; +String baseUrl = 'https://dev2.eatvio.com'; const List durationValues = [7, 14, 21]; diff --git a/lib/core/enums/complained_content.dart b/lib/core/enums/complained_content.dart index 66fe8ee1..4df54a33 100644 --- a/lib/core/enums/complained_content.dart +++ b/lib/core/enums/complained_content.dart @@ -1,7 +1 @@ -enum ComplainedContent { - diet, - plan, - recipe, - profile, - posts -} \ No newline at end of file +enum ComplainedContent { diet, plan, recipe, profile, posts, comment } diff --git a/lib/core/locator_service.dart b/lib/core/locator_service.dart index 9036809a..49c6c5dc 100644 --- a/lib/core/locator_service.dart +++ b/lib/core/locator_service.dart @@ -1,4 +1,6 @@ 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'; @@ -56,7 +58,7 @@ 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/features/feed/presentation/bloc/comments_cubit/comments_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'; @@ -128,7 +130,7 @@ Future init() async { 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())); + 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(() => RegisterConsultantCubit(consultantRepository: sl())); @@ -198,6 +200,7 @@ Future init() async { sl.registerFactory(() => MyRecipesRepository(myRecipesService: sl())); sl.registerFactory(() => ResetPasswordRepository(resetPasswordService: sl())); sl.registerLazySingleton(() => BlockedUsersRepository(blockedUserService: sl())); + sl.registerLazySingleton(() => CommentsRepository(service: sl())); // Services sl.registerLazySingleton(() => ChatService(sl())); @@ -230,6 +233,7 @@ Future init() async { sl.registerLazySingleton(() => FirebaseApi()); sl.registerLazySingleton(() => ResetPasswordService(sl())); sl.registerLazySingleton(() => BlockedUserService(sl())); + sl.registerLazySingleton(() => CommentsService(sl())); // External sl.registerLazySingleton( 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 aa4c82c2..25636a57 100644 --- a/lib/features/blog_post/presentation/pages/blog_post_screen.dart +++ b/lib/features/blog_post/presentation/pages/blog_post_screen.dart @@ -7,8 +7,8 @@ import 'package:eatvio_app/core/widgets/custom_popup.dart'; import 'package:eatvio_app/core/widgets/loader.dart'; import 'package:eatvio_app/features/blog_post/presentation/cubit/blog_post_cubit.dart'; import 'package:eatvio_app/features/blog_post/presentation/cubit/blog_post_state.dart'; -import 'package:eatvio_app/features/feed/presentation/bloc/comments_cubit/comments_cubit.dart'; -import 'package:eatvio_app/features/feed/presentation/bloc/comments_cubit/comments_state.dart'; +import 'package:eatvio_app/core/comments/comments_cubit/comments_cubit.dart'; +import 'package:eatvio_app/core/comments/comments_cubit/comments_state.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/comments_widget.dart'; import 'package:eatvio_app/core/locator_service.dart'; diff --git a/lib/features/complaining/presentation/cubit/complaining_cubit.dart b/lib/features/complaining/presentation/cubit/complaining_cubit.dart index 0a2827f3..896fa6e9 100644 --- a/lib/features/complaining/presentation/cubit/complaining_cubit.dart +++ b/lib/features/complaining/presentation/cubit/complaining_cubit.dart @@ -72,6 +72,12 @@ class ComplainingCubit extends Cubit { emit(state.copyWith(wasSentSuccessfully: true)); return; } + case ComplainedContent.comment: + if (id != null) { + await repository.sendCommentComplain(id: id, category: category.getValue(), body: state.complainInfo); + emit(state.copyWith(wasSentSuccessfully: true)); + return; + } } } } catch (e, st) { diff --git a/lib/features/complaining/repositories/complaining_repository.dart b/lib/features/complaining/repositories/complaining_repository.dart index 1b1a5d14..32aab577 100644 --- a/lib/features/complaining/repositories/complaining_repository.dart +++ b/lib/features/complaining/repositories/complaining_repository.dart @@ -64,4 +64,16 @@ class ComplainingRepository { rethrow; } } + + Future sendCommentComplain({ + required int id, + required String category, + required ComplainModel body, + }) async { + try { + await service.sendCommentComplain(id: id, category: category, body: body); + } catch (e) { + rethrow; + } + } } diff --git a/lib/features/complaining/services/complaining_service.dart b/lib/features/complaining/services/complaining_service.dart index 242545ad..276ac750 100644 --- a/lib/features/complaining/services/complaining_service.dart +++ b/lib/features/complaining/services/complaining_service.dart @@ -42,5 +42,12 @@ abstract class ComplainingService { @Path() required String category, @Body() required ComplainModel body, }); + + @POST('/api/comments/{id}/complain/{category}') + Future sendCommentComplain({ + @Path() required int id, + @Path() required String category, + @Body() required ComplainModel body, + }); } diff --git a/lib/features/diets/presentation/pages/about_diet_screen.dart b/lib/features/diets/presentation/pages/about_diet_screen.dart index b38d1afd..e4dd4a75 100644 --- a/lib/features/diets/presentation/pages/about_diet_screen.dart +++ b/lib/features/diets/presentation/pages/about_diet_screen.dart @@ -21,8 +21,8 @@ import 'package:eatvio_app/features/diets/presentation/widgets/info_bouble.dart' import 'package:eatvio_app/core/widgets/main_background_bouble.dart'; import 'package:eatvio_app/features/diets/presentation/widgets/start_diet_bottom_sheet.dart'; import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; -import 'package:eatvio_app/features/feed/presentation/bloc/comments_cubit/comments_cubit.dart'; -import 'package:eatvio_app/features/feed/presentation/bloc/comments_cubit/comments_state.dart'; +import 'package:eatvio_app/core/comments/comments_cubit/comments_cubit.dart'; +import 'package:eatvio_app/core/comments/comments_cubit/comments_state.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/comments_widget.dart'; import 'package:eatvio_app/generated/l10n.dart'; diff --git a/lib/features/feed/presentation/widgets/comments_widget.dart b/lib/features/feed/presentation/widgets/comments_widget.dart index 49f368cd..d3365b15 100644 --- a/lib/features/feed/presentation/widgets/comments_widget.dart +++ b/lib/features/feed/presentation/widgets/comments_widget.dart @@ -1,14 +1,19 @@ +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'; +import 'package:eatvio_app/core/utils/toasts.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/complaining/presentation/complaining_bottom_sheet.dart'; import 'package:eatvio_app/features/feed/models/comment_model.dart'; import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; -import 'package:eatvio_app/features/feed/presentation/bloc/comments_cubit/comments_cubit.dart'; +import 'package:eatvio_app/core/comments/comments_cubit/comments_cubit.dart'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; @@ -53,98 +58,128 @@ class _CommentsWidgetState extends State { ); } - Container _comment(S s, Comment comment, [double radius = 18, bool isChild = false]) { - return Container( - width: MediaQuery.of(context).size.width, - decoration: BoxDecoration( - color: widget.replyId == comment.id ? appTheme.blueA400.withValues(alpha: 0.1) : Colors.white, - borderRadius: BorderRadius.circular(16), - ), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - GestureDetector( - onTap: () { - GoRouter.of(context).pushNamed( - 'profileScreen', - extra: Author( - authorSlug: comment.authorSlug, - authorName: comment.authorName, - authorProfileIcon: comment.authorProfileIcon.contains('https') ? comment.authorProfileIcon : baseUrl + comment.authorProfileIcon, - authorIsMe: (sl().state as ProfileAvatarStateLoaded).profile.slug == comment.authorSlug ? true : false, - ), - ); - }, - child: CircleAvatar( - foregroundImage: NetworkImage( - comment.authorProfileIcon.contains('https') ? comment.authorProfileIcon : baseUrl + comment.authorProfileIcon, - ), - backgroundImage: const AssetImage('assets/images/no_avatar.png'), - radius: radius, + Widget _comment(S s, Comment comment, [double radius = 18, bool isChild = false]) { + final isMe = (sl().state as ProfileAvatarStateLoaded).profile.slug == comment.authorSlug; + return GestureDetector( + onTap: () { + showCommentsActionsBottomSheet( + context, + reply: !isChild + ? () { + context.read().setReplyId(comment.id); + } + : null, + copy: () { + Clipboard.setData(ClipboardData(text: comment.body)); + showInfoToast(s.link_was_copied); + }, + edit: () {}, + delete: () {}, + complain: () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (context) => ComplainingBottomSheet( + complainedContentType: ComplainedContent.comment, + id: comment.id, ), - ), - const SizedBox(width: 8), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Text( - comment.authorName, - style: TextStyle( - height: 1, - color: appTheme.gray, - fontSize: 14, - fontWeight: FontWeight.w500, + ); + }, + isMyComment: isMe, + ); + }, + child: Container( + width: MediaQuery.of(context).size.width, + decoration: BoxDecoration( + color: widget.replyId == comment.id ? appTheme.blueA400.withValues(alpha: 0.1) : Colors.white, + borderRadius: BorderRadius.circular(16), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + GestureDetector( + onTap: () { + GoRouter.of(context).pushNamed( + 'profileScreen', + extra: Author( + authorSlug: comment.authorSlug, + authorName: comment.authorName, + authorProfileIcon: comment.authorProfileIcon.contains('https') ? comment.authorProfileIcon : baseUrl + comment.authorProfileIcon, + authorIsMe: isMe, + ), + ); + }, + child: CircleAvatar( + foregroundImage: NetworkImage( + comment.authorProfileIcon.contains('https') ? comment.authorProfileIcon : baseUrl + comment.authorProfileIcon, ), + backgroundImage: const AssetImage('assets/images/no_avatar.png'), + radius: radius, ), - 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, + ), + const SizedBox(width: 8), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text( + comment.authorName, style: TextStyle( height: 1, - color: appTheme.black, - fontSize: 16, + color: appTheme.gray, + fontSize: 14, fontWeight: FontWeight.w500, ), ), - ), - const SizedBox(height: 4), - Row( - children: [ - Text( - comment.createdAt.format('dd MMMM', 'ru'), + 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.gray, - fontSize: 14, + color: appTheme.black, + fontSize: 16, fontWeight: FontWeight.w500, ), ), - const SizedBox(width: 16), - if (!isChild) - GestureDetector( - onTap: () { - context.read().setReplyId(comment.id); - }, - child: Text( - s.answer, - style: TextStyle( - height: 1, - color: appTheme.gray, - fontSize: 14, - fontWeight: FontWeight.w600, - ), + ), + const SizedBox(height: 4), + Row( + children: [ + Text( + comment.createdAt.format('dd MMMM', 'ru'), + style: TextStyle( + height: 1, + color: appTheme.gray, + fontSize: 14, + fontWeight: FontWeight.w500, ), ), - ], - ), - ], - ), - ], + const SizedBox(width: 16), + if (!isChild) + GestureDetector( + onTap: () { + context.read().setReplyId(comment.id); + }, + child: Text( + s.answer, + style: TextStyle( + height: 1, + color: appTheme.gray, + fontSize: 14, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + ], + ), + ], + ), ), ), ); diff --git a/lib/features/plans/presentation/pages/about_plan_screen.dart b/lib/features/plans/presentation/pages/about_plan_screen.dart index d2984b68..3903fc8d 100644 --- a/lib/features/plans/presentation/pages/about_plan_screen.dart +++ b/lib/features/plans/presentation/pages/about_plan_screen.dart @@ -15,8 +15,8 @@ import 'package:eatvio_app/features/diets/presentation/widgets/author_card.dart' import 'package:eatvio_app/features/diets/presentation/widgets/expandable_text.dart'; import 'package:eatvio_app/features/diets/presentation/widgets/info_bouble.dart'; import 'package:eatvio_app/core/widgets/main_background_bouble.dart'; -import 'package:eatvio_app/features/feed/presentation/bloc/comments_cubit/comments_cubit.dart'; -import 'package:eatvio_app/features/feed/presentation/bloc/comments_cubit/comments_state.dart'; +import 'package:eatvio_app/core/comments/comments_cubit/comments_cubit.dart'; +import 'package:eatvio_app/core/comments/comments_cubit/comments_state.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/comments_widget.dart'; import 'package:eatvio_app/features/plans/presentation/cubit/about_plan_cubit.dart'; diff --git a/lib/features/recipe/presentation/pages/about_recipe_screen.dart b/lib/features/recipe/presentation/pages/about_recipe_screen.dart index 8c6a5e8b..f23ac880 100644 --- a/lib/features/recipe/presentation/pages/about_recipe_screen.dart +++ b/lib/features/recipe/presentation/pages/about_recipe_screen.dart @@ -16,8 +16,8 @@ import 'package:eatvio_app/features/diets/presentation/widgets/author_card.dart' import 'package:eatvio_app/features/diets/presentation/widgets/expandable_text.dart'; import 'package:eatvio_app/features/diets/presentation/widgets/info_bouble.dart'; import 'package:eatvio_app/core/widgets/main_background_bouble.dart'; -import 'package:eatvio_app/features/feed/presentation/bloc/comments_cubit/comments_cubit.dart'; -import 'package:eatvio_app/features/feed/presentation/bloc/comments_cubit/comments_state.dart'; +import 'package:eatvio_app/core/comments/comments_cubit/comments_cubit.dart'; +import 'package:eatvio_app/core/comments/comments_cubit/comments_state.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/comments_widget.dart'; import 'package:eatvio_app/features/recipe/presentation/cubit/about_recipe_cubit.dart'; diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart index e318c962..73db6f27 100644 --- a/lib/generated/intl/messages_en.dart +++ b/lib/generated/intl/messages_en.dart @@ -200,6 +200,7 @@ class MessageLookup extends MessageLookupByLibrary { "continueButton": MessageLookupByLibrary.simpleMessage("Продолжить"), "continue_": MessageLookupByLibrary.simpleMessage("Продолжить"), "copied": MessageLookupByLibrary.simpleMessage("cкопирован"), + "copy": MessageLookupByLibrary.simpleMessage("Скопировать"), "copy_link": MessageLookupByLibrary.simpleMessage("Скопировать ссылку"), "copy_referral_link": MessageLookupByLibrary.simpleMessage( "Скопировать пригласительную ссылку", @@ -255,6 +256,9 @@ class MessageLookup extends MessageLookupByLibrary { ), "days": MessageLookupByLibrary.simpleMessage("дней"), "deception": MessageLookupByLibrary.simpleMessage("Обман"), + "deleteComment": MessageLookupByLibrary.simpleMessage( + "Удалить комментарий", + ), "deleted": MessageLookupByLibrary.simpleMessage("Удалена"), "diary": MessageLookupByLibrary.simpleMessage("Дневник"), "diet": MessageLookupByLibrary.simpleMessage("Диета"), @@ -619,6 +623,7 @@ class MessageLookup extends MessageLookupByLibrary { "removeFromActive": MessageLookupByLibrary.simpleMessage( "Снять с публикации", ), + "reply": MessageLookupByLibrary.simpleMessage("Ответить"), "save": MessageLookupByLibrary.simpleMessage("Сохранить"), "send": MessageLookupByLibrary.simpleMessage("Отправить"), "send_complain": MessageLookupByLibrary.simpleMessage("Отправить жалобу"), diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index 9f338062..5a4e420e 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -3394,6 +3394,26 @@ class S { args: [], ); } + + /// `Скопировать` + String get copy { + return Intl.message('Скопировать', name: 'copy', desc: '', args: []); + } + + /// `Ответить` + String get reply { + return Intl.message('Ответить', name: 'reply', desc: '', args: []); + } + + /// `Удалить комментарий` + String get deleteComment { + return Intl.message( + 'Удалить комментарий', + name: 'deleteComment', + desc: '', + args: [], + ); + } } class AppLocalizationDelegate extends LocalizationsDelegate { diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 96ddc4be..2052d17b 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -402,6 +402,10 @@ "allowAcessToGaleryDescription": "Предоставление доступа позволит вам добавлять фото к записям", "allowAccessToCamera": "Разрешите доступ к камере", "allowAccessToCameraDescription": "Предоставление доступа позволит вам делать фотографии и добавлять к записям", - "allowAccess": "Разрешить доступ" + "allowAccess": "Разрешить доступ", + "copy": "Скопировать", + "reply": "Ответить", + "deleteComment": "Удалить комментарий" + } From e04c5c97f95395ea4fa617c77d2ca6794d3c067a Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Tue, 8 Jul 2025 11:35:41 +0300 Subject: [PATCH 007/134] feat(chat): chat without delay --- lib/core/const.dart | 3 +- .../bloc/chat_room_cubit/chat_room_cubit.dart | 22 +- .../widgets/chat_bubble_widget.dart | 16 +- .../consultant_review_model.freezed.dart | 652 +++++++++++++++++- .../profile/models/profile_model.dart | 1 + .../services/profile_service_test.dart | 1 + 6 files changed, 685 insertions(+), 10 deletions(-) diff --git a/lib/core/const.dart b/lib/core/const.dart index 8f5ddb59..5d1aded6 100644 --- a/lib/core/const.dart +++ b/lib/core/const.dart @@ -1,4 +1,5 @@ -String baseUrl = 'https://mobile-stage.eatvio.ru'; +// String baseUrl = 'https://mobile-stage.eatvio.ru'; +String baseUrl = 'https://eatvio.ru'; const List durationValues = [7, 14, 21]; diff --git a/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart b/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart index 40ee862d..32244e78 100644 --- a/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart +++ b/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart @@ -189,12 +189,30 @@ class ChatRoomCubit extends Cubit { required String userSlug, }) async { try { + final compressedFiles = files != null ? await ImageCompressUtility.compressFilesList(List.from(files)) : null; + AudioPlayer().play((AssetSource('audio/send_message.wav'))); + emit( + (state as ChatRoomStateLoaded).copyWith( + chatMessages: [ + ChatMessageModel( + id: 1, + data: data ?? '', + mediaData: compressedFiles, + authorId: 1, + updatedAt: DateTime.now(), + isRead: true, + isCurrentUserAuthor: true, + roomId: '', + ), + ...(state as ChatRoomStateLoaded).chatMessages, + ], + ), + ); await chatRepository.sendMessage( data: data, - files: files != null ? await ImageCompressUtility.compressFilesList(List.from(files)) : null, + files: compressedFiles, slug: userSlug, ); - AudioPlayer().play((AssetSource('audio/send_message.wav'))); return true; } catch (e, st) { log(e.toString(), stackTrace: st); diff --git a/lib/features/chat/presentation/widgets/chat_bubble_widget.dart b/lib/features/chat/presentation/widgets/chat_bubble_widget.dart index 74e6f6a6..dd6c7b4c 100644 --- a/lib/features/chat/presentation/widgets/chat_bubble_widget.dart +++ b/lib/features/chat/presentation/widgets/chat_bubble_widget.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:cached_network_image/cached_network_image.dart'; import 'package:dismissible_page/dismissible_page.dart'; import 'package:eatvio_app/core/theme/custom_text_style.dart'; @@ -59,8 +61,9 @@ class ChatBubbleWidget extends StatelessWidget { ), ), child: Padding( - padding: - mediaData.isNotEmpty ? const EdgeInsets.all(8) : EdgeInsets.only(top: 8, bottom: 8, left: isCurrentUser ? 16 : 8, right: isCurrentUser ? 8 : 16), + padding: mediaData.isNotEmpty + ? const EdgeInsets.all(8) + : EdgeInsets.only(top: 8, bottom: 8, left: isCurrentUser ? 16 : 8, right: isCurrentUser ? 8 : 16), child: Column( crossAxisAlignment: isCurrentUser ? CrossAxisAlignment.end : CrossAxisAlignment.start, children: [ @@ -109,6 +112,7 @@ class ChatBubbleWidget extends StatelessWidget { PhotoViewScreen( imageUrls: mediaData.map((e) => '$e').toList(), index: index, + isLocal: mediaData[index] is File ? true : false, ), ); }, @@ -117,9 +121,11 @@ class ChatBubbleWidget extends StatelessWidget { transitionOnUserGestures: true, child: ClipRRect( borderRadius: BorderRadius.circular(16), - child: CachedNetworkImage( - imageUrl: '${mediaData[index]}', - ), + child: mediaData[index] is File + ? Image.file(mediaData[index]) + : CachedNetworkImage( + imageUrl: '${mediaData[index]}', + ), ), ), ); diff --git a/lib/features/diary/models/consultant_review_model.freezed.dart b/lib/features/diary/models/consultant_review_model.freezed.dart index a6f79469..36c72066 100644 --- a/lib/features/diary/models/consultant_review_model.freezed.dart +++ b/lib/features/diary/models/consultant_review_model.freezed.dart @@ -1,6 +1,5 @@ -// dart format width=80 -// coverage:ignore-file // GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file // ignore_for_file: type=lint // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark @@ -121,6 +120,172 @@ class _$ConsultantReviewModelCopyWithImpl<$Res> } } +/// Adds pattern-matching-related methods to [ConsultantReviewModel]. +extension ConsultantReviewModelPatterns on ConsultantReviewModel { + /// A variant of `map` that fallback to returning `orElse`. + /// + /// It is equivalent to doing: + /// ```dart + /// switch (sealedClass) { + /// case final Subclass value: + /// return ...; + /// case _: + /// return orElse(); + /// } + /// ``` + + @optionalTypeArgs + TResult maybeMap( + TResult Function(_ConsultantReviewModel value)? $default, { + required TResult orElse(), + }) { + final _that = this; + switch (_that) { + case _ConsultantReviewModel() when $default != null: + return $default(_that); + case _: + return orElse(); + } + } + + /// A `switch`-like method, using callbacks. + /// + /// Callbacks receives the raw object, upcasted. + /// It is equivalent to doing: + /// ```dart + /// switch (sealedClass) { + /// case final Subclass value: + /// return ...; + /// case final Subclass2 value: + /// return ...; + /// } + /// ``` + + @optionalTypeArgs + TResult map( + TResult Function(_ConsultantReviewModel value) $default, + ) { + final _that = this; + switch (_that) { + case _ConsultantReviewModel(): + return $default(_that); + case _: + throw StateError('Unexpected subclass'); + } + } + + /// A variant of `map` that fallback to returning `null`. + /// + /// It is equivalent to doing: + /// ```dart + /// switch (sealedClass) { + /// case final Subclass value: + /// return ...; + /// case _: + /// return null; + /// } + /// ``` + + @optionalTypeArgs + TResult? mapOrNull( + TResult? Function(_ConsultantReviewModel value)? $default, + ) { + final _that = this; + switch (_that) { + case _ConsultantReviewModel() when $default != null: + return $default(_that); + case _: + return null; + } + } + + /// A variant of `when` that fallback to an `orElse` callback. + /// + /// It is equivalent to doing: + /// ```dart + /// switch (sealedClass) { + /// case Subclass(:final field): + /// return ...; + /// case _: + /// return orElse(); + /// } + /// ``` + + @optionalTypeArgs + TResult maybeWhen( + TResult Function(String planName, int planDuration, int currentPlanDay, + ConsultantComments consultantComments)? + $default, { + required TResult orElse(), + }) { + final _that = this; + switch (_that) { + case _ConsultantReviewModel() when $default != null: + return $default(_that.planName, _that.planDuration, + _that.currentPlanDay, _that.consultantComments); + case _: + return orElse(); + } + } + + /// A `switch`-like method, using callbacks. + /// + /// As opposed to `map`, this offers destructuring. + /// It is equivalent to doing: + /// ```dart + /// switch (sealedClass) { + /// case Subclass(:final field): + /// return ...; + /// case Subclass2(:final field2): + /// return ...; + /// } + /// ``` + + @optionalTypeArgs + TResult when( + TResult Function(String planName, int planDuration, int currentPlanDay, + ConsultantComments consultantComments) + $default, + ) { + final _that = this; + switch (_that) { + case _ConsultantReviewModel(): + return $default(_that.planName, _that.planDuration, + _that.currentPlanDay, _that.consultantComments); + case _: + throw StateError('Unexpected subclass'); + } + } + + /// A variant of `when` that fallback to returning `null` + /// + /// It is equivalent to doing: + /// ```dart + /// switch (sealedClass) { + /// case Subclass(:final field): + /// return ...; + /// case _: + /// return null; + /// } + /// ``` + + @optionalTypeArgs + TResult? whenOrNull( + TResult? Function(String planName, int planDuration, int currentPlanDay, + ConsultantComments consultantComments)? + $default, + ) { + final _that = this; + switch (_that) { + case _ConsultantReviewModel() when $default != null: + return $default(_that.planName, _that.planDuration, + _that.currentPlanDay, _that.consultantComments); + case _: + return null; + } + } +} + /// @nodoc @JsonSerializable() class _ConsultantReviewModel implements ConsultantReviewModel { @@ -340,6 +505,172 @@ class _$ConsultantCommentsCopyWithImpl<$Res> } } +/// Adds pattern-matching-related methods to [ConsultantComments]. +extension ConsultantCommentsPatterns on ConsultantComments { + /// A variant of `map` that fallback to returning `orElse`. + /// + /// It is equivalent to doing: + /// ```dart + /// switch (sealedClass) { + /// case final Subclass value: + /// return ...; + /// case _: + /// return orElse(); + /// } + /// ``` + + @optionalTypeArgs + TResult maybeMap( + TResult Function(_ConsultantComments value)? $default, { + required TResult orElse(), + }) { + final _that = this; + switch (_that) { + case _ConsultantComments() when $default != null: + return $default(_that); + case _: + return orElse(); + } + } + + /// A `switch`-like method, using callbacks. + /// + /// Callbacks receives the raw object, upcasted. + /// It is equivalent to doing: + /// ```dart + /// switch (sealedClass) { + /// case final Subclass value: + /// return ...; + /// case final Subclass2 value: + /// return ...; + /// } + /// ``` + + @optionalTypeArgs + TResult map( + TResult Function(_ConsultantComments value) $default, + ) { + final _that = this; + switch (_that) { + case _ConsultantComments(): + return $default(_that); + case _: + throw StateError('Unexpected subclass'); + } + } + + /// A variant of `map` that fallback to returning `null`. + /// + /// It is equivalent to doing: + /// ```dart + /// switch (sealedClass) { + /// case final Subclass value: + /// return ...; + /// case _: + /// return null; + /// } + /// ``` + + @optionalTypeArgs + TResult? mapOrNull( + TResult? Function(_ConsultantComments value)? $default, + ) { + final _that = this; + switch (_that) { + case _ConsultantComments() when $default != null: + return $default(_that); + case _: + return null; + } + } + + /// A variant of `when` that fallback to an `orElse` callback. + /// + /// It is equivalent to doing: + /// ```dart + /// switch (sealedClass) { + /// case Subclass(:final field): + /// return ...; + /// case _: + /// return orElse(); + /// } + /// ``` + + @optionalTypeArgs + TResult maybeWhen( + TResult Function(String? dayComment, List mealsComments, + List portionsComments)? + $default, { + required TResult orElse(), + }) { + final _that = this; + switch (_that) { + case _ConsultantComments() when $default != null: + return $default( + _that.dayComment, _that.mealsComments, _that.portionsComments); + case _: + return orElse(); + } + } + + /// A `switch`-like method, using callbacks. + /// + /// As opposed to `map`, this offers destructuring. + /// It is equivalent to doing: + /// ```dart + /// switch (sealedClass) { + /// case Subclass(:final field): + /// return ...; + /// case Subclass2(:final field2): + /// return ...; + /// } + /// ``` + + @optionalTypeArgs + TResult when( + TResult Function(String? dayComment, List mealsComments, + List portionsComments) + $default, + ) { + final _that = this; + switch (_that) { + case _ConsultantComments(): + return $default( + _that.dayComment, _that.mealsComments, _that.portionsComments); + case _: + throw StateError('Unexpected subclass'); + } + } + + /// A variant of `when` that fallback to returning `null` + /// + /// It is equivalent to doing: + /// ```dart + /// switch (sealedClass) { + /// case Subclass(:final field): + /// return ...; + /// case _: + /// return null; + /// } + /// ``` + + @optionalTypeArgs + TResult? whenOrNull( + TResult? Function(String? dayComment, List mealsComments, + List portionsComments)? + $default, + ) { + final _that = this; + switch (_that) { + case _ConsultantComments() when $default != null: + return $default( + _that.dayComment, _that.mealsComments, _that.portionsComments); + case _: + return null; + } + } +} + /// @nodoc @JsonSerializable() class _ConsultantComments implements ConsultantComments { @@ -532,6 +863,163 @@ class _$MealCommentCopyWithImpl<$Res> implements $MealCommentCopyWith<$Res> { } } +/// Adds pattern-matching-related methods to [MealComment]. +extension MealCommentPatterns on MealComment { + /// A variant of `map` that fallback to returning `orElse`. + /// + /// It is equivalent to doing: + /// ```dart + /// switch (sealedClass) { + /// case final Subclass value: + /// return ...; + /// case _: + /// return orElse(); + /// } + /// ``` + + @optionalTypeArgs + TResult maybeMap( + TResult Function(_MealComment value)? $default, { + required TResult orElse(), + }) { + final _that = this; + switch (_that) { + case _MealComment() when $default != null: + return $default(_that); + case _: + return orElse(); + } + } + + /// A `switch`-like method, using callbacks. + /// + /// Callbacks receives the raw object, upcasted. + /// It is equivalent to doing: + /// ```dart + /// switch (sealedClass) { + /// case final Subclass value: + /// return ...; + /// case final Subclass2 value: + /// return ...; + /// } + /// ``` + + @optionalTypeArgs + TResult map( + TResult Function(_MealComment value) $default, + ) { + final _that = this; + switch (_that) { + case _MealComment(): + return $default(_that); + case _: + throw StateError('Unexpected subclass'); + } + } + + /// A variant of `map` that fallback to returning `null`. + /// + /// It is equivalent to doing: + /// ```dart + /// switch (sealedClass) { + /// case final Subclass value: + /// return ...; + /// case _: + /// return null; + /// } + /// ``` + + @optionalTypeArgs + TResult? mapOrNull( + TResult? Function(_MealComment value)? $default, + ) { + final _that = this; + switch (_that) { + case _MealComment() when $default != null: + return $default(_that); + case _: + return null; + } + } + + /// A variant of `when` that fallback to an `orElse` callback. + /// + /// It is equivalent to doing: + /// ```dart + /// switch (sealedClass) { + /// case Subclass(:final field): + /// return ...; + /// case _: + /// return orElse(); + /// } + /// ``` + + @optionalTypeArgs + TResult maybeWhen( + TResult Function(String comment, int typeNo)? $default, { + required TResult orElse(), + }) { + final _that = this; + switch (_that) { + case _MealComment() when $default != null: + return $default(_that.comment, _that.typeNo); + case _: + return orElse(); + } + } + + /// A `switch`-like method, using callbacks. + /// + /// As opposed to `map`, this offers destructuring. + /// It is equivalent to doing: + /// ```dart + /// switch (sealedClass) { + /// case Subclass(:final field): + /// return ...; + /// case Subclass2(:final field2): + /// return ...; + /// } + /// ``` + + @optionalTypeArgs + TResult when( + TResult Function(String comment, int typeNo) $default, + ) { + final _that = this; + switch (_that) { + case _MealComment(): + return $default(_that.comment, _that.typeNo); + case _: + throw StateError('Unexpected subclass'); + } + } + + /// A variant of `when` that fallback to returning `null` + /// + /// It is equivalent to doing: + /// ```dart + /// switch (sealedClass) { + /// case Subclass(:final field): + /// return ...; + /// case _: + /// return null; + /// } + /// ``` + + @optionalTypeArgs + TResult? whenOrNull( + TResult? Function(String comment, int typeNo)? $default, + ) { + final _that = this; + switch (_that) { + case _MealComment() when $default != null: + return $default(_that.comment, _that.typeNo); + case _: + return null; + } + } +} + /// @nodoc @JsonSerializable() class _MealComment implements MealComment { @@ -692,6 +1180,166 @@ class _$PortionCommentCopyWithImpl<$Res> } } +/// Adds pattern-matching-related methods to [PortionComment]. +extension PortionCommentPatterns on PortionComment { + /// A variant of `map` that fallback to returning `orElse`. + /// + /// It is equivalent to doing: + /// ```dart + /// switch (sealedClass) { + /// case final Subclass value: + /// return ...; + /// case _: + /// return orElse(); + /// } + /// ``` + + @optionalTypeArgs + TResult maybeMap( + TResult Function(_PortionComment value)? $default, { + required TResult orElse(), + }) { + final _that = this; + switch (_that) { + case _PortionComment() when $default != null: + return $default(_that); + case _: + return orElse(); + } + } + + /// A `switch`-like method, using callbacks. + /// + /// Callbacks receives the raw object, upcasted. + /// It is equivalent to doing: + /// ```dart + /// switch (sealedClass) { + /// case final Subclass value: + /// return ...; + /// case final Subclass2 value: + /// return ...; + /// } + /// ``` + + @optionalTypeArgs + TResult map( + TResult Function(_PortionComment value) $default, + ) { + final _that = this; + switch (_that) { + case _PortionComment(): + return $default(_that); + case _: + throw StateError('Unexpected subclass'); + } + } + + /// A variant of `map` that fallback to returning `null`. + /// + /// It is equivalent to doing: + /// ```dart + /// switch (sealedClass) { + /// case final Subclass value: + /// return ...; + /// case _: + /// return null; + /// } + /// ``` + + @optionalTypeArgs + TResult? mapOrNull( + TResult? Function(_PortionComment value)? $default, + ) { + final _that = this; + switch (_that) { + case _PortionComment() when $default != null: + return $default(_that); + case _: + return null; + } + } + + /// A variant of `when` that fallback to an `orElse` callback. + /// + /// It is equivalent to doing: + /// ```dart + /// switch (sealedClass) { + /// case Subclass(:final field): + /// return ...; + /// case _: + /// return orElse(); + /// } + /// ``` + + @optionalTypeArgs + TResult maybeWhen( + TResult Function(@JsonKey(name: 'id') int portionId, String comment)? + $default, { + required TResult orElse(), + }) { + final _that = this; + switch (_that) { + case _PortionComment() when $default != null: + return $default(_that.portionId, _that.comment); + case _: + return orElse(); + } + } + + /// A `switch`-like method, using callbacks. + /// + /// As opposed to `map`, this offers destructuring. + /// It is equivalent to doing: + /// ```dart + /// switch (sealedClass) { + /// case Subclass(:final field): + /// return ...; + /// case Subclass2(:final field2): + /// return ...; + /// } + /// ``` + + @optionalTypeArgs + TResult when( + TResult Function(@JsonKey(name: 'id') int portionId, String comment) + $default, + ) { + final _that = this; + switch (_that) { + case _PortionComment(): + return $default(_that.portionId, _that.comment); + case _: + throw StateError('Unexpected subclass'); + } + } + + /// A variant of `when` that fallback to returning `null` + /// + /// It is equivalent to doing: + /// ```dart + /// switch (sealedClass) { + /// case Subclass(:final field): + /// return ...; + /// case _: + /// return null; + /// } + /// ``` + + @optionalTypeArgs + TResult? whenOrNull( + TResult? Function(@JsonKey(name: 'id') int portionId, String comment)? + $default, + ) { + final _that = this; + switch (_that) { + case _PortionComment() when $default != null: + return $default(_that.portionId, _that.comment); + case _: + return null; + } + } +} + /// @nodoc @JsonSerializable() class _PortionComment implements PortionComment { diff --git a/lib/features/profile/models/profile_model.dart b/lib/features/profile/models/profile_model.dart index 8d13bc7e..72e91d0d 100644 --- a/lib/features/profile/models/profile_model.dart +++ b/lib/features/profile/models/profile_model.dart @@ -17,6 +17,7 @@ abstract class ProfileModel with _$ProfileModel { @JsonKey(name: "created_at") DateTime? createdAt, @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_entity_subscribed_by_me') required bool isEntitySubscribedByMe, @JsonKey(name: "slug") required String slug, @JsonKey(name: "profile_photo_url") required String profilePhotoUrl, diff --git a/test/features/profile/services/profile_service_test.dart b/test/features/profile/services/profile_service_test.dart index e735602c..b2c51b01 100644 --- a/test/features/profile/services/profile_service_test.dart +++ b/test/features/profile/services/profile_service_test.dart @@ -21,6 +21,7 @@ void main() { email: 'mso7nueh@mail.ru', isPublicProfile: true, isEntitySubscribedByMe: false, + isBlockedByMe: false, slug: 'slug', profilePhotoUrl: '', roles: ['user'], From 4426eef2dc6852f81ab135ae0efa747d306d4a92 Mon Sep 17 00:00:00 2001 From: Username1548 Date: Tue, 8 Jul 2025 21:19:33 +0700 Subject: [PATCH 008/134] fix(notifications): fixed wrong notifications rendring and changed requested pagination page --- .../notifications_cubit/notifications_cubit.dart | 2 +- .../presentation/widgets/notification_widget.dart | 8 ++++---- lib/l10n/intl_en.arb | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/features/notifications/presentation/notifications_cubit/notifications_cubit.dart b/lib/features/notifications/presentation/notifications_cubit/notifications_cubit.dart index b7822d46..16cf1f40 100644 --- a/lib/features/notifications/presentation/notifications_cubit/notifications_cubit.dart +++ b/lib/features/notifications/presentation/notifications_cubit/notifications_cubit.dart @@ -66,7 +66,7 @@ class NotificationsCubit extends Cubit { Future getNotifications() async { try { - final notifications = await notificationsRepository.getNotifications(page: 2); + final notifications = await notificationsRepository.getNotifications(page: 1); _notifications.clear(); _notifications.addAll(notifications ?? []); emit( diff --git a/lib/features/notifications/presentation/widgets/notification_widget.dart b/lib/features/notifications/presentation/widgets/notification_widget.dart index 4ff0faff..83157593 100644 --- a/lib/features/notifications/presentation/widgets/notification_widget.dart +++ b/lib/features/notifications/presentation/widgets/notification_widget.dart @@ -67,7 +67,7 @@ class _PlanSubscriptionStartedToAuthorWidget extends StatelessWidget { return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: s.fillYasterday, + notificationsText: s.userSubscribedToPlan(notification.customerName, notification.planName), ); } } @@ -81,7 +81,7 @@ class _PlanSubscriptionCanceledToAuthorWidget extends StatelessWidget { return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: s.fillYasterday, + notificationsText: s.userCanceldThePlanSubscription(notification.customerName, notification.planName), ); } } @@ -95,7 +95,7 @@ class _ChosenPlanStartedToClientWidget extends StatelessWidget { return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: s.fillYasterday, + notificationsText: s.planStarted(notification.planName), ); } } @@ -109,7 +109,7 @@ class _ChosenPlanTomorrowReminderToClientWidget extends StatelessWidget { return _BaseNotificationWidget( isRead: notification.isRead, - notificationsText: s.fillYasterday, + notificationsText: s.tomorrowYourPlanWillStart(notification.planName), ); } } diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 96ddc4be..ca980067 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -402,6 +402,6 @@ "allowAcessToGaleryDescription": "Предоставление доступа позволит вам добавлять фото к записям", "allowAccessToCamera": "Разрешите доступ к камере", "allowAccessToCameraDescription": "Предоставление доступа позволит вам делать фотографии и добавлять к записям", - "allowAccess": "Разрешить доступ" + "allowAccess": "Разрешить доступ" } From 6dc30f4c894343d44254d83966286b78c42d8e43 Mon Sep 17 00:00:00 2001 From: Username1548 Date: Wed, 9 Jul 2025 12:34:51 +0700 Subject: [PATCH 009/134] feat(comments): added comments deleting and updating --- .../comments_cubit/comments_cubit.dart | 10 ++ .../comments_cubit/comments_state.dart | 1 + .../comments/models/comment_update_model.dart | 12 ++ .../comments_actions_bottom_sheet.dart | 168 ++++++++++++------ .../repositories/comments_repository.dart | 3 +- .../comments/services/comments_service.dart | 7 +- lib/core/const.dart | 4 +- .../presentation/widgets/comments_widget.dart | 9 +- 8 files changed, 153 insertions(+), 61 deletions(-) create mode 100644 lib/core/comments/models/comment_update_model.dart diff --git a/lib/core/comments/comments_cubit/comments_cubit.dart b/lib/core/comments/comments_cubit/comments_cubit.dart index 2bc21d14..7e5df141 100644 --- a/lib/core/comments/comments_cubit/comments_cubit.dart +++ b/lib/core/comments/comments_cubit/comments_cubit.dart @@ -85,20 +85,30 @@ class CommentsCubit extends Cubit { } Future deleteComment({required int id}) async { + if (state is! CommentsStateLoaded) return false; + final commentModel = (state as CommentsStateLoaded).comment; + final updatedCommentModel = commentModel.copyWith(data: commentModel.data.where((comment) => comment.id != id).toList()); try { await commentsRepository.deleteComment(id: id); + emit(CommentsState.loaded(comment: updatedCommentModel)); return true; } catch (e, st) { + emit(CommentsState.loaded(comment: commentModel)); log(e.toString(), stackTrace: st); } return false; } Future updateComment({required int id, required String body}) async { + final commentModel = (state as CommentsStateLoaded).comment; + final updatedCommentModel = + commentModel.copyWith(data: commentModel.data.map((comment) => comment.id == id ? comment.copyWith(body: body) : comment).toList()); try { await commentsRepository.updateComment(id: id, body: body); + emit(CommentsState.loaded(comment: updatedCommentModel)); return true; } catch (e, st) { + emit(CommentsState.loaded(comment: commentModel)); log(e.toString(), stackTrace: st); } return false; diff --git a/lib/core/comments/comments_cubit/comments_state.dart b/lib/core/comments/comments_cubit/comments_state.dart index 0f9c49ea..79fed119 100644 --- a/lib/core/comments/comments_cubit/comments_state.dart +++ b/lib/core/comments/comments_cubit/comments_state.dart @@ -12,6 +12,7 @@ abstract class CommentsState with _$CommentsState { const factory CommentsState.loaded({ required CommentModel comment, int? replyId, + int? editedCommentId, }) = CommentsStateLoaded; const factory CommentsState.error({ diff --git a/lib/core/comments/models/comment_update_model.dart b/lib/core/comments/models/comment_update_model.dart new file mode 100644 index 00000000..dfe26dfe --- /dev/null +++ b/lib/core/comments/models/comment_update_model.dart @@ -0,0 +1,12 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'comment_update_model.freezed.dart'; +part 'comment_update_model.g.dart'; + +@freezed +abstract class CommentUpdateModel with _$CommentUpdateModel { + + factory CommentUpdateModel({required String body}) = _CommentUpdateModel; + + factory CommentUpdateModel.fromJson(Map json) => _$CommentUpdateModelFromJson(json); +} \ No newline at end of file diff --git a/lib/core/comments/presentation/comments_actions_bottom_sheet.dart b/lib/core/comments/presentation/comments_actions_bottom_sheet.dart index 2fc9498e..deeea5ff 100644 --- a/lib/core/comments/presentation/comments_actions_bottom_sheet.dart +++ b/lib/core/comments/presentation/comments_actions_bottom_sheet.dart @@ -2,17 +2,20 @@ import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/widgets/bottom_sheet_handler_bar.dart'; import 'package:eatvio_app/core/widgets/custom_divider.dart'; +import 'package:eatvio_app/core/widgets/custom_text_form_field.dart'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:go_router/go_router.dart'; void showCommentsActionsBottomSheet( BuildContext context, { required void Function()? reply, required void Function() copy, - required void Function() edit, + required void Function(String text) edit, required void Function() delete, required void Function() complain, + required String commentText, required bool isMyComment, }) { showModalBottomSheet( @@ -32,11 +35,12 @@ void showCommentsActionsBottomSheet( edit: edit, delete: delete, complain: complain, + commentText: commentText, ), )); } -class CommentsActionsBottomSheet extends StatelessWidget { +class CommentsActionsBottomSheet extends StatefulWidget { const CommentsActionsBottomSheet({ super.key, required this.isMyComment, @@ -45,82 +49,140 @@ class CommentsActionsBottomSheet extends StatelessWidget { required this.edit, required this.delete, required this.complain, + required this.commentText, }); final bool isMyComment; final void Function()? reply; final void Function() copy; - final void Function() edit; + final void Function(String text) edit; final void Function() delete; final void Function() complain; + final String commentText; + + @override + State createState() => _CommentsActionsBottomSheetState(); +} + +class _CommentsActionsBottomSheetState extends State { + bool isUserEditingComment = false; + final controller = TextEditingController(); + @override + void initState() { + super.initState(); + controller.text = widget.commentText; + } + @override Widget build(BuildContext context) { final s = S.of(context); return Padding( padding: const EdgeInsets.symmetric(horizontal: 16), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const SizedBox( - height: 7, - ), - const BottomSheetHandlerBar(), - if (isMyComment) - _ActionWidget( - text: s.edit, - iconPath: 'assets/icons/edit_paint.svg', - onTap: edit, - ) - else if (reply != null) - _ActionWidget( - text: s.reply, - iconPath: 'assets/icons/reply.svg', - onTap: reply!, - ), - if (reply != null) const CustomDivider(), - _ActionWidget( - text: s.copy, - iconPath: 'assets/icons/copy2.svg', - onTap: copy, - ), - if (isMyComment) ...[ - if (reply != null) ...[ - const CustomDivider(), - _ActionWidget( - text: s.reply, - iconPath: 'assets/icons/reply.svg', - onTap: reply!, + child: isUserEditingComment + ? Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + 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 CustomDivider(), - _ActionWidget( - text: s.deleteComment, - iconPath: 'assets/icons/red_trash.svg', - onTap: delete, + ) + : Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox( + height: 7, + ), + const BottomSheetHandlerBar(), + if (widget.isMyComment) + _ActionWidget( + text: s.edit, + iconPath: 'assets/icons/edit_paint.svg', + needToPop: false, + onTap: () => setState(() { + isUserEditingComment = true; + }), + ) + else if (widget.reply != null) + _ActionWidget( + text: s.reply, + iconPath: 'assets/icons/reply.svg', + onTap: widget.reply!, + ), + if (widget.reply != null) const CustomDivider(), + _ActionWidget( + text: s.copy, + iconPath: 'assets/icons/copy2.svg', + onTap: widget.copy, + ), + if (widget.isMyComment) ...[ + if (widget.reply != null) ...[ + const CustomDivider(), + _ActionWidget( + text: s.reply, + iconPath: 'assets/icons/reply.svg', + onTap: widget.reply!, + ), + ], + const CustomDivider(), + _ActionWidget( + text: s.deleteComment, + iconPath: 'assets/icons/red_trash.svg', + onTap: widget.delete, + ), + ] else ...[ + const CustomDivider(), + _ActionWidget( + text: s.complain, + iconPath: 'assets/icons/complain.svg', + onTap: widget.complain, + ), + ] + ], ), - ] else ...[ - const CustomDivider(), - _ActionWidget( - text: s.complain, - iconPath: 'assets/icons/complain.svg', - onTap: complain, - ), - ] - ], - ), ); } } class _ActionWidget extends StatelessWidget { - const _ActionWidget({required this.text, required this.iconPath, required this.onTap}); + const _ActionWidget({required this.text, required this.iconPath, required this.onTap, this.needToPop = true}); final String text; final String iconPath; final void Function() onTap; + final bool needToPop; @override Widget build(BuildContext context) { return GestureDetector( onTap: () { - Navigator.pop(context); + if (needToPop) Navigator.pop(context); onTap(); }, child: Padding( diff --git a/lib/core/comments/repositories/comments_repository.dart b/lib/core/comments/repositories/comments_repository.dart index bc6e654b..096c3690 100644 --- a/lib/core/comments/repositories/comments_repository.dart +++ b/lib/core/comments/repositories/comments_repository.dart @@ -1,3 +1,4 @@ +import 'package:eatvio_app/core/comments/models/comment_update_model.dart'; import 'package:eatvio_app/core/comments/services/comments_service.dart'; class CommentsRepository { @@ -15,7 +16,7 @@ class CommentsRepository { Future updateComment({required int id, required String body}) async { try { - return service.updateComment(id, body); + return service.updateComment(id, CommentUpdateModel(body: body)); } catch (e) { rethrow; } diff --git a/lib/core/comments/services/comments_service.dart b/lib/core/comments/services/comments_service.dart index d2db4218..606f61bd 100644 --- a/lib/core/comments/services/comments_service.dart +++ b/lib/core/comments/services/comments_service.dart @@ -1,4 +1,5 @@ import 'package:dio/dio.dart'; +import 'package:eatvio_app/core/comments/models/comment_update_model.dart'; import 'package:retrofit/retrofit.dart'; part 'comments_service.g.dart'; @@ -7,9 +8,9 @@ part 'comments_service.g.dart'; abstract class CommentsService { factory CommentsService(Dio dio, {String baseUrl}) = _CommentsService; - @DELETE('/api/comments/{comment}') + @DELETE('/api/comments/{id}') Future deleteComment(@Path('id') int id); - @PATCH('/api/comments/{comment}') - Future updateComment(@Path('id') int id, @Body() String body); + @PATCH('/api/comments/{id}') + Future updateComment(@Path('id') int id, @Body() CommentUpdateModel body); } diff --git a/lib/core/const.dart b/lib/core/const.dart index 4f38a54e..31c47e9c 100644 --- a/lib/core/const.dart +++ b/lib/core/const.dart @@ -1,5 +1,5 @@ -// String baseUrl = 'https://eatvio.ru'; -String baseUrl = 'https://dev2.eatvio.com'; +String baseUrl = 'https://eatvio.ru'; +// String baseUrl = 'https://dev2.eatvio.com'; const List durationValues = [7, 14, 21]; diff --git a/lib/features/feed/presentation/widgets/comments_widget.dart b/lib/features/feed/presentation/widgets/comments_widget.dart index d3365b15..e36bdf52 100644 --- a/lib/features/feed/presentation/widgets/comments_widget.dart +++ b/lib/features/feed/presentation/widgets/comments_widget.dart @@ -64,6 +64,7 @@ class _CommentsWidgetState extends State { onTap: () { showCommentsActionsBottomSheet( context, + commentText: comment.body, reply: !isChild ? () { context.read().setReplyId(comment.id); @@ -73,8 +74,12 @@ class _CommentsWidgetState extends State { Clipboard.setData(ClipboardData(text: comment.body)); showInfoToast(s.link_was_copied); }, - edit: () {}, - delete: () {}, + edit: (String text) { + context.read().updateComment(id: comment.id, body: text); + }, + delete: () { + context.read().deleteComment(id: comment.id); + }, complain: () { showModalBottomSheet( context: context, From 7b1574392b92cd89da70c6568c6c76367711f32f Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Wed, 9 Jul 2025 10:20:21 +0300 Subject: [PATCH 010/134] feat(block): Issue-523 --- .../bloc/profile_cubit/profile_cubit.dart | 2 ++ .../presentation/pages/profile_screen.dart | 27 ++++++++++++++++--- 2 files changed, 25 insertions(+), 4 deletions(-) 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 36a98658..d0d7a932 100644 --- a/lib/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart +++ b/lib/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart @@ -25,6 +25,7 @@ class ProfileCubit extends Cubit { }) : super(const ProfileState.loading()); Future getProfileBySlug(String slug) async { + emit(const ProfileState.loading()); try { final profile = await profileRepository.getProfileBySlug(slug); emit( @@ -44,6 +45,7 @@ class ProfileCubit extends Cubit { Future getMyProfile({ String period = '1', }) async { + emit(const ProfileState.loading()); _currentPeriod = period; try { final profile = await profileRepository.getMyProfile(); diff --git a/lib/features/profile/presentation/pages/profile_screen.dart b/lib/features/profile/presentation/pages/profile_screen.dart index 7c94e537..07ab1bd2 100644 --- a/lib/features/profile/presentation/pages/profile_screen.dart +++ b/lib/features/profile/presentation/pages/profile_screen.dart @@ -194,9 +194,9 @@ class _ProfileScreenState extends State { ], ), if (state.profileDetails.additionalInfo.isNotEmpty) _additionalInfo(state), - const SizedBox(height: 8), - _messageAndSubscribe(state), - const SizedBox(height: 16), + if (!state.profile.isBlockedByMe) const SizedBox(height: 8), + if (!state.profile.isBlockedByMe) _messageAndSubscribe(state), + if (!state.profile.isBlockedByMe) const SizedBox(height: 16), ], ), ), @@ -1177,7 +1177,21 @@ class _ProfileScreenState extends State { ), ); } else if (value == 'block') { + isFirstFetch = true; sl().blockUser(state.profile.slug); + widget.author == null || widget.author!.authorIsMe ? profileCubit.getMyProfile() : profileCubit.getProfileBySlug(widget.author!.authorSlug); + if (state.profile.roles.contains('consultant')) { + plansCubit.getPlans(state.profile.slug); + } + isFirstFetch = false; + } else if (value == 'unblock') { + isFirstFetch = true; + sl().unblockUser(state.profile.slug); + widget.author == null || widget.author!.authorIsMe ? profileCubit.getMyProfile() : profileCubit.getProfileBySlug(widget.author!.authorSlug); + if (state.profile.roles.contains('consultant')) { + plansCubit.getPlans(state.profile.slug); + } + isFirstFetch = false; } else { showModalBottomSheet( context: context, @@ -1199,11 +1213,16 @@ class _ProfileScreenState extends State { value: 'complain', child: Text('Пожаловаться'), ), - if (widget.author != null) + if (widget.author != null && !state.profile.isBlockedByMe) const PopupMenuItem( value: 'block', child: Text('Заблокировать'), ), + if (widget.author != null && state.profile.isBlockedByMe) + const PopupMenuItem( + value: 'unblock', + child: Text('Разблокировать'), + ), ], child: const Icon(Icons.more_horiz_rounded), ), From ea2ec05ed62696ad62353e54118f1b083df6cbca Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Wed, 9 Jul 2025 15:06:51 +0300 Subject: [PATCH 011/134] fix(chat): websockets --- lib/core/const.dart | 1 - lib/core/websockets/ws_manager.dart | 1 - .../bloc/chat_room_cubit/chat_room_cubit.dart | 24 ++++++++++----- .../registration_cubit.dart | 29 +++++++------------ .../repositories/registration_repository.dart | 10 ++++++- 5 files changed, 36 insertions(+), 29 deletions(-) diff --git a/lib/core/const.dart b/lib/core/const.dart index 5d1aded6..618a9866 100644 --- a/lib/core/const.dart +++ b/lib/core/const.dart @@ -1,4 +1,3 @@ -// String baseUrl = 'https://mobile-stage.eatvio.ru'; String baseUrl = 'https://eatvio.ru'; const List durationValues = [7, 14, 21]; diff --git a/lib/core/websockets/ws_manager.dart b/lib/core/websockets/ws_manager.dart index 0a5f7241..528f2ae9 100644 --- a/lib/core/websockets/ws_manager.dart +++ b/lib/core/websockets/ws_manager.dart @@ -98,7 +98,6 @@ class WsManager { const Duration(milliseconds: 300), () => sl().getChatRooms(), ); - log(event.rootObject.toString()); }); _subscribe(); } catch (e, st) { diff --git a/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart b/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart index 32244e78..f4c95d12 100644 --- a/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart +++ b/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart @@ -89,7 +89,10 @@ class ChatRoomCubit extends Cubit { } } - Future initChat(String uuid, {bool sendInitialMessage = false}) async { + Future initChat( + String uuid, { + bool sendInitialMessage = false, + }) async { try { final chatRoom = await chatRepository.getChatRoom(uuid); ChosenPlanModel? chosenPlan; @@ -98,8 +101,7 @@ class ChatRoomCubit extends Cubit { } final chatMessages = await chatRepository.getChatMessages(uuid, currentPage); lastPage = chatMessages.meta.lastPage; - - await _subscribeToChatChannel(uuid: uuid); + await _subscribeToChatChannel(uuid: uuid, chatId: chatRoom.id); emit( ChatRoomState.loaded( chatRoom: chatRoom.copyWith(chosenPlan: chosenPlan), @@ -158,14 +160,14 @@ class ChatRoomCubit extends Cubit { } } - Future _subscribeToChatChannel({required String uuid}) async { + Future _subscribeToChatChannel({required String uuid, required String chatId}) async { final client = wsManager.client; if (client == null) return; - await wsManager.initPrivateDirectChatChannel(uuid); + await wsManager.initPrivateDirectChatChannel(chatId); wsManager.subscribeDirectChatChannel(); messagesSubscription = wsManager.directChatStream?.listen((event) { AudioPlayer().play((AssetSource('audio/recive_message.wav'))); - final message = ChatMessageModel.fromJson(jsonDecode(event.data)['message']); + final message = ChatMessageModel.fromJson(jsonDecode(event.data)); List messagesList = [message, ...(state as ChatRoomStateLoaded).chatMessages]; chatRepository.emitNewMessageToChats(message); emit((state as ChatRoomStateLoaded).copyWith(chatMessages: messagesList, isLoading: isLoading)); @@ -179,7 +181,15 @@ class ChatRoomCubit extends Cubit { typingSubscription = wsManager.clientTypingStream?.listen((event) { emit((state as ChatRoomStateLoaded).copyWith(isTyping: true, isLoading: isLoading)); EasyDebounce.debounce( - 'client-typing', const Duration(seconds: 3), () => emit((state as ChatRoomStateLoaded).copyWith(isTyping: false, isLoading: isLoading))); + 'client-typing', + const Duration(seconds: 3), + () => emit( + (state as ChatRoomStateLoaded).copyWith( + isTyping: false, + isLoading: isLoading, + ), + ), + ); }); } diff --git a/lib/features/registration/presentation/bloc/registration_cubit/registration_cubit.dart b/lib/features/registration/presentation/bloc/registration_cubit/registration_cubit.dart index 9581779d..83592c3b 100644 --- a/lib/features/registration/presentation/bloc/registration_cubit/registration_cubit.dart +++ b/lib/features/registration/presentation/bloc/registration_cubit/registration_cubit.dart @@ -3,7 +3,6 @@ import 'dart:developer'; import 'package:eatvio_app/core/app_router.dart'; import 'package:eatvio_app/core/token_storage.dart'; -import 'package:eatvio_app/core/widgets/show_error_dialog.dart'; import 'package:eatvio_app/features/registration/models/user_registration_model.dart'; import 'package:eatvio_app/features/registration/presentation/bloc/registration_cubit/registration_state.dart'; import 'package:eatvio_app/features/registration/repositories/registration_repository.dart'; @@ -51,25 +50,17 @@ class RegistrationCubit extends Cubit { void updateSheet(UserRegistrationSheet sheet) => emit(RegistrationState(sheet: sheet)); Future tryRegistration() async { - if (await registrationRepository.emailIsReachable(state.sheet.email)) { - try { - final token = await registrationRepository.tryRegister( - state.sheet.copyWith( - phone: state.sheet.phone.replaceAll(' ', '').replaceAll('(', '').replaceAll(')', ''), - responseBody: '{}', - ), - ); - await TokenStorage.saveTokenInStorage(token); - AppRouter.navigatorKey.currentContext?.pushNamed('verifyEmailScreen'); - } catch (e) { - log(e.toString()); - } - } else { - showErrorDialog( - AppRouter.navigatorKey.currentContext!, - title: 'Ошибка регистрации', - description: 'Несуществующая почта.', + try { + final token = await registrationRepository.tryRegister( + state.sheet.copyWith( + phone: state.sheet.phone.replaceAll(' ', '').replaceAll('(', '').replaceAll(')', ''), + responseBody: '{}', + ), ); + await TokenStorage.saveTokenInStorage(token); + AppRouter.navigatorKey.currentContext?.pushNamed('verifyEmailScreen'); + } catch (e, st) { + log(e.toString(), stackTrace: st); } } diff --git a/lib/features/registration/repositories/registration_repository.dart b/lib/features/registration/repositories/registration_repository.dart index 7a4a14ae..0c3008e0 100644 --- a/lib/features/registration/repositories/registration_repository.dart +++ b/lib/features/registration/repositories/registration_repository.dart @@ -1,5 +1,7 @@ + import 'package:dio/dio.dart'; import 'package:eatvio_app/core/locator_service.dart'; +import 'package:eatvio_app/core/utils/toasts.dart'; import 'package:eatvio_app/features/registration/models/user_registration_model.dart'; import 'package:eatvio_app/features/registration/models/location_model.dart'; import 'package:eatvio_app/features/registration/services/registration_service.dart'; @@ -11,7 +13,13 @@ class RegistrationRepository { Future tryRegister(UserRegistrationSheet sheet) async { try { - return (await registrationService.register(sheet: sheet.toJson()))['token']!; + final response = await registrationService.register(sheet: sheet.toJson()); + try { + return (response['token']!); + } catch (e) { + showErrorToast(response.toString()); + rethrow; + } } catch (e) { rethrow; } From 27f97d0e114c244d7b7e975e288d21c03a584f27 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Wed, 9 Jul 2025 18:49:48 +0300 Subject: [PATCH 012/134] fix: permission --- lib/core/permission_handler/permission_handler.dart | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/core/permission_handler/permission_handler.dart b/lib/core/permission_handler/permission_handler.dart index a311e84d..f107c2c0 100644 --- a/lib/core/permission_handler/permission_handler.dart +++ b/lib/core/permission_handler/permission_handler.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:eatvio_app/core/widgets/show_error_dialog.dart'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/cupertino.dart'; @@ -11,6 +13,10 @@ class PermissionHandler { required Future Function() onGranted, required BuildContext context, }) async { + if (Platform.isAndroid) { + await onGranted(); + return; + } final permissionState = await permission.request(); if (permissionState.isGranted) { await onGranted(); From 9bbd149d030d5ddbdfaa8f3b8389e5d38073f3eb Mon Sep 17 00:00:00 2001 From: Username1548 Date: Thu, 10 Jul 2025 08:58:05 +0700 Subject: [PATCH 013/134] fix(permissions): added galery permissions for android --- android/app/src/main/AndroidManifest.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 4945e428..3cc6894e 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -2,6 +2,9 @@ + + + Date: Thu, 10 Jul 2025 22:03:38 +0700 Subject: [PATCH 014/134] app version update --- ios/Runner.xcodeproj/project.pbxproj | 12 ++++++------ pubspec.yaml | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 046c0614..956db669 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 = 39; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -406,7 +406,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - MARKETING_VERSION = 1.0.50; + MARKETING_VERSION = 1.2.1; PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044"; @@ -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 = 39; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -556,7 +556,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - MARKETING_VERSION = 1.0.50; + MARKETING_VERSION = 1.2.1; PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044"; @@ -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 = 39; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -594,7 +594,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - MARKETING_VERSION = 1.0.50; + MARKETING_VERSION = 1.2.1; PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044"; diff --git a/pubspec.yaml b/pubspec.yaml index d5c928e7..bd577875 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: eatvio_app description: A new Flutter project. -version: 1.1.16+46 +version: 1.2.1+1 publish_to: 'none' environment: From 0749a53e248e4993c3cd1dacca0ee70aad02746b Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Fri, 11 Jul 2025 14:05:14 +0300 Subject: [PATCH 015/134] fix: horizontal layout --- android/build/reports/problems/problems-report.html | 4 ++-- lib/core/const.dart | 3 ++- lib/core/locator_service.dart | 1 - lib/features/auth/presentation/pages/auth_screen.dart | 3 +++ .../chat/presentation/widgets/chat_bubble_widget.dart | 6 +++--- 5 files changed, 10 insertions(+), 7 deletions(-) diff --git a/android/build/reports/problems/problems-report.html b/android/build/reports/problems/problems-report.html index f21ab9cc..4921e6e4 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/lib/core/const.dart b/lib/core/const.dart index 618a9866..401c7365 100644 --- a/lib/core/const.dart +++ b/lib/core/const.dart @@ -1,4 +1,5 @@ -String baseUrl = 'https://eatvio.ru'; +// String baseUrl = 'https://eatvio.ru'; +String baseUrl = 'https://mobile-stage.eatvio.ru'; const List durationValues = [7, 14, 21]; diff --git a/lib/core/locator_service.dart b/lib/core/locator_service.dart index 9036809a..145a0fce 100644 --- a/lib/core/locator_service.dart +++ b/lib/core/locator_service.dart @@ -240,7 +240,6 @@ Future init() async { 'accept': 'application/json', 'content-type': 'application/json', }, - validateStatus: (status) => status != null && status < 500, ), )..interceptors.addAll( [ diff --git a/lib/features/auth/presentation/pages/auth_screen.dart b/lib/features/auth/presentation/pages/auth_screen.dart index 6b4ec260..10fb4172 100644 --- a/lib/features/auth/presentation/pages/auth_screen.dart +++ b/lib/features/auth/presentation/pages/auth_screen.dart @@ -79,6 +79,9 @@ class _AuthScreenState extends State { @override Widget build(BuildContext context) { + var shortestSide = MediaQuery.sizeOf(context).shortestSide; + final bool useMobileLayout = shortestSide < 600; + if (!useMobileLayout) isFocused = true; final s = S.of(context); FlutterNativeSplash.remove(); return BlocProvider( diff --git a/lib/features/chat/presentation/widgets/chat_bubble_widget.dart b/lib/features/chat/presentation/widgets/chat_bubble_widget.dart index dd6c7b4c..d2f5f1bd 100644 --- a/lib/features/chat/presentation/widgets/chat_bubble_widget.dart +++ b/lib/features/chat/presentation/widgets/chat_bubble_widget.dart @@ -89,17 +89,17 @@ class ChatBubbleWidget extends StatelessWidget { }, leading: const Icon( Icons.file_copy_outlined, - color: Colors.white, + color: Colors.black, ), trailing: const Icon( Icons.download_outlined, - color: Colors.white, + color: Colors.black, ), title: Text( mediaData[index].toString().replaceAll('chat_files/', ''), maxLines: 2, style: const TextStyle( - color: Colors.white, + color: Colors.black, fontSize: 12, overflow: TextOverflow.ellipsis, ), From 631efe6f9ebf12e6adcdc940e9bb4c7c153a5b5e Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Fri, 11 Jul 2025 14:22:11 +0300 Subject: [PATCH 016/134] fix: chats --- lib/features/chat/models/chat_room_pagination_model.dart | 2 ++ .../chat/presentation/bloc/chats_cubit/chats_cubit.dart | 7 ++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/features/chat/models/chat_room_pagination_model.dart b/lib/features/chat/models/chat_room_pagination_model.dart index 699df5e8..19de28c3 100644 --- a/lib/features/chat/models/chat_room_pagination_model.dart +++ b/lib/features/chat/models/chat_room_pagination_model.dart @@ -1,3 +1,4 @@ +import 'package:eatvio_app/core/models/pagination_model.dart'; import 'package:eatvio_app/features/chat/models/chat_message_model.dart'; import 'package:eatvio_app/features/plans/models/chosen_plan_model.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -10,6 +11,7 @@ abstract class ChatRoomPaginationModel with _$ChatRoomPaginationModel { const factory ChatRoomPaginationModel({ @JsonKey(name: "data") required List data, @JsonKey(name: "number_of_unread_chat_rooms") required int unreadChatsCount, + required Meta meta, }) = _ChatRoomPreviewModel; factory ChatRoomPaginationModel.fromJson(Map json) => _$ChatRoomPaginationModelFromJson(json); diff --git a/lib/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart b/lib/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart index 24028797..174108b3 100644 --- a/lib/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart +++ b/lib/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart @@ -21,7 +21,7 @@ class ChatsCubit extends Cubit { required this.chatRepository, required this.wsManager, required this.profileRepository, - }) : super(ChatsState()) { + }) : super(ChatsState()) { newLastMessageSubscription = chatRepository.newUserMessagesStream.listen((message) { _setNewLastMessage(message); }); @@ -29,6 +29,7 @@ class ChatsCubit extends Cubit { String? _query; int page = 1; late int lastPage; + Future connectToWebSocket() async { await newIncomingMessagesSubscription?.cancel(); newIncomingMessagesSubscription = wsManager.newMessagesStream?.listen((event) { @@ -43,7 +44,7 @@ class ChatsCubit extends Cubit { try { final chatRoomsPaginationModel = await chatRepository.getChatRooms(query: query); final unreadChats = chatRoomsPaginationModel.unreadChatsCount; - // lastPage = chatRoomsPaginationModel.chatRooms.lastPage + 1; + lastPage = chatRoomsPaginationModel.meta.lastPage + 1; emit( state.copyWith( chatRooms: chatRoomsPaginationModel.data, @@ -65,7 +66,7 @@ class ChatsCubit extends Cubit { try { final chatRoomsPaginationModel = await chatRepository.getChatRooms(query: query); final unreadChats = chatRoomsPaginationModel.unreadChatsCount; - // lastPage = chatRoomsPaginationModel.chatRooms.lastPage + 1; + lastPage = chatRoomsPaginationModel.meta.lastPage + 1; emit( state.copyWith( chatRooms: chatRoomsPaginationModel.data, From 958d09906b85a033709ded82a48afeb2415c2665 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Fri, 11 Jul 2025 14:34:48 +0300 Subject: [PATCH 017/134] baseurl --- ios/Runner.xcodeproj/project.pbxproj | 6 +++--- ios/Runner/Info.plist | 2 +- lib/core/const.dart | 3 +-- pubspec.yaml | 2 +- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 046c0614..d2269b71 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 = 39; + CURRENT_PROJECT_VERSION = 52; 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 = 39; + CURRENT_PROJECT_VERSION = 52; 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 = 39; + CURRENT_PROJECT_VERSION = 52; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index d7a769f2..6de3b03b 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -21,7 +21,7 @@ CFBundleSignature ???? CFBundleVersion - 39 + 52 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/lib/core/const.dart b/lib/core/const.dart index 401c7365..618a9866 100644 --- a/lib/core/const.dart +++ b/lib/core/const.dart @@ -1,5 +1,4 @@ -// String baseUrl = 'https://eatvio.ru'; -String baseUrl = 'https://mobile-stage.eatvio.ru'; +String baseUrl = 'https://eatvio.ru'; const List durationValues = [7, 14, 21]; diff --git a/pubspec.yaml b/pubspec.yaml index d5c928e7..74b00964 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: eatvio_app description: A new Flutter project. -version: 1.1.16+46 +version: 1.2.2+52 publish_to: 'none' environment: From 2c59fd09232671ae4995458b54f618f3969b9024 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Fri, 11 Jul 2025 14:43:56 +0300 Subject: [PATCH 018/134] =?UTF-8?q?=D0=A3=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD?= =?UTF-8?q?=D1=8B=20=D1=81=D0=B3=D0=B5=D0=BD=D0=B5=D1=80=D0=B8=D1=80=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D0=BD=D0=BD=D1=8B=D0=B5=20=D1=84=D0=B0=D0=B9=D0=BB?= =?UTF-8?q?=D1=8B=20(freezed,=20g.dart,=20build)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../consultant_review_model.freezed.dart | 784 ------------------ .../models/consultant_review_model.g.dart | 67 -- .../services/profile_service_test.mocks.dart | 526 ------------ 3 files changed, 1377 deletions(-) delete mode 100644 lib/features/diary/models/consultant_review_model.freezed.dart delete mode 100644 lib/features/diary/models/consultant_review_model.g.dart delete mode 100644 test/features/profile/services/profile_service_test.mocks.dart diff --git a/lib/features/diary/models/consultant_review_model.freezed.dart b/lib/features/diary/models/consultant_review_model.freezed.dart deleted file mode 100644 index a6f79469..00000000 --- a/lib/features/diary/models/consultant_review_model.freezed.dart +++ /dev/null @@ -1,784 +0,0 @@ -// dart format width=80 -// coverage:ignore-file -// GENERATED CODE - DO NOT MODIFY BY HAND -// ignore_for_file: type=lint -// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark - -part of 'consultant_review_model.dart'; - -// ************************************************************************** -// FreezedGenerator -// ************************************************************************** - -// dart format off -T _$identity(T value) => value; - -/// @nodoc -mixin _$ConsultantReviewModel { - String get planName; - int get planDuration; - int get currentPlanDay; - ConsultantComments get consultantComments; - - /// Create a copy of ConsultantReviewModel - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - @pragma('vm:prefer-inline') - $ConsultantReviewModelCopyWith get copyWith => - _$ConsultantReviewModelCopyWithImpl( - this as ConsultantReviewModel, _$identity); - - /// Serializes this ConsultantReviewModel to a JSON map. - Map toJson(); - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is ConsultantReviewModel && - (identical(other.planName, planName) || - other.planName == planName) && - (identical(other.planDuration, planDuration) || - other.planDuration == planDuration) && - (identical(other.currentPlanDay, currentPlanDay) || - other.currentPlanDay == currentPlanDay) && - (identical(other.consultantComments, consultantComments) || - other.consultantComments == consultantComments)); - } - - @JsonKey(includeFromJson: false, includeToJson: false) - @override - int get hashCode => Object.hash( - runtimeType, planName, planDuration, currentPlanDay, consultantComments); - - @override - String toString() { - return 'ConsultantReviewModel(planName: $planName, planDuration: $planDuration, currentPlanDay: $currentPlanDay, consultantComments: $consultantComments)'; - } -} - -/// @nodoc -abstract mixin class $ConsultantReviewModelCopyWith<$Res> { - factory $ConsultantReviewModelCopyWith(ConsultantReviewModel value, - $Res Function(ConsultantReviewModel) _then) = - _$ConsultantReviewModelCopyWithImpl; - @useResult - $Res call( - {String planName, - int planDuration, - int currentPlanDay, - ConsultantComments consultantComments}); - - $ConsultantCommentsCopyWith<$Res> get consultantComments; -} - -/// @nodoc -class _$ConsultantReviewModelCopyWithImpl<$Res> - implements $ConsultantReviewModelCopyWith<$Res> { - _$ConsultantReviewModelCopyWithImpl(this._self, this._then); - - final ConsultantReviewModel _self; - final $Res Function(ConsultantReviewModel) _then; - - /// Create a copy of ConsultantReviewModel - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? planName = null, - Object? planDuration = null, - Object? currentPlanDay = null, - Object? consultantComments = null, - }) { - return _then(_self.copyWith( - planName: null == planName - ? _self.planName - : planName // ignore: cast_nullable_to_non_nullable - as String, - planDuration: null == planDuration - ? _self.planDuration - : planDuration // ignore: cast_nullable_to_non_nullable - as int, - currentPlanDay: null == currentPlanDay - ? _self.currentPlanDay - : currentPlanDay // ignore: cast_nullable_to_non_nullable - as int, - consultantComments: null == consultantComments - ? _self.consultantComments - : consultantComments // ignore: cast_nullable_to_non_nullable - as ConsultantComments, - )); - } - - /// Create a copy of ConsultantReviewModel - /// with the given fields replaced by the non-null parameter values. - @override - @pragma('vm:prefer-inline') - $ConsultantCommentsCopyWith<$Res> get consultantComments { - return $ConsultantCommentsCopyWith<$Res>(_self.consultantComments, (value) { - return _then(_self.copyWith(consultantComments: value)); - }); - } -} - -/// @nodoc -@JsonSerializable() -class _ConsultantReviewModel implements ConsultantReviewModel { - _ConsultantReviewModel( - {required this.planName, - required this.planDuration, - required this.currentPlanDay, - required this.consultantComments}); - factory _ConsultantReviewModel.fromJson(Map json) => - _$ConsultantReviewModelFromJson(json); - - @override - final String planName; - @override - final int planDuration; - @override - final int currentPlanDay; - @override - final ConsultantComments consultantComments; - - /// Create a copy of ConsultantReviewModel - /// with the given fields replaced by the non-null parameter values. - @override - @JsonKey(includeFromJson: false, includeToJson: false) - @pragma('vm:prefer-inline') - _$ConsultantReviewModelCopyWith<_ConsultantReviewModel> get copyWith => - __$ConsultantReviewModelCopyWithImpl<_ConsultantReviewModel>( - this, _$identity); - - @override - Map toJson() { - return _$ConsultantReviewModelToJson( - this, - ); - } - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _ConsultantReviewModel && - (identical(other.planName, planName) || - other.planName == planName) && - (identical(other.planDuration, planDuration) || - other.planDuration == planDuration) && - (identical(other.currentPlanDay, currentPlanDay) || - other.currentPlanDay == currentPlanDay) && - (identical(other.consultantComments, consultantComments) || - other.consultantComments == consultantComments)); - } - - @JsonKey(includeFromJson: false, includeToJson: false) - @override - int get hashCode => Object.hash( - runtimeType, planName, planDuration, currentPlanDay, consultantComments); - - @override - String toString() { - return 'ConsultantReviewModel(planName: $planName, planDuration: $planDuration, currentPlanDay: $currentPlanDay, consultantComments: $consultantComments)'; - } -} - -/// @nodoc -abstract mixin class _$ConsultantReviewModelCopyWith<$Res> - implements $ConsultantReviewModelCopyWith<$Res> { - factory _$ConsultantReviewModelCopyWith(_ConsultantReviewModel value, - $Res Function(_ConsultantReviewModel) _then) = - __$ConsultantReviewModelCopyWithImpl; - @override - @useResult - $Res call( - {String planName, - int planDuration, - int currentPlanDay, - ConsultantComments consultantComments}); - - @override - $ConsultantCommentsCopyWith<$Res> get consultantComments; -} - -/// @nodoc -class __$ConsultantReviewModelCopyWithImpl<$Res> - implements _$ConsultantReviewModelCopyWith<$Res> { - __$ConsultantReviewModelCopyWithImpl(this._self, this._then); - - final _ConsultantReviewModel _self; - final $Res Function(_ConsultantReviewModel) _then; - - /// Create a copy of ConsultantReviewModel - /// with the given fields replaced by the non-null parameter values. - @override - @pragma('vm:prefer-inline') - $Res call({ - Object? planName = null, - Object? planDuration = null, - Object? currentPlanDay = null, - Object? consultantComments = null, - }) { - return _then(_ConsultantReviewModel( - planName: null == planName - ? _self.planName - : planName // ignore: cast_nullable_to_non_nullable - as String, - planDuration: null == planDuration - ? _self.planDuration - : planDuration // ignore: cast_nullable_to_non_nullable - as int, - currentPlanDay: null == currentPlanDay - ? _self.currentPlanDay - : currentPlanDay // ignore: cast_nullable_to_non_nullable - as int, - consultantComments: null == consultantComments - ? _self.consultantComments - : consultantComments // ignore: cast_nullable_to_non_nullable - as ConsultantComments, - )); - } - - /// Create a copy of ConsultantReviewModel - /// with the given fields replaced by the non-null parameter values. - @override - @pragma('vm:prefer-inline') - $ConsultantCommentsCopyWith<$Res> get consultantComments { - return $ConsultantCommentsCopyWith<$Res>(_self.consultantComments, (value) { - return _then(_self.copyWith(consultantComments: value)); - }); - } -} - -/// @nodoc -mixin _$ConsultantComments { - String? get dayComment; - List get mealsComments; - List get portionsComments; - - /// Create a copy of ConsultantComments - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - @pragma('vm:prefer-inline') - $ConsultantCommentsCopyWith get copyWith => - _$ConsultantCommentsCopyWithImpl( - this as ConsultantComments, _$identity); - - /// Serializes this ConsultantComments to a JSON map. - Map toJson(); - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is ConsultantComments && - (identical(other.dayComment, dayComment) || - other.dayComment == dayComment) && - const DeepCollectionEquality() - .equals(other.mealsComments, mealsComments) && - const DeepCollectionEquality() - .equals(other.portionsComments, portionsComments)); - } - - @JsonKey(includeFromJson: false, includeToJson: false) - @override - int get hashCode => Object.hash( - runtimeType, - dayComment, - const DeepCollectionEquality().hash(mealsComments), - const DeepCollectionEquality().hash(portionsComments)); - - @override - String toString() { - return 'ConsultantComments(dayComment: $dayComment, mealsComments: $mealsComments, portionsComments: $portionsComments)'; - } -} - -/// @nodoc -abstract mixin class $ConsultantCommentsCopyWith<$Res> { - factory $ConsultantCommentsCopyWith( - ConsultantComments value, $Res Function(ConsultantComments) _then) = - _$ConsultantCommentsCopyWithImpl; - @useResult - $Res call( - {String? dayComment, - List mealsComments, - List portionsComments}); -} - -/// @nodoc -class _$ConsultantCommentsCopyWithImpl<$Res> - implements $ConsultantCommentsCopyWith<$Res> { - _$ConsultantCommentsCopyWithImpl(this._self, this._then); - - final ConsultantComments _self; - final $Res Function(ConsultantComments) _then; - - /// Create a copy of ConsultantComments - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? dayComment = freezed, - Object? mealsComments = null, - Object? portionsComments = null, - }) { - return _then(_self.copyWith( - dayComment: freezed == dayComment - ? _self.dayComment - : dayComment // ignore: cast_nullable_to_non_nullable - as String?, - mealsComments: null == mealsComments - ? _self.mealsComments - : mealsComments // ignore: cast_nullable_to_non_nullable - as List, - portionsComments: null == portionsComments - ? _self.portionsComments - : portionsComments // ignore: cast_nullable_to_non_nullable - as List, - )); - } -} - -/// @nodoc -@JsonSerializable() -class _ConsultantComments implements ConsultantComments { - _ConsultantComments( - {required this.dayComment, - required final List mealsComments, - required final List portionsComments}) - : _mealsComments = mealsComments, - _portionsComments = portionsComments; - factory _ConsultantComments.fromJson(Map json) => - _$ConsultantCommentsFromJson(json); - - @override - final String? dayComment; - final List _mealsComments; - @override - List get mealsComments { - if (_mealsComments is EqualUnmodifiableListView) return _mealsComments; - // ignore: implicit_dynamic_type - return EqualUnmodifiableListView(_mealsComments); - } - - final List _portionsComments; - @override - List get portionsComments { - if (_portionsComments is EqualUnmodifiableListView) - return _portionsComments; - // ignore: implicit_dynamic_type - return EqualUnmodifiableListView(_portionsComments); - } - - /// Create a copy of ConsultantComments - /// with the given fields replaced by the non-null parameter values. - @override - @JsonKey(includeFromJson: false, includeToJson: false) - @pragma('vm:prefer-inline') - _$ConsultantCommentsCopyWith<_ConsultantComments> get copyWith => - __$ConsultantCommentsCopyWithImpl<_ConsultantComments>(this, _$identity); - - @override - Map toJson() { - return _$ConsultantCommentsToJson( - this, - ); - } - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _ConsultantComments && - (identical(other.dayComment, dayComment) || - other.dayComment == dayComment) && - const DeepCollectionEquality() - .equals(other._mealsComments, _mealsComments) && - const DeepCollectionEquality() - .equals(other._portionsComments, _portionsComments)); - } - - @JsonKey(includeFromJson: false, includeToJson: false) - @override - int get hashCode => Object.hash( - runtimeType, - dayComment, - const DeepCollectionEquality().hash(_mealsComments), - const DeepCollectionEquality().hash(_portionsComments)); - - @override - String toString() { - return 'ConsultantComments(dayComment: $dayComment, mealsComments: $mealsComments, portionsComments: $portionsComments)'; - } -} - -/// @nodoc -abstract mixin class _$ConsultantCommentsCopyWith<$Res> - implements $ConsultantCommentsCopyWith<$Res> { - factory _$ConsultantCommentsCopyWith( - _ConsultantComments value, $Res Function(_ConsultantComments) _then) = - __$ConsultantCommentsCopyWithImpl; - @override - @useResult - $Res call( - {String? dayComment, - List mealsComments, - List portionsComments}); -} - -/// @nodoc -class __$ConsultantCommentsCopyWithImpl<$Res> - implements _$ConsultantCommentsCopyWith<$Res> { - __$ConsultantCommentsCopyWithImpl(this._self, this._then); - - final _ConsultantComments _self; - final $Res Function(_ConsultantComments) _then; - - /// Create a copy of ConsultantComments - /// with the given fields replaced by the non-null parameter values. - @override - @pragma('vm:prefer-inline') - $Res call({ - Object? dayComment = freezed, - Object? mealsComments = null, - Object? portionsComments = null, - }) { - return _then(_ConsultantComments( - dayComment: freezed == dayComment - ? _self.dayComment - : dayComment // ignore: cast_nullable_to_non_nullable - as String?, - mealsComments: null == mealsComments - ? _self._mealsComments - : mealsComments // ignore: cast_nullable_to_non_nullable - as List, - portionsComments: null == portionsComments - ? _self._portionsComments - : portionsComments // ignore: cast_nullable_to_non_nullable - as List, - )); - } -} - -/// @nodoc -mixin _$MealComment { - String get comment; - int get typeNo; - - /// Create a copy of MealComment - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - @pragma('vm:prefer-inline') - $MealCommentCopyWith get copyWith => - _$MealCommentCopyWithImpl(this as MealComment, _$identity); - - /// Serializes this MealComment to a JSON map. - Map toJson(); - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is MealComment && - (identical(other.comment, comment) || other.comment == comment) && - (identical(other.typeNo, typeNo) || other.typeNo == typeNo)); - } - - @JsonKey(includeFromJson: false, includeToJson: false) - @override - int get hashCode => Object.hash(runtimeType, comment, typeNo); - - @override - String toString() { - return 'MealComment(comment: $comment, typeNo: $typeNo)'; - } -} - -/// @nodoc -abstract mixin class $MealCommentCopyWith<$Res> { - factory $MealCommentCopyWith( - MealComment value, $Res Function(MealComment) _then) = - _$MealCommentCopyWithImpl; - @useResult - $Res call({String comment, int typeNo}); -} - -/// @nodoc -class _$MealCommentCopyWithImpl<$Res> implements $MealCommentCopyWith<$Res> { - _$MealCommentCopyWithImpl(this._self, this._then); - - final MealComment _self; - final $Res Function(MealComment) _then; - - /// Create a copy of MealComment - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? comment = null, - Object? typeNo = null, - }) { - return _then(_self.copyWith( - comment: null == comment - ? _self.comment - : comment // ignore: cast_nullable_to_non_nullable - as String, - typeNo: null == typeNo - ? _self.typeNo - : typeNo // ignore: cast_nullable_to_non_nullable - as int, - )); - } -} - -/// @nodoc -@JsonSerializable() -class _MealComment implements MealComment { - _MealComment({required this.comment, required this.typeNo}); - factory _MealComment.fromJson(Map json) => - _$MealCommentFromJson(json); - - @override - final String comment; - @override - final int typeNo; - - /// Create a copy of MealComment - /// with the given fields replaced by the non-null parameter values. - @override - @JsonKey(includeFromJson: false, includeToJson: false) - @pragma('vm:prefer-inline') - _$MealCommentCopyWith<_MealComment> get copyWith => - __$MealCommentCopyWithImpl<_MealComment>(this, _$identity); - - @override - Map toJson() { - return _$MealCommentToJson( - this, - ); - } - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _MealComment && - (identical(other.comment, comment) || other.comment == comment) && - (identical(other.typeNo, typeNo) || other.typeNo == typeNo)); - } - - @JsonKey(includeFromJson: false, includeToJson: false) - @override - int get hashCode => Object.hash(runtimeType, comment, typeNo); - - @override - String toString() { - return 'MealComment(comment: $comment, typeNo: $typeNo)'; - } -} - -/// @nodoc -abstract mixin class _$MealCommentCopyWith<$Res> - implements $MealCommentCopyWith<$Res> { - factory _$MealCommentCopyWith( - _MealComment value, $Res Function(_MealComment) _then) = - __$MealCommentCopyWithImpl; - @override - @useResult - $Res call({String comment, int typeNo}); -} - -/// @nodoc -class __$MealCommentCopyWithImpl<$Res> implements _$MealCommentCopyWith<$Res> { - __$MealCommentCopyWithImpl(this._self, this._then); - - final _MealComment _self; - final $Res Function(_MealComment) _then; - - /// Create a copy of MealComment - /// with the given fields replaced by the non-null parameter values. - @override - @pragma('vm:prefer-inline') - $Res call({ - Object? comment = null, - Object? typeNo = null, - }) { - return _then(_MealComment( - comment: null == comment - ? _self.comment - : comment // ignore: cast_nullable_to_non_nullable - as String, - typeNo: null == typeNo - ? _self.typeNo - : typeNo // ignore: cast_nullable_to_non_nullable - as int, - )); - } -} - -/// @nodoc -mixin _$PortionComment { - @JsonKey(name: 'id') - int get portionId; - String get comment; - - /// Create a copy of PortionComment - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - @pragma('vm:prefer-inline') - $PortionCommentCopyWith get copyWith => - _$PortionCommentCopyWithImpl( - this as PortionComment, _$identity); - - /// Serializes this PortionComment to a JSON map. - Map toJson(); - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is PortionComment && - (identical(other.portionId, portionId) || - other.portionId == portionId) && - (identical(other.comment, comment) || other.comment == comment)); - } - - @JsonKey(includeFromJson: false, includeToJson: false) - @override - int get hashCode => Object.hash(runtimeType, portionId, comment); - - @override - String toString() { - return 'PortionComment(portionId: $portionId, comment: $comment)'; - } -} - -/// @nodoc -abstract mixin class $PortionCommentCopyWith<$Res> { - factory $PortionCommentCopyWith( - PortionComment value, $Res Function(PortionComment) _then) = - _$PortionCommentCopyWithImpl; - @useResult - $Res call({@JsonKey(name: 'id') int portionId, String comment}); -} - -/// @nodoc -class _$PortionCommentCopyWithImpl<$Res> - implements $PortionCommentCopyWith<$Res> { - _$PortionCommentCopyWithImpl(this._self, this._then); - - final PortionComment _self; - final $Res Function(PortionComment) _then; - - /// Create a copy of PortionComment - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? portionId = null, - Object? comment = null, - }) { - return _then(_self.copyWith( - portionId: null == portionId - ? _self.portionId - : portionId // ignore: cast_nullable_to_non_nullable - as int, - comment: null == comment - ? _self.comment - : comment // ignore: cast_nullable_to_non_nullable - as String, - )); - } -} - -/// @nodoc -@JsonSerializable() -class _PortionComment implements PortionComment { - _PortionComment( - {@JsonKey(name: 'id') required this.portionId, required this.comment}); - factory _PortionComment.fromJson(Map json) => - _$PortionCommentFromJson(json); - - @override - @JsonKey(name: 'id') - final int portionId; - @override - final String comment; - - /// Create a copy of PortionComment - /// with the given fields replaced by the non-null parameter values. - @override - @JsonKey(includeFromJson: false, includeToJson: false) - @pragma('vm:prefer-inline') - _$PortionCommentCopyWith<_PortionComment> get copyWith => - __$PortionCommentCopyWithImpl<_PortionComment>(this, _$identity); - - @override - Map toJson() { - return _$PortionCommentToJson( - this, - ); - } - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _PortionComment && - (identical(other.portionId, portionId) || - other.portionId == portionId) && - (identical(other.comment, comment) || other.comment == comment)); - } - - @JsonKey(includeFromJson: false, includeToJson: false) - @override - int get hashCode => Object.hash(runtimeType, portionId, comment); - - @override - String toString() { - return 'PortionComment(portionId: $portionId, comment: $comment)'; - } -} - -/// @nodoc -abstract mixin class _$PortionCommentCopyWith<$Res> - implements $PortionCommentCopyWith<$Res> { - factory _$PortionCommentCopyWith( - _PortionComment value, $Res Function(_PortionComment) _then) = - __$PortionCommentCopyWithImpl; - @override - @useResult - $Res call({@JsonKey(name: 'id') int portionId, String comment}); -} - -/// @nodoc -class __$PortionCommentCopyWithImpl<$Res> - implements _$PortionCommentCopyWith<$Res> { - __$PortionCommentCopyWithImpl(this._self, this._then); - - final _PortionComment _self; - final $Res Function(_PortionComment) _then; - - /// Create a copy of PortionComment - /// with the given fields replaced by the non-null parameter values. - @override - @pragma('vm:prefer-inline') - $Res call({ - Object? portionId = null, - Object? comment = null, - }) { - return _then(_PortionComment( - portionId: null == portionId - ? _self.portionId - : portionId // ignore: cast_nullable_to_non_nullable - as int, - comment: null == comment - ? _self.comment - : comment // ignore: cast_nullable_to_non_nullable - as String, - )); - } -} - -// dart format on diff --git a/lib/features/diary/models/consultant_review_model.g.dart b/lib/features/diary/models/consultant_review_model.g.dart deleted file mode 100644 index ad7fa537..00000000 --- a/lib/features/diary/models/consultant_review_model.g.dart +++ /dev/null @@ -1,67 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'consultant_review_model.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -_ConsultantReviewModel _$ConsultantReviewModelFromJson( - Map json) => - _ConsultantReviewModel( - planName: json['planName'] as String, - planDuration: (json['planDuration'] as num).toInt(), - currentPlanDay: (json['currentPlanDay'] as num).toInt(), - consultantComments: ConsultantComments.fromJson( - json['consultantComments'] as Map), - ); - -Map _$ConsultantReviewModelToJson( - _ConsultantReviewModel instance) => - { - 'planName': instance.planName, - 'planDuration': instance.planDuration, - 'currentPlanDay': instance.currentPlanDay, - 'consultantComments': instance.consultantComments, - }; - -_ConsultantComments _$ConsultantCommentsFromJson(Map json) => - _ConsultantComments( - dayComment: json['dayComment'] as String?, - mealsComments: (json['mealsComments'] as List) - .map((e) => MealComment.fromJson(e as Map)) - .toList(), - portionsComments: (json['portionsComments'] as List) - .map((e) => PortionComment.fromJson(e as Map)) - .toList(), - ); - -Map _$ConsultantCommentsToJson(_ConsultantComments instance) => - { - 'dayComment': instance.dayComment, - 'mealsComments': instance.mealsComments, - 'portionsComments': instance.portionsComments, - }; - -_MealComment _$MealCommentFromJson(Map json) => _MealComment( - comment: json['comment'] as String, - typeNo: (json['typeNo'] as num).toInt(), - ); - -Map _$MealCommentToJson(_MealComment instance) => - { - 'comment': instance.comment, - 'typeNo': instance.typeNo, - }; - -_PortionComment _$PortionCommentFromJson(Map json) => - _PortionComment( - portionId: (json['id'] as num).toInt(), - comment: json['comment'] as String, - ); - -Map _$PortionCommentToJson(_PortionComment instance) => - { - 'id': instance.portionId, - 'comment': instance.comment, - }; diff --git a/test/features/profile/services/profile_service_test.mocks.dart b/test/features/profile/services/profile_service_test.mocks.dart deleted file mode 100644 index 22c3af52..00000000 --- a/test/features/profile/services/profile_service_test.mocks.dart +++ /dev/null @@ -1,526 +0,0 @@ -// Mocks generated by Mockito 5.4.6 from annotations -// in eatvio_app/test/features/profile/services/profile_service_test.dart. -// Do not manually edit this file. - -// ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i6; -import 'dart:io' as _i10; - -import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart' - as _i7; -import 'package:eatvio_app/features/profile/models/chosen_diets_pagination_model.dart' - as _i4; -import 'package:eatvio_app/features/profile/models/circumference_model.dart' - as _i9; -import 'package:eatvio_app/features/profile/models/profile_model.dart' as _i2; -import 'package:eatvio_app/features/profile/models/sub_model.dart' as _i3; -import 'package:eatvio_app/features/profile/models/weight_model.dart' as _i8; -import 'package:eatvio_app/features/profile/services/profile_service.dart' - as _i5; -import 'package:mockito/mockito.dart' as _i1; -import 'package:mockito/src/dummies.dart' as _i11; - -// ignore_for_file: type=lint -// ignore_for_file: avoid_redundant_argument_values -// ignore_for_file: avoid_setters_without_getters -// ignore_for_file: comment_references -// ignore_for_file: deprecated_member_use -// ignore_for_file: deprecated_member_use_from_same_package -// ignore_for_file: implementation_imports -// ignore_for_file: invalid_use_of_visible_for_testing_member -// ignore_for_file: must_be_immutable -// ignore_for_file: prefer_const_constructors -// ignore_for_file: unnecessary_parenthesis -// ignore_for_file: camel_case_types -// ignore_for_file: subtype_of_sealed_class - -class _FakeProfileModel_0 extends _i1.SmartFake implements _i2.ProfileModel { - _FakeProfileModel_0( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -class _FakeProfileDetailsModel_1 extends _i1.SmartFake - implements _i2.ProfileDetailsModel { - _FakeProfileDetailsModel_1( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -class _FakeSubPaginationModel_2 extends _i1.SmartFake - implements _i3.SubPaginationModel { - _FakeSubPaginationModel_2( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -class _FakeChosenDietsPaginationModel_3 extends _i1.SmartFake - implements _i4.ChosenDietsPaginationModel { - _FakeChosenDietsPaginationModel_3( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -/// A class which mocks [ProfileService]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockProfileService extends _i1.Mock implements _i5.ProfileService { - @override - _i6.Future<_i2.ProfileModel> getProfileBySlug({required String? slug}) => - (super.noSuchMethod( - Invocation.method( - #getProfileBySlug, - [], - {#slug: slug}, - ), - returnValue: _i6.Future<_i2.ProfileModel>.value(_FakeProfileModel_0( - this, - Invocation.method( - #getProfileBySlug, - [], - {#slug: slug}, - ), - )), - returnValueForMissingStub: - _i6.Future<_i2.ProfileModel>.value(_FakeProfileModel_0( - this, - Invocation.method( - #getProfileBySlug, - [], - {#slug: slug}, - ), - )), - ) as _i6.Future<_i2.ProfileModel>); - - @override - _i6.Future<_i2.ProfileModel> getMyProfile() => (super.noSuchMethod( - Invocation.method( - #getMyProfile, - [], - ), - returnValue: _i6.Future<_i2.ProfileModel>.value(_FakeProfileModel_0( - this, - Invocation.method( - #getMyProfile, - [], - ), - )), - returnValueForMissingStub: - _i6.Future<_i2.ProfileModel>.value(_FakeProfileModel_0( - this, - Invocation.method( - #getMyProfile, - [], - ), - )), - ) as _i6.Future<_i2.ProfileModel>); - - @override - _i6.Future<_i2.ProfileDetailsModel> getMyProfileDetails() => - (super.noSuchMethod( - Invocation.method( - #getMyProfileDetails, - [], - ), - returnValue: _i6.Future<_i2.ProfileDetailsModel>.value( - _FakeProfileDetailsModel_1( - this, - Invocation.method( - #getMyProfileDetails, - [], - ), - )), - returnValueForMissingStub: _i6.Future<_i2.ProfileDetailsModel>.value( - _FakeProfileDetailsModel_1( - this, - Invocation.method( - #getMyProfileDetails, - [], - ), - )), - ) as _i6.Future<_i2.ProfileDetailsModel>); - - @override - _i6.Future<_i2.ProfileDetailsModel> getProfileDetailsBySlug( - {required String? slug}) => - (super.noSuchMethod( - Invocation.method( - #getProfileDetailsBySlug, - [], - {#slug: slug}, - ), - returnValue: _i6.Future<_i2.ProfileDetailsModel>.value( - _FakeProfileDetailsModel_1( - this, - Invocation.method( - #getProfileDetailsBySlug, - [], - {#slug: slug}, - ), - )), - returnValueForMissingStub: _i6.Future<_i2.ProfileDetailsModel>.value( - _FakeProfileDetailsModel_1( - this, - Invocation.method( - #getProfileDetailsBySlug, - [], - {#slug: slug}, - ), - )), - ) as _i6.Future<_i2.ProfileDetailsModel>); - - @override - _i6.Future addCircumference({ - required int? chestCircumference, - required int? waistCircumference, - required int? hipCircumference, - }) => - (super.noSuchMethod( - Invocation.method( - #addCircumference, - [], - { - #chestCircumference: chestCircumference, - #waistCircumference: waistCircumference, - #hipCircumference: hipCircumference, - }, - ), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) as _i6.Future); - - @override - _i6.Future addWeight({required double? weight}) => (super.noSuchMethod( - Invocation.method( - #addWeight, - [], - {#weight: weight}, - ), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) as _i6.Future); - - @override - _i6.Future> getBlogPostByAuthorSlug( - {required String? slug}) => - (super.noSuchMethod( - Invocation.method( - #getBlogPostByAuthorSlug, - [], - {#slug: slug}, - ), - returnValue: _i6.Future>.value( - <_i7.FeedBlogPostModel>[]), - returnValueForMissingStub: - _i6.Future>.value( - <_i7.FeedBlogPostModel>[]), - ) as _i6.Future>); - - @override - _i6.Future> getWeight({required String? period}) => - (super.noSuchMethod( - Invocation.method( - #getWeight, - [], - {#period: period}, - ), - returnValue: - _i6.Future>.value(<_i8.WeightModel>[]), - returnValueForMissingStub: - _i6.Future>.value(<_i8.WeightModel>[]), - ) as _i6.Future>); - - @override - _i6.Future> getWeightBySlug({ - required String? period, - required String? slug, - }) => - (super.noSuchMethod( - Invocation.method( - #getWeightBySlug, - [], - { - #period: period, - #slug: slug, - }, - ), - returnValue: - _i6.Future>.value(<_i8.WeightModel>[]), - returnValueForMissingStub: - _i6.Future>.value(<_i8.WeightModel>[]), - ) as _i6.Future>); - - @override - _i6.Future> getCircumferences( - {required String? period}) => - (super.noSuchMethod( - Invocation.method( - #getCircumferences, - [], - {#period: period}, - ), - returnValue: _i6.Future>.value( - <_i9.CircumferenceModel>[]), - returnValueForMissingStub: - _i6.Future>.value( - <_i9.CircumferenceModel>[]), - ) as _i6.Future>); - - @override - _i6.Future> getCircumferencesBySlug({ - required String? period, - required String? slug, - }) => - (super.noSuchMethod( - Invocation.method( - #getCircumferencesBySlug, - [], - { - #period: period, - #slug: slug, - }, - ), - returnValue: _i6.Future>.value( - <_i9.CircumferenceModel>[]), - returnValueForMissingStub: - _i6.Future>.value( - <_i9.CircumferenceModel>[]), - ) as _i6.Future>); - - @override - _i6.Future<_i3.SubPaginationModel> getSubscribers({ - required String? slug, - String? query, - }) => - (super.noSuchMethod( - Invocation.method( - #getSubscribers, - [], - { - #slug: slug, - #query: query, - }, - ), - returnValue: - _i6.Future<_i3.SubPaginationModel>.value(_FakeSubPaginationModel_2( - this, - Invocation.method( - #getSubscribers, - [], - { - #slug: slug, - #query: query, - }, - ), - )), - returnValueForMissingStub: - _i6.Future<_i3.SubPaginationModel>.value(_FakeSubPaginationModel_2( - this, - Invocation.method( - #getSubscribers, - [], - { - #slug: slug, - #query: query, - }, - ), - )), - ) as _i6.Future<_i3.SubPaginationModel>); - - @override - _i6.Future<_i3.SubPaginationModel> getSubscriptions({ - required String? slug, - String? query, - }) => - (super.noSuchMethod( - Invocation.method( - #getSubscriptions, - [], - { - #slug: slug, - #query: query, - }, - ), - returnValue: - _i6.Future<_i3.SubPaginationModel>.value(_FakeSubPaginationModel_2( - this, - Invocation.method( - #getSubscriptions, - [], - { - #slug: slug, - #query: query, - }, - ), - )), - returnValueForMissingStub: - _i6.Future<_i3.SubPaginationModel>.value(_FakeSubPaginationModel_2( - this, - Invocation.method( - #getSubscriptions, - [], - { - #slug: slug, - #query: query, - }, - ), - )), - ) as _i6.Future<_i3.SubPaginationModel>); - - @override - _i6.Future subscribe({required String? slug}) => (super.noSuchMethod( - Invocation.method( - #subscribe, - [], - {#slug: slug}, - ), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) as _i6.Future); - - @override - _i6.Future unsubscribe({required String? slug}) => (super.noSuchMethod( - Invocation.method( - #unsubscribe, - [], - {#slug: slug}, - ), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) as _i6.Future); - - @override - _i6.Future uploadPhoto({required _i10.File? photo}) => - (super.noSuchMethod( - Invocation.method( - #uploadPhoto, - [], - {#photo: photo}, - ), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) as _i6.Future); - - @override - _i6.Future deletePhoto() => (super.noSuchMethod( - Invocation.method( - #deletePhoto, - [], - ), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) as _i6.Future); - - @override - _i6.Future<_i4.ChosenDietsPaginationModel> getChosenDiets() => - (super.noSuchMethod( - Invocation.method( - #getChosenDiets, - [], - ), - returnValue: _i6.Future<_i4.ChosenDietsPaginationModel>.value( - _FakeChosenDietsPaginationModel_3( - this, - Invocation.method( - #getChosenDiets, - [], - ), - )), - returnValueForMissingStub: - _i6.Future<_i4.ChosenDietsPaginationModel>.value( - _FakeChosenDietsPaginationModel_3( - this, - Invocation.method( - #getChosenDiets, - [], - ), - )), - ) as _i6.Future<_i4.ChosenDietsPaginationModel>); - - @override - _i6.Future getPassKey({required String? json}) => (super.noSuchMethod( - Invocation.method( - #getPassKey, - [], - {#json: json}, - ), - returnValue: _i6.Future.value(_i11.dummyValue( - this, - Invocation.method( - #getPassKey, - [], - {#json: json}, - ), - )), - returnValueForMissingStub: - _i6.Future.value(_i11.dummyValue( - this, - Invocation.method( - #getPassKey, - [], - {#json: json}, - ), - )), - ) as _i6.Future); - - @override - _i6.Future getMyPhone() => (super.noSuchMethod( - Invocation.method( - #getMyPhone, - [], - ), - returnValue: _i6.Future.value(_i11.dummyValue( - this, - Invocation.method( - #getMyPhone, - [], - ), - )), - returnValueForMissingStub: - _i6.Future.value(_i11.dummyValue( - this, - Invocation.method( - #getMyPhone, - [], - ), - )), - ) as _i6.Future); - - @override - _i6.Future sendEmailCode() => (super.noSuchMethod( - Invocation.method( - #sendEmailCode, - [], - ), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) as _i6.Future); - - @override - _i6.Future tryVerifyEmailByCode({required String? code}) => - (super.noSuchMethod( - Invocation.method( - #tryVerifyEmailByCode, - [], - {#code: code}, - ), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) as _i6.Future); -} From 08cf1ad370c993841149094223a09ca7bffdaaa7 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Fri, 11 Jul 2025 14:44:44 +0300 Subject: [PATCH 019/134] =?UTF-8?q?=D0=A3=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD?= =?UTF-8?q?=D1=8B=20=D1=81=D0=B3=D0=B5=D0=BD=D0=B5=D1=80=D0=B8=D1=80=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D0=BD=D0=BD=D1=8B=D0=B5=20=D1=84=D0=B0=D0=B9=D0=BB?= =?UTF-8?q?=D1=8B=20(freezed,=20g.dart,=20build)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../consultant_review_model.freezed.dart | 1432 ----------------- .../models/consultant_review_model.g.dart | 67 - .../services/profile_service_test.mocks.dart | 526 ------ 3 files changed, 2025 deletions(-) delete mode 100644 lib/features/diary/models/consultant_review_model.freezed.dart delete mode 100644 lib/features/diary/models/consultant_review_model.g.dart delete mode 100644 test/features/profile/services/profile_service_test.mocks.dart diff --git a/lib/features/diary/models/consultant_review_model.freezed.dart b/lib/features/diary/models/consultant_review_model.freezed.dart deleted file mode 100644 index 36c72066..00000000 --- a/lib/features/diary/models/consultant_review_model.freezed.dart +++ /dev/null @@ -1,1432 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND -// coverage:ignore-file -// ignore_for_file: type=lint -// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark - -part of 'consultant_review_model.dart'; - -// ************************************************************************** -// FreezedGenerator -// ************************************************************************** - -// dart format off -T _$identity(T value) => value; - -/// @nodoc -mixin _$ConsultantReviewModel { - String get planName; - int get planDuration; - int get currentPlanDay; - ConsultantComments get consultantComments; - - /// Create a copy of ConsultantReviewModel - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - @pragma('vm:prefer-inline') - $ConsultantReviewModelCopyWith get copyWith => - _$ConsultantReviewModelCopyWithImpl( - this as ConsultantReviewModel, _$identity); - - /// Serializes this ConsultantReviewModel to a JSON map. - Map toJson(); - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is ConsultantReviewModel && - (identical(other.planName, planName) || - other.planName == planName) && - (identical(other.planDuration, planDuration) || - other.planDuration == planDuration) && - (identical(other.currentPlanDay, currentPlanDay) || - other.currentPlanDay == currentPlanDay) && - (identical(other.consultantComments, consultantComments) || - other.consultantComments == consultantComments)); - } - - @JsonKey(includeFromJson: false, includeToJson: false) - @override - int get hashCode => Object.hash( - runtimeType, planName, planDuration, currentPlanDay, consultantComments); - - @override - String toString() { - return 'ConsultantReviewModel(planName: $planName, planDuration: $planDuration, currentPlanDay: $currentPlanDay, consultantComments: $consultantComments)'; - } -} - -/// @nodoc -abstract mixin class $ConsultantReviewModelCopyWith<$Res> { - factory $ConsultantReviewModelCopyWith(ConsultantReviewModel value, - $Res Function(ConsultantReviewModel) _then) = - _$ConsultantReviewModelCopyWithImpl; - @useResult - $Res call( - {String planName, - int planDuration, - int currentPlanDay, - ConsultantComments consultantComments}); - - $ConsultantCommentsCopyWith<$Res> get consultantComments; -} - -/// @nodoc -class _$ConsultantReviewModelCopyWithImpl<$Res> - implements $ConsultantReviewModelCopyWith<$Res> { - _$ConsultantReviewModelCopyWithImpl(this._self, this._then); - - final ConsultantReviewModel _self; - final $Res Function(ConsultantReviewModel) _then; - - /// Create a copy of ConsultantReviewModel - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? planName = null, - Object? planDuration = null, - Object? currentPlanDay = null, - Object? consultantComments = null, - }) { - return _then(_self.copyWith( - planName: null == planName - ? _self.planName - : planName // ignore: cast_nullable_to_non_nullable - as String, - planDuration: null == planDuration - ? _self.planDuration - : planDuration // ignore: cast_nullable_to_non_nullable - as int, - currentPlanDay: null == currentPlanDay - ? _self.currentPlanDay - : currentPlanDay // ignore: cast_nullable_to_non_nullable - as int, - consultantComments: null == consultantComments - ? _self.consultantComments - : consultantComments // ignore: cast_nullable_to_non_nullable - as ConsultantComments, - )); - } - - /// Create a copy of ConsultantReviewModel - /// with the given fields replaced by the non-null parameter values. - @override - @pragma('vm:prefer-inline') - $ConsultantCommentsCopyWith<$Res> get consultantComments { - return $ConsultantCommentsCopyWith<$Res>(_self.consultantComments, (value) { - return _then(_self.copyWith(consultantComments: value)); - }); - } -} - -/// Adds pattern-matching-related methods to [ConsultantReviewModel]. -extension ConsultantReviewModelPatterns on ConsultantReviewModel { - /// A variant of `map` that fallback to returning `orElse`. - /// - /// It is equivalent to doing: - /// ```dart - /// switch (sealedClass) { - /// case final Subclass value: - /// return ...; - /// case _: - /// return orElse(); - /// } - /// ``` - - @optionalTypeArgs - TResult maybeMap( - TResult Function(_ConsultantReviewModel value)? $default, { - required TResult orElse(), - }) { - final _that = this; - switch (_that) { - case _ConsultantReviewModel() when $default != null: - return $default(_that); - case _: - return orElse(); - } - } - - /// A `switch`-like method, using callbacks. - /// - /// Callbacks receives the raw object, upcasted. - /// It is equivalent to doing: - /// ```dart - /// switch (sealedClass) { - /// case final Subclass value: - /// return ...; - /// case final Subclass2 value: - /// return ...; - /// } - /// ``` - - @optionalTypeArgs - TResult map( - TResult Function(_ConsultantReviewModel value) $default, - ) { - final _that = this; - switch (_that) { - case _ConsultantReviewModel(): - return $default(_that); - case _: - throw StateError('Unexpected subclass'); - } - } - - /// A variant of `map` that fallback to returning `null`. - /// - /// It is equivalent to doing: - /// ```dart - /// switch (sealedClass) { - /// case final Subclass value: - /// return ...; - /// case _: - /// return null; - /// } - /// ``` - - @optionalTypeArgs - TResult? mapOrNull( - TResult? Function(_ConsultantReviewModel value)? $default, - ) { - final _that = this; - switch (_that) { - case _ConsultantReviewModel() when $default != null: - return $default(_that); - case _: - return null; - } - } - - /// A variant of `when` that fallback to an `orElse` callback. - /// - /// It is equivalent to doing: - /// ```dart - /// switch (sealedClass) { - /// case Subclass(:final field): - /// return ...; - /// case _: - /// return orElse(); - /// } - /// ``` - - @optionalTypeArgs - TResult maybeWhen( - TResult Function(String planName, int planDuration, int currentPlanDay, - ConsultantComments consultantComments)? - $default, { - required TResult orElse(), - }) { - final _that = this; - switch (_that) { - case _ConsultantReviewModel() when $default != null: - return $default(_that.planName, _that.planDuration, - _that.currentPlanDay, _that.consultantComments); - case _: - return orElse(); - } - } - - /// A `switch`-like method, using callbacks. - /// - /// As opposed to `map`, this offers destructuring. - /// It is equivalent to doing: - /// ```dart - /// switch (sealedClass) { - /// case Subclass(:final field): - /// return ...; - /// case Subclass2(:final field2): - /// return ...; - /// } - /// ``` - - @optionalTypeArgs - TResult when( - TResult Function(String planName, int planDuration, int currentPlanDay, - ConsultantComments consultantComments) - $default, - ) { - final _that = this; - switch (_that) { - case _ConsultantReviewModel(): - return $default(_that.planName, _that.planDuration, - _that.currentPlanDay, _that.consultantComments); - case _: - throw StateError('Unexpected subclass'); - } - } - - /// A variant of `when` that fallback to returning `null` - /// - /// It is equivalent to doing: - /// ```dart - /// switch (sealedClass) { - /// case Subclass(:final field): - /// return ...; - /// case _: - /// return null; - /// } - /// ``` - - @optionalTypeArgs - TResult? whenOrNull( - TResult? Function(String planName, int planDuration, int currentPlanDay, - ConsultantComments consultantComments)? - $default, - ) { - final _that = this; - switch (_that) { - case _ConsultantReviewModel() when $default != null: - return $default(_that.planName, _that.planDuration, - _that.currentPlanDay, _that.consultantComments); - case _: - return null; - } - } -} - -/// @nodoc -@JsonSerializable() -class _ConsultantReviewModel implements ConsultantReviewModel { - _ConsultantReviewModel( - {required this.planName, - required this.planDuration, - required this.currentPlanDay, - required this.consultantComments}); - factory _ConsultantReviewModel.fromJson(Map json) => - _$ConsultantReviewModelFromJson(json); - - @override - final String planName; - @override - final int planDuration; - @override - final int currentPlanDay; - @override - final ConsultantComments consultantComments; - - /// Create a copy of ConsultantReviewModel - /// with the given fields replaced by the non-null parameter values. - @override - @JsonKey(includeFromJson: false, includeToJson: false) - @pragma('vm:prefer-inline') - _$ConsultantReviewModelCopyWith<_ConsultantReviewModel> get copyWith => - __$ConsultantReviewModelCopyWithImpl<_ConsultantReviewModel>( - this, _$identity); - - @override - Map toJson() { - return _$ConsultantReviewModelToJson( - this, - ); - } - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _ConsultantReviewModel && - (identical(other.planName, planName) || - other.planName == planName) && - (identical(other.planDuration, planDuration) || - other.planDuration == planDuration) && - (identical(other.currentPlanDay, currentPlanDay) || - other.currentPlanDay == currentPlanDay) && - (identical(other.consultantComments, consultantComments) || - other.consultantComments == consultantComments)); - } - - @JsonKey(includeFromJson: false, includeToJson: false) - @override - int get hashCode => Object.hash( - runtimeType, planName, planDuration, currentPlanDay, consultantComments); - - @override - String toString() { - return 'ConsultantReviewModel(planName: $planName, planDuration: $planDuration, currentPlanDay: $currentPlanDay, consultantComments: $consultantComments)'; - } -} - -/// @nodoc -abstract mixin class _$ConsultantReviewModelCopyWith<$Res> - implements $ConsultantReviewModelCopyWith<$Res> { - factory _$ConsultantReviewModelCopyWith(_ConsultantReviewModel value, - $Res Function(_ConsultantReviewModel) _then) = - __$ConsultantReviewModelCopyWithImpl; - @override - @useResult - $Res call( - {String planName, - int planDuration, - int currentPlanDay, - ConsultantComments consultantComments}); - - @override - $ConsultantCommentsCopyWith<$Res> get consultantComments; -} - -/// @nodoc -class __$ConsultantReviewModelCopyWithImpl<$Res> - implements _$ConsultantReviewModelCopyWith<$Res> { - __$ConsultantReviewModelCopyWithImpl(this._self, this._then); - - final _ConsultantReviewModel _self; - final $Res Function(_ConsultantReviewModel) _then; - - /// Create a copy of ConsultantReviewModel - /// with the given fields replaced by the non-null parameter values. - @override - @pragma('vm:prefer-inline') - $Res call({ - Object? planName = null, - Object? planDuration = null, - Object? currentPlanDay = null, - Object? consultantComments = null, - }) { - return _then(_ConsultantReviewModel( - planName: null == planName - ? _self.planName - : planName // ignore: cast_nullable_to_non_nullable - as String, - planDuration: null == planDuration - ? _self.planDuration - : planDuration // ignore: cast_nullable_to_non_nullable - as int, - currentPlanDay: null == currentPlanDay - ? _self.currentPlanDay - : currentPlanDay // ignore: cast_nullable_to_non_nullable - as int, - consultantComments: null == consultantComments - ? _self.consultantComments - : consultantComments // ignore: cast_nullable_to_non_nullable - as ConsultantComments, - )); - } - - /// Create a copy of ConsultantReviewModel - /// with the given fields replaced by the non-null parameter values. - @override - @pragma('vm:prefer-inline') - $ConsultantCommentsCopyWith<$Res> get consultantComments { - return $ConsultantCommentsCopyWith<$Res>(_self.consultantComments, (value) { - return _then(_self.copyWith(consultantComments: value)); - }); - } -} - -/// @nodoc -mixin _$ConsultantComments { - String? get dayComment; - List get mealsComments; - List get portionsComments; - - /// Create a copy of ConsultantComments - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - @pragma('vm:prefer-inline') - $ConsultantCommentsCopyWith get copyWith => - _$ConsultantCommentsCopyWithImpl( - this as ConsultantComments, _$identity); - - /// Serializes this ConsultantComments to a JSON map. - Map toJson(); - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is ConsultantComments && - (identical(other.dayComment, dayComment) || - other.dayComment == dayComment) && - const DeepCollectionEquality() - .equals(other.mealsComments, mealsComments) && - const DeepCollectionEquality() - .equals(other.portionsComments, portionsComments)); - } - - @JsonKey(includeFromJson: false, includeToJson: false) - @override - int get hashCode => Object.hash( - runtimeType, - dayComment, - const DeepCollectionEquality().hash(mealsComments), - const DeepCollectionEquality().hash(portionsComments)); - - @override - String toString() { - return 'ConsultantComments(dayComment: $dayComment, mealsComments: $mealsComments, portionsComments: $portionsComments)'; - } -} - -/// @nodoc -abstract mixin class $ConsultantCommentsCopyWith<$Res> { - factory $ConsultantCommentsCopyWith( - ConsultantComments value, $Res Function(ConsultantComments) _then) = - _$ConsultantCommentsCopyWithImpl; - @useResult - $Res call( - {String? dayComment, - List mealsComments, - List portionsComments}); -} - -/// @nodoc -class _$ConsultantCommentsCopyWithImpl<$Res> - implements $ConsultantCommentsCopyWith<$Res> { - _$ConsultantCommentsCopyWithImpl(this._self, this._then); - - final ConsultantComments _self; - final $Res Function(ConsultantComments) _then; - - /// Create a copy of ConsultantComments - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? dayComment = freezed, - Object? mealsComments = null, - Object? portionsComments = null, - }) { - return _then(_self.copyWith( - dayComment: freezed == dayComment - ? _self.dayComment - : dayComment // ignore: cast_nullable_to_non_nullable - as String?, - mealsComments: null == mealsComments - ? _self.mealsComments - : mealsComments // ignore: cast_nullable_to_non_nullable - as List, - portionsComments: null == portionsComments - ? _self.portionsComments - : portionsComments // ignore: cast_nullable_to_non_nullable - as List, - )); - } -} - -/// Adds pattern-matching-related methods to [ConsultantComments]. -extension ConsultantCommentsPatterns on ConsultantComments { - /// A variant of `map` that fallback to returning `orElse`. - /// - /// It is equivalent to doing: - /// ```dart - /// switch (sealedClass) { - /// case final Subclass value: - /// return ...; - /// case _: - /// return orElse(); - /// } - /// ``` - - @optionalTypeArgs - TResult maybeMap( - TResult Function(_ConsultantComments value)? $default, { - required TResult orElse(), - }) { - final _that = this; - switch (_that) { - case _ConsultantComments() when $default != null: - return $default(_that); - case _: - return orElse(); - } - } - - /// A `switch`-like method, using callbacks. - /// - /// Callbacks receives the raw object, upcasted. - /// It is equivalent to doing: - /// ```dart - /// switch (sealedClass) { - /// case final Subclass value: - /// return ...; - /// case final Subclass2 value: - /// return ...; - /// } - /// ``` - - @optionalTypeArgs - TResult map( - TResult Function(_ConsultantComments value) $default, - ) { - final _that = this; - switch (_that) { - case _ConsultantComments(): - return $default(_that); - case _: - throw StateError('Unexpected subclass'); - } - } - - /// A variant of `map` that fallback to returning `null`. - /// - /// It is equivalent to doing: - /// ```dart - /// switch (sealedClass) { - /// case final Subclass value: - /// return ...; - /// case _: - /// return null; - /// } - /// ``` - - @optionalTypeArgs - TResult? mapOrNull( - TResult? Function(_ConsultantComments value)? $default, - ) { - final _that = this; - switch (_that) { - case _ConsultantComments() when $default != null: - return $default(_that); - case _: - return null; - } - } - - /// A variant of `when` that fallback to an `orElse` callback. - /// - /// It is equivalent to doing: - /// ```dart - /// switch (sealedClass) { - /// case Subclass(:final field): - /// return ...; - /// case _: - /// return orElse(); - /// } - /// ``` - - @optionalTypeArgs - TResult maybeWhen( - TResult Function(String? dayComment, List mealsComments, - List portionsComments)? - $default, { - required TResult orElse(), - }) { - final _that = this; - switch (_that) { - case _ConsultantComments() when $default != null: - return $default( - _that.dayComment, _that.mealsComments, _that.portionsComments); - case _: - return orElse(); - } - } - - /// A `switch`-like method, using callbacks. - /// - /// As opposed to `map`, this offers destructuring. - /// It is equivalent to doing: - /// ```dart - /// switch (sealedClass) { - /// case Subclass(:final field): - /// return ...; - /// case Subclass2(:final field2): - /// return ...; - /// } - /// ``` - - @optionalTypeArgs - TResult when( - TResult Function(String? dayComment, List mealsComments, - List portionsComments) - $default, - ) { - final _that = this; - switch (_that) { - case _ConsultantComments(): - return $default( - _that.dayComment, _that.mealsComments, _that.portionsComments); - case _: - throw StateError('Unexpected subclass'); - } - } - - /// A variant of `when` that fallback to returning `null` - /// - /// It is equivalent to doing: - /// ```dart - /// switch (sealedClass) { - /// case Subclass(:final field): - /// return ...; - /// case _: - /// return null; - /// } - /// ``` - - @optionalTypeArgs - TResult? whenOrNull( - TResult? Function(String? dayComment, List mealsComments, - List portionsComments)? - $default, - ) { - final _that = this; - switch (_that) { - case _ConsultantComments() when $default != null: - return $default( - _that.dayComment, _that.mealsComments, _that.portionsComments); - case _: - return null; - } - } -} - -/// @nodoc -@JsonSerializable() -class _ConsultantComments implements ConsultantComments { - _ConsultantComments( - {required this.dayComment, - required final List mealsComments, - required final List portionsComments}) - : _mealsComments = mealsComments, - _portionsComments = portionsComments; - factory _ConsultantComments.fromJson(Map json) => - _$ConsultantCommentsFromJson(json); - - @override - final String? dayComment; - final List _mealsComments; - @override - List get mealsComments { - if (_mealsComments is EqualUnmodifiableListView) return _mealsComments; - // ignore: implicit_dynamic_type - return EqualUnmodifiableListView(_mealsComments); - } - - final List _portionsComments; - @override - List get portionsComments { - if (_portionsComments is EqualUnmodifiableListView) - return _portionsComments; - // ignore: implicit_dynamic_type - return EqualUnmodifiableListView(_portionsComments); - } - - /// Create a copy of ConsultantComments - /// with the given fields replaced by the non-null parameter values. - @override - @JsonKey(includeFromJson: false, includeToJson: false) - @pragma('vm:prefer-inline') - _$ConsultantCommentsCopyWith<_ConsultantComments> get copyWith => - __$ConsultantCommentsCopyWithImpl<_ConsultantComments>(this, _$identity); - - @override - Map toJson() { - return _$ConsultantCommentsToJson( - this, - ); - } - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _ConsultantComments && - (identical(other.dayComment, dayComment) || - other.dayComment == dayComment) && - const DeepCollectionEquality() - .equals(other._mealsComments, _mealsComments) && - const DeepCollectionEquality() - .equals(other._portionsComments, _portionsComments)); - } - - @JsonKey(includeFromJson: false, includeToJson: false) - @override - int get hashCode => Object.hash( - runtimeType, - dayComment, - const DeepCollectionEquality().hash(_mealsComments), - const DeepCollectionEquality().hash(_portionsComments)); - - @override - String toString() { - return 'ConsultantComments(dayComment: $dayComment, mealsComments: $mealsComments, portionsComments: $portionsComments)'; - } -} - -/// @nodoc -abstract mixin class _$ConsultantCommentsCopyWith<$Res> - implements $ConsultantCommentsCopyWith<$Res> { - factory _$ConsultantCommentsCopyWith( - _ConsultantComments value, $Res Function(_ConsultantComments) _then) = - __$ConsultantCommentsCopyWithImpl; - @override - @useResult - $Res call( - {String? dayComment, - List mealsComments, - List portionsComments}); -} - -/// @nodoc -class __$ConsultantCommentsCopyWithImpl<$Res> - implements _$ConsultantCommentsCopyWith<$Res> { - __$ConsultantCommentsCopyWithImpl(this._self, this._then); - - final _ConsultantComments _self; - final $Res Function(_ConsultantComments) _then; - - /// Create a copy of ConsultantComments - /// with the given fields replaced by the non-null parameter values. - @override - @pragma('vm:prefer-inline') - $Res call({ - Object? dayComment = freezed, - Object? mealsComments = null, - Object? portionsComments = null, - }) { - return _then(_ConsultantComments( - dayComment: freezed == dayComment - ? _self.dayComment - : dayComment // ignore: cast_nullable_to_non_nullable - as String?, - mealsComments: null == mealsComments - ? _self._mealsComments - : mealsComments // ignore: cast_nullable_to_non_nullable - as List, - portionsComments: null == portionsComments - ? _self._portionsComments - : portionsComments // ignore: cast_nullable_to_non_nullable - as List, - )); - } -} - -/// @nodoc -mixin _$MealComment { - String get comment; - int get typeNo; - - /// Create a copy of MealComment - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - @pragma('vm:prefer-inline') - $MealCommentCopyWith get copyWith => - _$MealCommentCopyWithImpl(this as MealComment, _$identity); - - /// Serializes this MealComment to a JSON map. - Map toJson(); - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is MealComment && - (identical(other.comment, comment) || other.comment == comment) && - (identical(other.typeNo, typeNo) || other.typeNo == typeNo)); - } - - @JsonKey(includeFromJson: false, includeToJson: false) - @override - int get hashCode => Object.hash(runtimeType, comment, typeNo); - - @override - String toString() { - return 'MealComment(comment: $comment, typeNo: $typeNo)'; - } -} - -/// @nodoc -abstract mixin class $MealCommentCopyWith<$Res> { - factory $MealCommentCopyWith( - MealComment value, $Res Function(MealComment) _then) = - _$MealCommentCopyWithImpl; - @useResult - $Res call({String comment, int typeNo}); -} - -/// @nodoc -class _$MealCommentCopyWithImpl<$Res> implements $MealCommentCopyWith<$Res> { - _$MealCommentCopyWithImpl(this._self, this._then); - - final MealComment _self; - final $Res Function(MealComment) _then; - - /// Create a copy of MealComment - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? comment = null, - Object? typeNo = null, - }) { - return _then(_self.copyWith( - comment: null == comment - ? _self.comment - : comment // ignore: cast_nullable_to_non_nullable - as String, - typeNo: null == typeNo - ? _self.typeNo - : typeNo // ignore: cast_nullable_to_non_nullable - as int, - )); - } -} - -/// Adds pattern-matching-related methods to [MealComment]. -extension MealCommentPatterns on MealComment { - /// A variant of `map` that fallback to returning `orElse`. - /// - /// It is equivalent to doing: - /// ```dart - /// switch (sealedClass) { - /// case final Subclass value: - /// return ...; - /// case _: - /// return orElse(); - /// } - /// ``` - - @optionalTypeArgs - TResult maybeMap( - TResult Function(_MealComment value)? $default, { - required TResult orElse(), - }) { - final _that = this; - switch (_that) { - case _MealComment() when $default != null: - return $default(_that); - case _: - return orElse(); - } - } - - /// A `switch`-like method, using callbacks. - /// - /// Callbacks receives the raw object, upcasted. - /// It is equivalent to doing: - /// ```dart - /// switch (sealedClass) { - /// case final Subclass value: - /// return ...; - /// case final Subclass2 value: - /// return ...; - /// } - /// ``` - - @optionalTypeArgs - TResult map( - TResult Function(_MealComment value) $default, - ) { - final _that = this; - switch (_that) { - case _MealComment(): - return $default(_that); - case _: - throw StateError('Unexpected subclass'); - } - } - - /// A variant of `map` that fallback to returning `null`. - /// - /// It is equivalent to doing: - /// ```dart - /// switch (sealedClass) { - /// case final Subclass value: - /// return ...; - /// case _: - /// return null; - /// } - /// ``` - - @optionalTypeArgs - TResult? mapOrNull( - TResult? Function(_MealComment value)? $default, - ) { - final _that = this; - switch (_that) { - case _MealComment() when $default != null: - return $default(_that); - case _: - return null; - } - } - - /// A variant of `when` that fallback to an `orElse` callback. - /// - /// It is equivalent to doing: - /// ```dart - /// switch (sealedClass) { - /// case Subclass(:final field): - /// return ...; - /// case _: - /// return orElse(); - /// } - /// ``` - - @optionalTypeArgs - TResult maybeWhen( - TResult Function(String comment, int typeNo)? $default, { - required TResult orElse(), - }) { - final _that = this; - switch (_that) { - case _MealComment() when $default != null: - return $default(_that.comment, _that.typeNo); - case _: - return orElse(); - } - } - - /// A `switch`-like method, using callbacks. - /// - /// As opposed to `map`, this offers destructuring. - /// It is equivalent to doing: - /// ```dart - /// switch (sealedClass) { - /// case Subclass(:final field): - /// return ...; - /// case Subclass2(:final field2): - /// return ...; - /// } - /// ``` - - @optionalTypeArgs - TResult when( - TResult Function(String comment, int typeNo) $default, - ) { - final _that = this; - switch (_that) { - case _MealComment(): - return $default(_that.comment, _that.typeNo); - case _: - throw StateError('Unexpected subclass'); - } - } - - /// A variant of `when` that fallback to returning `null` - /// - /// It is equivalent to doing: - /// ```dart - /// switch (sealedClass) { - /// case Subclass(:final field): - /// return ...; - /// case _: - /// return null; - /// } - /// ``` - - @optionalTypeArgs - TResult? whenOrNull( - TResult? Function(String comment, int typeNo)? $default, - ) { - final _that = this; - switch (_that) { - case _MealComment() when $default != null: - return $default(_that.comment, _that.typeNo); - case _: - return null; - } - } -} - -/// @nodoc -@JsonSerializable() -class _MealComment implements MealComment { - _MealComment({required this.comment, required this.typeNo}); - factory _MealComment.fromJson(Map json) => - _$MealCommentFromJson(json); - - @override - final String comment; - @override - final int typeNo; - - /// Create a copy of MealComment - /// with the given fields replaced by the non-null parameter values. - @override - @JsonKey(includeFromJson: false, includeToJson: false) - @pragma('vm:prefer-inline') - _$MealCommentCopyWith<_MealComment> get copyWith => - __$MealCommentCopyWithImpl<_MealComment>(this, _$identity); - - @override - Map toJson() { - return _$MealCommentToJson( - this, - ); - } - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _MealComment && - (identical(other.comment, comment) || other.comment == comment) && - (identical(other.typeNo, typeNo) || other.typeNo == typeNo)); - } - - @JsonKey(includeFromJson: false, includeToJson: false) - @override - int get hashCode => Object.hash(runtimeType, comment, typeNo); - - @override - String toString() { - return 'MealComment(comment: $comment, typeNo: $typeNo)'; - } -} - -/// @nodoc -abstract mixin class _$MealCommentCopyWith<$Res> - implements $MealCommentCopyWith<$Res> { - factory _$MealCommentCopyWith( - _MealComment value, $Res Function(_MealComment) _then) = - __$MealCommentCopyWithImpl; - @override - @useResult - $Res call({String comment, int typeNo}); -} - -/// @nodoc -class __$MealCommentCopyWithImpl<$Res> implements _$MealCommentCopyWith<$Res> { - __$MealCommentCopyWithImpl(this._self, this._then); - - final _MealComment _self; - final $Res Function(_MealComment) _then; - - /// Create a copy of MealComment - /// with the given fields replaced by the non-null parameter values. - @override - @pragma('vm:prefer-inline') - $Res call({ - Object? comment = null, - Object? typeNo = null, - }) { - return _then(_MealComment( - comment: null == comment - ? _self.comment - : comment // ignore: cast_nullable_to_non_nullable - as String, - typeNo: null == typeNo - ? _self.typeNo - : typeNo // ignore: cast_nullable_to_non_nullable - as int, - )); - } -} - -/// @nodoc -mixin _$PortionComment { - @JsonKey(name: 'id') - int get portionId; - String get comment; - - /// Create a copy of PortionComment - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - @pragma('vm:prefer-inline') - $PortionCommentCopyWith get copyWith => - _$PortionCommentCopyWithImpl( - this as PortionComment, _$identity); - - /// Serializes this PortionComment to a JSON map. - Map toJson(); - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is PortionComment && - (identical(other.portionId, portionId) || - other.portionId == portionId) && - (identical(other.comment, comment) || other.comment == comment)); - } - - @JsonKey(includeFromJson: false, includeToJson: false) - @override - int get hashCode => Object.hash(runtimeType, portionId, comment); - - @override - String toString() { - return 'PortionComment(portionId: $portionId, comment: $comment)'; - } -} - -/// @nodoc -abstract mixin class $PortionCommentCopyWith<$Res> { - factory $PortionCommentCopyWith( - PortionComment value, $Res Function(PortionComment) _then) = - _$PortionCommentCopyWithImpl; - @useResult - $Res call({@JsonKey(name: 'id') int portionId, String comment}); -} - -/// @nodoc -class _$PortionCommentCopyWithImpl<$Res> - implements $PortionCommentCopyWith<$Res> { - _$PortionCommentCopyWithImpl(this._self, this._then); - - final PortionComment _self; - final $Res Function(PortionComment) _then; - - /// Create a copy of PortionComment - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? portionId = null, - Object? comment = null, - }) { - return _then(_self.copyWith( - portionId: null == portionId - ? _self.portionId - : portionId // ignore: cast_nullable_to_non_nullable - as int, - comment: null == comment - ? _self.comment - : comment // ignore: cast_nullable_to_non_nullable - as String, - )); - } -} - -/// Adds pattern-matching-related methods to [PortionComment]. -extension PortionCommentPatterns on PortionComment { - /// A variant of `map` that fallback to returning `orElse`. - /// - /// It is equivalent to doing: - /// ```dart - /// switch (sealedClass) { - /// case final Subclass value: - /// return ...; - /// case _: - /// return orElse(); - /// } - /// ``` - - @optionalTypeArgs - TResult maybeMap( - TResult Function(_PortionComment value)? $default, { - required TResult orElse(), - }) { - final _that = this; - switch (_that) { - case _PortionComment() when $default != null: - return $default(_that); - case _: - return orElse(); - } - } - - /// A `switch`-like method, using callbacks. - /// - /// Callbacks receives the raw object, upcasted. - /// It is equivalent to doing: - /// ```dart - /// switch (sealedClass) { - /// case final Subclass value: - /// return ...; - /// case final Subclass2 value: - /// return ...; - /// } - /// ``` - - @optionalTypeArgs - TResult map( - TResult Function(_PortionComment value) $default, - ) { - final _that = this; - switch (_that) { - case _PortionComment(): - return $default(_that); - case _: - throw StateError('Unexpected subclass'); - } - } - - /// A variant of `map` that fallback to returning `null`. - /// - /// It is equivalent to doing: - /// ```dart - /// switch (sealedClass) { - /// case final Subclass value: - /// return ...; - /// case _: - /// return null; - /// } - /// ``` - - @optionalTypeArgs - TResult? mapOrNull( - TResult? Function(_PortionComment value)? $default, - ) { - final _that = this; - switch (_that) { - case _PortionComment() when $default != null: - return $default(_that); - case _: - return null; - } - } - - /// A variant of `when` that fallback to an `orElse` callback. - /// - /// It is equivalent to doing: - /// ```dart - /// switch (sealedClass) { - /// case Subclass(:final field): - /// return ...; - /// case _: - /// return orElse(); - /// } - /// ``` - - @optionalTypeArgs - TResult maybeWhen( - TResult Function(@JsonKey(name: 'id') int portionId, String comment)? - $default, { - required TResult orElse(), - }) { - final _that = this; - switch (_that) { - case _PortionComment() when $default != null: - return $default(_that.portionId, _that.comment); - case _: - return orElse(); - } - } - - /// A `switch`-like method, using callbacks. - /// - /// As opposed to `map`, this offers destructuring. - /// It is equivalent to doing: - /// ```dart - /// switch (sealedClass) { - /// case Subclass(:final field): - /// return ...; - /// case Subclass2(:final field2): - /// return ...; - /// } - /// ``` - - @optionalTypeArgs - TResult when( - TResult Function(@JsonKey(name: 'id') int portionId, String comment) - $default, - ) { - final _that = this; - switch (_that) { - case _PortionComment(): - return $default(_that.portionId, _that.comment); - case _: - throw StateError('Unexpected subclass'); - } - } - - /// A variant of `when` that fallback to returning `null` - /// - /// It is equivalent to doing: - /// ```dart - /// switch (sealedClass) { - /// case Subclass(:final field): - /// return ...; - /// case _: - /// return null; - /// } - /// ``` - - @optionalTypeArgs - TResult? whenOrNull( - TResult? Function(@JsonKey(name: 'id') int portionId, String comment)? - $default, - ) { - final _that = this; - switch (_that) { - case _PortionComment() when $default != null: - return $default(_that.portionId, _that.comment); - case _: - return null; - } - } -} - -/// @nodoc -@JsonSerializable() -class _PortionComment implements PortionComment { - _PortionComment( - {@JsonKey(name: 'id') required this.portionId, required this.comment}); - factory _PortionComment.fromJson(Map json) => - _$PortionCommentFromJson(json); - - @override - @JsonKey(name: 'id') - final int portionId; - @override - final String comment; - - /// Create a copy of PortionComment - /// with the given fields replaced by the non-null parameter values. - @override - @JsonKey(includeFromJson: false, includeToJson: false) - @pragma('vm:prefer-inline') - _$PortionCommentCopyWith<_PortionComment> get copyWith => - __$PortionCommentCopyWithImpl<_PortionComment>(this, _$identity); - - @override - Map toJson() { - return _$PortionCommentToJson( - this, - ); - } - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _PortionComment && - (identical(other.portionId, portionId) || - other.portionId == portionId) && - (identical(other.comment, comment) || other.comment == comment)); - } - - @JsonKey(includeFromJson: false, includeToJson: false) - @override - int get hashCode => Object.hash(runtimeType, portionId, comment); - - @override - String toString() { - return 'PortionComment(portionId: $portionId, comment: $comment)'; - } -} - -/// @nodoc -abstract mixin class _$PortionCommentCopyWith<$Res> - implements $PortionCommentCopyWith<$Res> { - factory _$PortionCommentCopyWith( - _PortionComment value, $Res Function(_PortionComment) _then) = - __$PortionCommentCopyWithImpl; - @override - @useResult - $Res call({@JsonKey(name: 'id') int portionId, String comment}); -} - -/// @nodoc -class __$PortionCommentCopyWithImpl<$Res> - implements _$PortionCommentCopyWith<$Res> { - __$PortionCommentCopyWithImpl(this._self, this._then); - - final _PortionComment _self; - final $Res Function(_PortionComment) _then; - - /// Create a copy of PortionComment - /// with the given fields replaced by the non-null parameter values. - @override - @pragma('vm:prefer-inline') - $Res call({ - Object? portionId = null, - Object? comment = null, - }) { - return _then(_PortionComment( - portionId: null == portionId - ? _self.portionId - : portionId // ignore: cast_nullable_to_non_nullable - as int, - comment: null == comment - ? _self.comment - : comment // ignore: cast_nullable_to_non_nullable - as String, - )); - } -} - -// dart format on diff --git a/lib/features/diary/models/consultant_review_model.g.dart b/lib/features/diary/models/consultant_review_model.g.dart deleted file mode 100644 index ad7fa537..00000000 --- a/lib/features/diary/models/consultant_review_model.g.dart +++ /dev/null @@ -1,67 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'consultant_review_model.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -_ConsultantReviewModel _$ConsultantReviewModelFromJson( - Map json) => - _ConsultantReviewModel( - planName: json['planName'] as String, - planDuration: (json['planDuration'] as num).toInt(), - currentPlanDay: (json['currentPlanDay'] as num).toInt(), - consultantComments: ConsultantComments.fromJson( - json['consultantComments'] as Map), - ); - -Map _$ConsultantReviewModelToJson( - _ConsultantReviewModel instance) => - { - 'planName': instance.planName, - 'planDuration': instance.planDuration, - 'currentPlanDay': instance.currentPlanDay, - 'consultantComments': instance.consultantComments, - }; - -_ConsultantComments _$ConsultantCommentsFromJson(Map json) => - _ConsultantComments( - dayComment: json['dayComment'] as String?, - mealsComments: (json['mealsComments'] as List) - .map((e) => MealComment.fromJson(e as Map)) - .toList(), - portionsComments: (json['portionsComments'] as List) - .map((e) => PortionComment.fromJson(e as Map)) - .toList(), - ); - -Map _$ConsultantCommentsToJson(_ConsultantComments instance) => - { - 'dayComment': instance.dayComment, - 'mealsComments': instance.mealsComments, - 'portionsComments': instance.portionsComments, - }; - -_MealComment _$MealCommentFromJson(Map json) => _MealComment( - comment: json['comment'] as String, - typeNo: (json['typeNo'] as num).toInt(), - ); - -Map _$MealCommentToJson(_MealComment instance) => - { - 'comment': instance.comment, - 'typeNo': instance.typeNo, - }; - -_PortionComment _$PortionCommentFromJson(Map json) => - _PortionComment( - portionId: (json['id'] as num).toInt(), - comment: json['comment'] as String, - ); - -Map _$PortionCommentToJson(_PortionComment instance) => - { - 'id': instance.portionId, - 'comment': instance.comment, - }; diff --git a/test/features/profile/services/profile_service_test.mocks.dart b/test/features/profile/services/profile_service_test.mocks.dart deleted file mode 100644 index 22c3af52..00000000 --- a/test/features/profile/services/profile_service_test.mocks.dart +++ /dev/null @@ -1,526 +0,0 @@ -// Mocks generated by Mockito 5.4.6 from annotations -// in eatvio_app/test/features/profile/services/profile_service_test.dart. -// Do not manually edit this file. - -// ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i6; -import 'dart:io' as _i10; - -import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart' - as _i7; -import 'package:eatvio_app/features/profile/models/chosen_diets_pagination_model.dart' - as _i4; -import 'package:eatvio_app/features/profile/models/circumference_model.dart' - as _i9; -import 'package:eatvio_app/features/profile/models/profile_model.dart' as _i2; -import 'package:eatvio_app/features/profile/models/sub_model.dart' as _i3; -import 'package:eatvio_app/features/profile/models/weight_model.dart' as _i8; -import 'package:eatvio_app/features/profile/services/profile_service.dart' - as _i5; -import 'package:mockito/mockito.dart' as _i1; -import 'package:mockito/src/dummies.dart' as _i11; - -// ignore_for_file: type=lint -// ignore_for_file: avoid_redundant_argument_values -// ignore_for_file: avoid_setters_without_getters -// ignore_for_file: comment_references -// ignore_for_file: deprecated_member_use -// ignore_for_file: deprecated_member_use_from_same_package -// ignore_for_file: implementation_imports -// ignore_for_file: invalid_use_of_visible_for_testing_member -// ignore_for_file: must_be_immutable -// ignore_for_file: prefer_const_constructors -// ignore_for_file: unnecessary_parenthesis -// ignore_for_file: camel_case_types -// ignore_for_file: subtype_of_sealed_class - -class _FakeProfileModel_0 extends _i1.SmartFake implements _i2.ProfileModel { - _FakeProfileModel_0( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -class _FakeProfileDetailsModel_1 extends _i1.SmartFake - implements _i2.ProfileDetailsModel { - _FakeProfileDetailsModel_1( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -class _FakeSubPaginationModel_2 extends _i1.SmartFake - implements _i3.SubPaginationModel { - _FakeSubPaginationModel_2( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -class _FakeChosenDietsPaginationModel_3 extends _i1.SmartFake - implements _i4.ChosenDietsPaginationModel { - _FakeChosenDietsPaginationModel_3( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -/// A class which mocks [ProfileService]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockProfileService extends _i1.Mock implements _i5.ProfileService { - @override - _i6.Future<_i2.ProfileModel> getProfileBySlug({required String? slug}) => - (super.noSuchMethod( - Invocation.method( - #getProfileBySlug, - [], - {#slug: slug}, - ), - returnValue: _i6.Future<_i2.ProfileModel>.value(_FakeProfileModel_0( - this, - Invocation.method( - #getProfileBySlug, - [], - {#slug: slug}, - ), - )), - returnValueForMissingStub: - _i6.Future<_i2.ProfileModel>.value(_FakeProfileModel_0( - this, - Invocation.method( - #getProfileBySlug, - [], - {#slug: slug}, - ), - )), - ) as _i6.Future<_i2.ProfileModel>); - - @override - _i6.Future<_i2.ProfileModel> getMyProfile() => (super.noSuchMethod( - Invocation.method( - #getMyProfile, - [], - ), - returnValue: _i6.Future<_i2.ProfileModel>.value(_FakeProfileModel_0( - this, - Invocation.method( - #getMyProfile, - [], - ), - )), - returnValueForMissingStub: - _i6.Future<_i2.ProfileModel>.value(_FakeProfileModel_0( - this, - Invocation.method( - #getMyProfile, - [], - ), - )), - ) as _i6.Future<_i2.ProfileModel>); - - @override - _i6.Future<_i2.ProfileDetailsModel> getMyProfileDetails() => - (super.noSuchMethod( - Invocation.method( - #getMyProfileDetails, - [], - ), - returnValue: _i6.Future<_i2.ProfileDetailsModel>.value( - _FakeProfileDetailsModel_1( - this, - Invocation.method( - #getMyProfileDetails, - [], - ), - )), - returnValueForMissingStub: _i6.Future<_i2.ProfileDetailsModel>.value( - _FakeProfileDetailsModel_1( - this, - Invocation.method( - #getMyProfileDetails, - [], - ), - )), - ) as _i6.Future<_i2.ProfileDetailsModel>); - - @override - _i6.Future<_i2.ProfileDetailsModel> getProfileDetailsBySlug( - {required String? slug}) => - (super.noSuchMethod( - Invocation.method( - #getProfileDetailsBySlug, - [], - {#slug: slug}, - ), - returnValue: _i6.Future<_i2.ProfileDetailsModel>.value( - _FakeProfileDetailsModel_1( - this, - Invocation.method( - #getProfileDetailsBySlug, - [], - {#slug: slug}, - ), - )), - returnValueForMissingStub: _i6.Future<_i2.ProfileDetailsModel>.value( - _FakeProfileDetailsModel_1( - this, - Invocation.method( - #getProfileDetailsBySlug, - [], - {#slug: slug}, - ), - )), - ) as _i6.Future<_i2.ProfileDetailsModel>); - - @override - _i6.Future addCircumference({ - required int? chestCircumference, - required int? waistCircumference, - required int? hipCircumference, - }) => - (super.noSuchMethod( - Invocation.method( - #addCircumference, - [], - { - #chestCircumference: chestCircumference, - #waistCircumference: waistCircumference, - #hipCircumference: hipCircumference, - }, - ), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) as _i6.Future); - - @override - _i6.Future addWeight({required double? weight}) => (super.noSuchMethod( - Invocation.method( - #addWeight, - [], - {#weight: weight}, - ), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) as _i6.Future); - - @override - _i6.Future> getBlogPostByAuthorSlug( - {required String? slug}) => - (super.noSuchMethod( - Invocation.method( - #getBlogPostByAuthorSlug, - [], - {#slug: slug}, - ), - returnValue: _i6.Future>.value( - <_i7.FeedBlogPostModel>[]), - returnValueForMissingStub: - _i6.Future>.value( - <_i7.FeedBlogPostModel>[]), - ) as _i6.Future>); - - @override - _i6.Future> getWeight({required String? period}) => - (super.noSuchMethod( - Invocation.method( - #getWeight, - [], - {#period: period}, - ), - returnValue: - _i6.Future>.value(<_i8.WeightModel>[]), - returnValueForMissingStub: - _i6.Future>.value(<_i8.WeightModel>[]), - ) as _i6.Future>); - - @override - _i6.Future> getWeightBySlug({ - required String? period, - required String? slug, - }) => - (super.noSuchMethod( - Invocation.method( - #getWeightBySlug, - [], - { - #period: period, - #slug: slug, - }, - ), - returnValue: - _i6.Future>.value(<_i8.WeightModel>[]), - returnValueForMissingStub: - _i6.Future>.value(<_i8.WeightModel>[]), - ) as _i6.Future>); - - @override - _i6.Future> getCircumferences( - {required String? period}) => - (super.noSuchMethod( - Invocation.method( - #getCircumferences, - [], - {#period: period}, - ), - returnValue: _i6.Future>.value( - <_i9.CircumferenceModel>[]), - returnValueForMissingStub: - _i6.Future>.value( - <_i9.CircumferenceModel>[]), - ) as _i6.Future>); - - @override - _i6.Future> getCircumferencesBySlug({ - required String? period, - required String? slug, - }) => - (super.noSuchMethod( - Invocation.method( - #getCircumferencesBySlug, - [], - { - #period: period, - #slug: slug, - }, - ), - returnValue: _i6.Future>.value( - <_i9.CircumferenceModel>[]), - returnValueForMissingStub: - _i6.Future>.value( - <_i9.CircumferenceModel>[]), - ) as _i6.Future>); - - @override - _i6.Future<_i3.SubPaginationModel> getSubscribers({ - required String? slug, - String? query, - }) => - (super.noSuchMethod( - Invocation.method( - #getSubscribers, - [], - { - #slug: slug, - #query: query, - }, - ), - returnValue: - _i6.Future<_i3.SubPaginationModel>.value(_FakeSubPaginationModel_2( - this, - Invocation.method( - #getSubscribers, - [], - { - #slug: slug, - #query: query, - }, - ), - )), - returnValueForMissingStub: - _i6.Future<_i3.SubPaginationModel>.value(_FakeSubPaginationModel_2( - this, - Invocation.method( - #getSubscribers, - [], - { - #slug: slug, - #query: query, - }, - ), - )), - ) as _i6.Future<_i3.SubPaginationModel>); - - @override - _i6.Future<_i3.SubPaginationModel> getSubscriptions({ - required String? slug, - String? query, - }) => - (super.noSuchMethod( - Invocation.method( - #getSubscriptions, - [], - { - #slug: slug, - #query: query, - }, - ), - returnValue: - _i6.Future<_i3.SubPaginationModel>.value(_FakeSubPaginationModel_2( - this, - Invocation.method( - #getSubscriptions, - [], - { - #slug: slug, - #query: query, - }, - ), - )), - returnValueForMissingStub: - _i6.Future<_i3.SubPaginationModel>.value(_FakeSubPaginationModel_2( - this, - Invocation.method( - #getSubscriptions, - [], - { - #slug: slug, - #query: query, - }, - ), - )), - ) as _i6.Future<_i3.SubPaginationModel>); - - @override - _i6.Future subscribe({required String? slug}) => (super.noSuchMethod( - Invocation.method( - #subscribe, - [], - {#slug: slug}, - ), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) as _i6.Future); - - @override - _i6.Future unsubscribe({required String? slug}) => (super.noSuchMethod( - Invocation.method( - #unsubscribe, - [], - {#slug: slug}, - ), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) as _i6.Future); - - @override - _i6.Future uploadPhoto({required _i10.File? photo}) => - (super.noSuchMethod( - Invocation.method( - #uploadPhoto, - [], - {#photo: photo}, - ), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) as _i6.Future); - - @override - _i6.Future deletePhoto() => (super.noSuchMethod( - Invocation.method( - #deletePhoto, - [], - ), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) as _i6.Future); - - @override - _i6.Future<_i4.ChosenDietsPaginationModel> getChosenDiets() => - (super.noSuchMethod( - Invocation.method( - #getChosenDiets, - [], - ), - returnValue: _i6.Future<_i4.ChosenDietsPaginationModel>.value( - _FakeChosenDietsPaginationModel_3( - this, - Invocation.method( - #getChosenDiets, - [], - ), - )), - returnValueForMissingStub: - _i6.Future<_i4.ChosenDietsPaginationModel>.value( - _FakeChosenDietsPaginationModel_3( - this, - Invocation.method( - #getChosenDiets, - [], - ), - )), - ) as _i6.Future<_i4.ChosenDietsPaginationModel>); - - @override - _i6.Future getPassKey({required String? json}) => (super.noSuchMethod( - Invocation.method( - #getPassKey, - [], - {#json: json}, - ), - returnValue: _i6.Future.value(_i11.dummyValue( - this, - Invocation.method( - #getPassKey, - [], - {#json: json}, - ), - )), - returnValueForMissingStub: - _i6.Future.value(_i11.dummyValue( - this, - Invocation.method( - #getPassKey, - [], - {#json: json}, - ), - )), - ) as _i6.Future); - - @override - _i6.Future getMyPhone() => (super.noSuchMethod( - Invocation.method( - #getMyPhone, - [], - ), - returnValue: _i6.Future.value(_i11.dummyValue( - this, - Invocation.method( - #getMyPhone, - [], - ), - )), - returnValueForMissingStub: - _i6.Future.value(_i11.dummyValue( - this, - Invocation.method( - #getMyPhone, - [], - ), - )), - ) as _i6.Future); - - @override - _i6.Future sendEmailCode() => (super.noSuchMethod( - Invocation.method( - #sendEmailCode, - [], - ), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) as _i6.Future); - - @override - _i6.Future tryVerifyEmailByCode({required String? code}) => - (super.noSuchMethod( - Invocation.method( - #tryVerifyEmailByCode, - [], - {#code: code}, - ), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) as _i6.Future); -} From 74df7d82db0dd9f8af2f5ad6a28cc65b9a0c9b54 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Mon, 14 Jul 2025 14:06:07 +0300 Subject: [PATCH 020/134] feat(notifications): Issue-562 --- lib/core/app_router.dart | 18 +++---------- lib/core/const.dart | 1 - lib/core/firebase_api.dart | 25 +++++++++++++++++++ .../auth/presentation/pages/auth_screen.dart | 2 ++ lib/features/chat/services/chat_service.dart | 4 +-- .../registration_cubit.dart | 3 +++ lib/main.dart | 3 ++- 7 files changed, 38 insertions(+), 18 deletions(-) diff --git a/lib/core/app_router.dart b/lib/core/app_router.dart index 9dc8612f..ee0278c8 100644 --- a/lib/core/app_router.dart +++ b/lib/core/app_router.dart @@ -121,7 +121,9 @@ class AppRouter { GoRoute( path: 'forget-password-screen', name: 'forgetPasswordScreen', - builder: (context, state) => ForgetPasswordScreen(isAuth: state.extra as bool,), + builder: (context, state) => ForgetPasswordScreen( + isAuth: state.extra as bool, + ), ), GoRoute( path: 'scan-qr-code-screen', @@ -205,21 +207,9 @@ class AppRouter { builder: (context, state) => const WelcomeScreen(), redirect: (context, state) async { try { - if (await TokenStorage.haveTokenInStorage()) { - final firebaseApi = sl(); - final fbToken = await firebaseApi.getToken(); - final deviceInfo = DeviceInfoPlugin(); - await sl().post( - '/api/profile/notification/token/register', - data: { - 'platform': Platform.isIOS ? 'ios' : 'android', - 'hw_id': Platform.isIOS ? (await deviceInfo.iosInfo).identifierForVendor : (await deviceInfo.androidInfo).id, - 'token': fbToken, - }, - ); - } final sharedPreferences = await SharedPreferences.getInstance(); final welcomeScreenWasShowed = sharedPreferences.getBool('welcomeScreenWasShowed') ?? false; + await sl().registerPushToken(); if (welcomeScreenWasShowed == false) { return null; } else if (await TokenStorage.haveTokenInStorage()) { diff --git a/lib/core/const.dart b/lib/core/const.dart index 31c47e9c..618a9866 100644 --- a/lib/core/const.dart +++ b/lib/core/const.dart @@ -1,5 +1,4 @@ String baseUrl = 'https://eatvio.ru'; -// String baseUrl = 'https://dev2.eatvio.com'; const List durationValues = [7, 14, 21]; diff --git a/lib/core/firebase_api.dart b/lib/core/firebase_api.dart index fbf8e5c1..572b56f6 100644 --- a/lib/core/firebase_api.dart +++ b/lib/core/firebase_api.dart @@ -1,3 +1,10 @@ +import 'dart:developer'; +import 'dart:io'; + +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'; class FirebaseApi { @@ -7,4 +14,22 @@ class FirebaseApi { await _firebaseMessaging.requestPermission(); return await _firebaseMessaging.getToken(); } + + Future registerPushToken() async { + if (await TokenStorage.haveTokenInStorage()) { + try { + final deviceInfo = DeviceInfoPlugin(); + await sl().post( + '/api/profile/notification/token/register', + 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); + } + } + } } diff --git a/lib/features/auth/presentation/pages/auth_screen.dart b/lib/features/auth/presentation/pages/auth_screen.dart index 10fb4172..f5fd7a34 100644 --- a/lib/features/auth/presentation/pages/auth_screen.dart +++ b/lib/features/auth/presentation/pages/auth_screen.dart @@ -1,6 +1,7 @@ import 'dart:async'; 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/theme/theme_helper.dart'; import 'package:eatvio_app/core/utils/image_constant.dart'; @@ -66,6 +67,7 @@ class _AuthScreenState extends State { await sl().connectToWebSocket(); await sl().getNotifications(); await sl().connectToWebSocket(); + await sl().registerPushToken(); showSuccessToast('Успешный вход'); } catch (e) { rethrow; diff --git a/lib/features/chat/services/chat_service.dart b/lib/features/chat/services/chat_service.dart index dee63eab..cceaa9d0 100644 --- a/lib/features/chat/services/chat_service.dart +++ b/lib/features/chat/services/chat_service.dart @@ -16,7 +16,7 @@ abstract class ChatService { @GET('/api/chat/index') Future getChatRooms({ @Query('page') required int page, - @Query('find') String? query, + @Query('filters[data][\$conversation_name]') String? query, }); @GET('/api/chat/show/{uuid}') @@ -82,7 +82,7 @@ abstract class ChatService { @Body() required PlanComplainModel body, }); - @GET('/api/v2/chat/get_by_user/{slug}') + @GET('/api/chat/get_by_user/{slug}') Future getChatByUserSlug({ @Path() required String slug, }); diff --git a/lib/features/registration/presentation/bloc/registration_cubit/registration_cubit.dart b/lib/features/registration/presentation/bloc/registration_cubit/registration_cubit.dart index 83592c3b..fa2948bc 100644 --- a/lib/features/registration/presentation/bloc/registration_cubit/registration_cubit.dart +++ b/lib/features/registration/presentation/bloc/registration_cubit/registration_cubit.dart @@ -1,6 +1,8 @@ import 'dart:developer'; import 'package:eatvio_app/core/app_router.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'; import 'package:eatvio_app/features/registration/models/user_registration_model.dart'; @@ -59,6 +61,7 @@ class RegistrationCubit extends Cubit { ); await TokenStorage.saveTokenInStorage(token); AppRouter.navigatorKey.currentContext?.pushNamed('verifyEmailScreen'); + await sl().registerPushToken(); } catch (e, st) { log(e.toString(), stackTrace: st); } diff --git a/lib/main.dart b/lib/main.dart index 005afaac..3fd39e7f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -11,6 +11,7 @@ import 'package:eatvio_app/features/internet_connection_checker/internet_connect 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/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; @@ -22,8 +23,8 @@ import 'package:sentry_flutter/sentry_flutter.dart'; Future main() async { WidgetsBinding widgetsBinding = SentryWidgetsFlutterBinding.ensureInitialized(); FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); - await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); + await FirebaseMessaging.instance.requestPermission(); await di.init(); await SentryFlutter.init( From 2c38be9fa4d7fc6741645dbd4cf47ceac035d0ca Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Mon, 14 Jul 2025 14:49:33 +0300 Subject: [PATCH 021/134] fix(diary): current plan day --- .../diary/presentation/bloc/diary_cubit.dart | 29 +++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/lib/features/diary/presentation/bloc/diary_cubit.dart b/lib/features/diary/presentation/bloc/diary_cubit.dart index c973bcaa..45394cbe 100644 --- a/lib/features/diary/presentation/bloc/diary_cubit.dart +++ b/lib/features/diary/presentation/bloc/diary_cubit.dart @@ -183,7 +183,7 @@ class DiaryCubit extends Cubit { final chosenPlanModel = await chatRepository.getChosenPlan(id: diaryDay.chosenPlanId!); planDuration = chosenPlanModel.plan.duration; planName = chosenPlanModel.plan.name; - currentPlanDay = currentDay ?? DateTime.now().difference(chosenPlanModel.startDate).inDays + 1; + currentPlanDay = consultantReview.currentPlanDay; } final nutrition = await getNutrition(diaryDay.id); @@ -194,20 +194,19 @@ class DiaryCubit extends Cubit { diaryDay.typeOfDiary.diaryState == DiaryStateEnum.reviewedByUser; emit( DiaryState.loaded( - diary: diaryDay, - nutrition: nutrition, - selectedDay: selectedDay, - selectedMeal: selectedMeal, - isConsultantView: this.diaryId != null, - consultantReview: consultantReview, - isDiaryFinished: isDiaryFinished, - isDiaryCommented: isDiaryCommented, - chosenPlanCurrentDay: currentPlanDay, - chosenPlanDuration: planDuration, - chosenPlanName: planName, - isMoodEnabled: isMoodEnabled, - isWakeUpAndRetireEnabled: isWakeUpAndRetireTimeEnabled - ), + diary: diaryDay, + nutrition: nutrition, + selectedDay: selectedDay, + selectedMeal: selectedMeal, + isConsultantView: this.diaryId != null, + consultantReview: consultantReview, + isDiaryFinished: isDiaryFinished, + isDiaryCommented: isDiaryCommented, + chosenPlanCurrentDay: currentPlanDay, + chosenPlanDuration: planDuration, + chosenPlanName: planName, + isMoodEnabled: isMoodEnabled, + isWakeUpAndRetireEnabled: isWakeUpAndRetireTimeEnabled), ); } catch (e, st) { log(e.toString(), stackTrace: st); From 6bcd540d546d37e0a9d7e2d90ef2be545edcbd61 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Mon, 14 Jul 2025 15:16:11 +0300 Subject: [PATCH 022/134] fix: analyzer --- lib/core/app_router.dart | 2 -- lib/features/chat/models/chat_room_model.dart | 6 +----- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/lib/core/app_router.dart b/lib/core/app_router.dart index ee0278c8..0654a961 100644 --- a/lib/core/app_router.dart +++ b/lib/core/app_router.dart @@ -1,5 +1,3 @@ -import 'dart:io'; -import 'package:device_info_plus/device_info_plus.dart'; import 'package:dio/dio.dart'; import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/firebase_api.dart'; diff --git a/lib/features/chat/models/chat_room_model.dart b/lib/features/chat/models/chat_room_model.dart index dc091ea6..c5ee559f 100644 --- a/lib/features/chat/models/chat_room_model.dart +++ b/lib/features/chat/models/chat_room_model.dart @@ -9,7 +9,7 @@ part 'chat_room_model.g.dart'; @freezed abstract class ChatRoomModel with _$ChatRoomModel { const factory ChatRoomModel({ - required String id, + required String id, @JsonKey(name: 'chat_room_setting') required ChatRoomSettingsModel chatRoomSettings, required Users users, @JsonKey(name: 'chosen_plan_id') required int? chosenPlanId, @@ -22,13 +22,9 @@ abstract class ChatRoomModel with _$ChatRoomModel { @freezed abstract class Users with _$Users { const factory Users({ - @JsonKey(name: 'from_chat_user') required ChatUserModel sender, @JsonKey(name: 'to_chat_user') required ChatUserModel receiver, - }) = _Users; factory Users.fromJson(Map json) => _$UsersFromJson(json); } - - From 452c603b95cbebed0eeccd83000175019ab4dacf Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Mon, 14 Jul 2025 15:59:43 +0300 Subject: [PATCH 023/134] feat: diary comments view --- lib/features/diary/repositories/diary_repository.dart | 1 + lib/features/diary/services/diary_service.dart | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/lib/features/diary/repositories/diary_repository.dart b/lib/features/diary/repositories/diary_repository.dart index e807e3d0..14a9576b 100644 --- a/lib/features/diary/repositories/diary_repository.dart +++ b/lib/features/diary/repositories/diary_repository.dart @@ -292,6 +292,7 @@ class DiaryRepository { required int diaryId, }) async { try { + await diaryService.viewCommentOfDiaryDayByConsultant(diaryId: diaryId); return await diaryService.getDiaryComments(planId: planId, diaryId: diaryId); } catch (e) { rethrow; diff --git a/lib/features/diary/services/diary_service.dart b/lib/features/diary/services/diary_service.dart index bb6672d3..d9967200 100644 --- a/lib/features/diary/services/diary_service.dart +++ b/lib/features/diary/services/diary_service.dart @@ -150,4 +150,9 @@ abstract class DiaryService { @Path() required int planId, @Path() required int diaryId, }); + + @POST('/api/diary/{diaryId}/consultant/comment/view') + Future viewCommentOfDiaryDayByConsultant({ + @Path() required int diaryId, + }); } From 09c53a64928605d67e6b5886c93c56c88c20bb01 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Mon, 14 Jul 2025 17:08:53 +0300 Subject: [PATCH 024/134] feat(notificaion): add notificaion for last day --- lib/features/chat/services/chat_service.dart | 10 ++++---- .../models/notification_model.dart | 24 +++++++++++++++++-- .../notifications_cubit.dart | 1 + .../widgets/notification_widget.dart | 1 + .../service/notifications_service.dart | 5 ++++ 5 files changed, 34 insertions(+), 7 deletions(-) diff --git a/lib/features/chat/services/chat_service.dart b/lib/features/chat/services/chat_service.dart index cceaa9d0..89d65749 100644 --- a/lib/features/chat/services/chat_service.dart +++ b/lib/features/chat/services/chat_service.dart @@ -13,18 +13,18 @@ part 'chat_service.g.dart'; abstract class ChatService { factory ChatService(Dio dio, {String baseUrl}) = _ChatService; - @GET('/api/chat/index') + @GET('/api/v2/chat/index') Future getChatRooms({ @Query('page') required int page, @Query('filters[data][\$conversation_name]') String? query, }); - @GET('/api/chat/show/{uuid}') + @GET('/api/v2/chat/show/{uuid}') Future getChatRoom({ @Path() required String uuid, }); - @POST('/api/chat/message/send/{slug}') + @POST('/api/v2/chat/message/send/{slug}') @MultiPart() Future sendMessage({ @Header('X-Socket-ID') required String socketId, @@ -33,13 +33,13 @@ abstract class ChatService { @Part(name: 'media_data[]', contentType: 'multipart/form-data') List? mediaData, }); - @GET('/api/chat/show/{id}/messages?page={page}') + @GET('/api/v2/chat/show/{id}/messages?page={page}') Future getChatMessages({ @Path() required String id, @Path() required int page, }); - @GET('/api/chat/get_by_user/{user}') + @GET('/api/v2/chat/get_by_user/{user}') Future getChatRoomIdByUserSlug({ @Path() required String user, }); diff --git a/lib/features/notifications/models/notification_model.dart b/lib/features/notifications/models/notification_model.dart index e8489a5a..827a0904 100644 --- a/lib/features/notifications/models/notification_model.dart +++ b/lib/features/notifications/models/notification_model.dart @@ -106,8 +106,7 @@ class DiaryReminderNotification implements BaseNotificationModel { final NotificationType type; @override final bool isRead; - - + DiaryReminderNotification({required this.type, required this.isRead}); factory DiaryReminderNotification.fromJson(Map json, NotificationType type, {bool? isRead}) { @@ -297,6 +296,25 @@ class ChosenPlanFinishedToConsultantNotification implements BaseNotificationMode } } +class ChosenPlanLastDayFinishedForUser implements BaseNotificationModel { + @override + final NotificationType type; + @override + final bool isRead; + + ChosenPlanLastDayFinishedForUser({ + required this.type, + required this.isRead, + }); + + factory ChosenPlanLastDayFinishedForUser.fromJson(Map json, NotificationType type, {bool? isRead}) { + return ChosenPlanLastDayFinishedForUser( + type: type, + isRead: isRead ?? false, + ); + } +} + class UnknownNotification implements BaseNotificationModel { @override final NotificationType type; @@ -323,6 +341,7 @@ enum NotificationType { chosenPlanTomorrowReminderToConsultantNotification, planSubscriptionPaidToAuthor, chosenPlanFinishedToConsultant, + chosenPlanLastDayFinishedForUser, unknown; static NotificationType getTypeFromString(String type) => switch (type) { @@ -339,6 +358,7 @@ enum NotificationType { 'App\\Notifications\\Plan\\ChosenPlanTomorrowReminderToConsultantNotification' => NotificationType.chosenPlanTomorrowReminderToConsultantNotification, 'App\\Notifications\\Plan\\PlanSubscriptionPaidToAuthor' => NotificationType.planSubscriptionPaidToAuthor, 'App\\Notifications\\Plan\\ChosenPlanFinishedToConsultant' => NotificationType.chosenPlanFinishedToConsultant, + 'App\\Notifications\\ChosenPlan\\LastDayFinishedForUser' => NotificationType.chosenPlanLastDayFinishedForUser, _ => NotificationType.unknown, }; } diff --git a/lib/features/notifications/presentation/notifications_cubit/notifications_cubit.dart b/lib/features/notifications/presentation/notifications_cubit/notifications_cubit.dart index 16cf1f40..61eba9a7 100644 --- a/lib/features/notifications/presentation/notifications_cubit/notifications_cubit.dart +++ b/lib/features/notifications/presentation/notifications_cubit/notifications_cubit.dart @@ -42,6 +42,7 @@ class NotificationsCubit extends Cubit { ChosenPlanTomorrowReminderToConsultantNotification.fromJson(notificationJson, type), NotificationType.planSubscriptionPaidToAuthor => PlanSubscriptionPaidToAuthorNotification.fromJson(notificationJson, type), NotificationType.chosenPlanFinishedToConsultant => ChosenPlanFinishedToConsultantNotification.fromJson(notificationJson, type), + NotificationType.chosenPlanLastDayFinishedForUser => ChosenPlanLastDayFinishedForUser.fromJson(notificationJson, type), NotificationType.unknown => UnknownNotification(type: type), }; } catch (e) { diff --git a/lib/features/notifications/presentation/widgets/notification_widget.dart b/lib/features/notifications/presentation/widgets/notification_widget.dart index 83157593..66b6cf18 100644 --- a/lib/features/notifications/presentation/widgets/notification_widget.dart +++ b/lib/features/notifications/presentation/widgets/notification_widget.dart @@ -53,6 +53,7 @@ class NotificationWidget extends StatelessWidget { ChosenPlanFinishedToConsultantNotification() => _ChosenPlanFinishedToConsultantWidget( notification: notification as ChosenPlanFinishedToConsultantNotification, ), + ChosenPlanLastDayFinishedForUser() => Text((notification as ChosenPlanLastDayFinishedForUser).type.toString()), UnknownNotification() => const SizedBox.shrink(), }; } diff --git a/lib/features/notifications/service/notifications_service.dart b/lib/features/notifications/service/notifications_service.dart index 860aab55..ec2aad74 100644 --- a/lib/features/notifications/service/notifications_service.dart +++ b/lib/features/notifications/service/notifications_service.dart @@ -40,6 +40,11 @@ class NotificationsService { PlanSubscriptionPaidToAuthorNotification.fromJson(notificationJson['data'], type, isRead: readDate != null), NotificationType.chosenPlanFinishedToConsultant => ChosenPlanFinishedToConsultantNotification.fromJson(notificationJson['data'], type, isRead: readDate != null), + NotificationType.chosenPlanLastDayFinishedForUser => ChosenPlanLastDayFinishedForUser.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + ), NotificationType.unknown => UnknownNotification(type: type), }; } catch (e) { From 3e2aff3ad4d4beb1e384b15cd82ac1daa987ba66 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Tue, 15 Jul 2025 13:47:00 +0300 Subject: [PATCH 025/134] fix(chat): model fields --- ios/Runner.xcodeproj/project.pbxproj | 6 +++--- ios/Runner/Info.plist | 2 +- lib/features/chat/models/chat_room_settings_model.dart | 2 +- .../presentation/bloc/chat_room_cubit/chat_room_cubit.dart | 1 + lib/features/chat/repositories/chat_repository.dart | 3 ++- pubspec.yaml | 2 +- 6 files changed, 9 insertions(+), 7 deletions(-) diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index d2269b71..dc837658 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 = 52; + CURRENT_PROJECT_VERSION = 53; 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 = 52; + CURRENT_PROJECT_VERSION = 53; 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 = 52; + CURRENT_PROJECT_VERSION = 53; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 6de3b03b..fcaddf14 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -21,7 +21,7 @@ CFBundleSignature ???? CFBundleVersion - 52 + 53 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/lib/features/chat/models/chat_room_settings_model.dart b/lib/features/chat/models/chat_room_settings_model.dart index fa332843..737d0c82 100644 --- a/lib/features/chat/models/chat_room_settings_model.dart +++ b/lib/features/chat/models/chat_room_settings_model.dart @@ -6,7 +6,7 @@ part 'chat_room_settings_model.g.dart'; @freezed abstract class ChatRoomSettingsModel with _$ChatRoomSettingsModel { const factory ChatRoomSettingsModel({ - required String name, + @JsonKey(name: 'room_name') required String name, required String state, }) = _ChatRoomSettingsModel; diff --git a/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart b/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart index f4c95d12..c4d8ed45 100644 --- a/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart +++ b/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart @@ -223,6 +223,7 @@ class ChatRoomCubit extends Cubit { files: compressedFiles, slug: userSlug, ); + sl().getChatRooms(); return true; } catch (e, st) { log(e.toString(), stackTrace: st); diff --git a/lib/features/chat/repositories/chat_repository.dart b/lib/features/chat/repositories/chat_repository.dart index d682c9a0..b1961b2a 100644 --- a/lib/features/chat/repositories/chat_repository.dart +++ b/lib/features/chat/repositories/chat_repository.dart @@ -63,7 +63,8 @@ class ChatRepository { Future getChatRoom(String uuid) async { try { return (await chatService.getChatRoom(uuid: uuid)); - } catch (e) { + } catch (e, st) { + log(e.toString(), stackTrace: st); rethrow; } } diff --git a/pubspec.yaml b/pubspec.yaml index 74b00964..4223ded7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: eatvio_app description: A new Flutter project. -version: 1.2.2+52 +version: 1.2.4+54 publish_to: 'none' environment: From 46f11a8e786521a9a5f1f3b0a5d17a0b19998dd7 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Wed, 16 Jul 2025 12:19:29 +0300 Subject: [PATCH 026/134] fix(chat): photo view --- .../chat/presentation/pages/photo_view_screen.dart | 14 ++++++-------- .../presentation/widgets/chat_bubble_widget.dart | 2 +- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/lib/features/chat/presentation/pages/photo_view_screen.dart b/lib/features/chat/presentation/pages/photo_view_screen.dart index 260c051d..886b815a 100644 --- a/lib/features/chat/presentation/pages/photo_view_screen.dart +++ b/lib/features/chat/presentation/pages/photo_view_screen.dart @@ -11,7 +11,7 @@ import 'package:photo_view/photo_view_gallery.dart'; import 'package:url_launcher/url_launcher.dart'; class PhotoViewScreen extends StatefulWidget { - final List imageUrls; + final List imageUrls; final int index; final bool isLocal; @@ -50,14 +50,12 @@ class _PhotoViewScreenState extends State { ), scrollPhysics: const BouncingScrollPhysics(), builder: (BuildContext context, int index) { + final imageProvider = widget.isLocal + ? FileImage(widget.imageUrls[index]) as ImageProvider + : CachedNetworkImageProvider(widget.imageUrls[index]) as ImageProvider; + return PhotoViewGalleryPageOptions( - imageProvider: widget.isLocal - ? FileImage( - File(widget.imageUrls[index]), - ) as ImageProvider - : CachedNetworkImageProvider( - widget.imageUrls[index], - ), + imageProvider: imageProvider, initialScale: PhotoViewComputedScale.contained, minScale: PhotoViewComputedScale.contained, heroAttributes: PhotoViewHeroAttributes( diff --git a/lib/features/chat/presentation/widgets/chat_bubble_widget.dart b/lib/features/chat/presentation/widgets/chat_bubble_widget.dart index d2f5f1bd..60a31c44 100644 --- a/lib/features/chat/presentation/widgets/chat_bubble_widget.dart +++ b/lib/features/chat/presentation/widgets/chat_bubble_widget.dart @@ -110,7 +110,7 @@ class ChatBubbleWidget extends StatelessWidget { onTap: () { context.pushTransparentRoute( PhotoViewScreen( - imageUrls: mediaData.map((e) => '$e').toList(), + imageUrls: mediaData, index: index, isLocal: mediaData[index] is File ? true : false, ), From 773359e90019c76fdff4824d18e3b5be7656f927 Mon Sep 17 00:00:00 2001 From: Username1548 Date: Wed, 16 Jul 2025 19:30:30 +0700 Subject: [PATCH 027/134] feat(setting): added account deleting --- lib/core/app_router.dart | 2 +- .../repositories/profile_repository.dart | 8 ++ .../profile/services/profile_service.dart | 3 + .../bloc/settings_cubit/settings_cubit.dart | 9 ++ .../presentation/pages/settings_screen.dart | 92 ++++++++++++++++++- lib/generated/intl/messages_en.dart | 13 +++ lib/generated/l10n.dart | 45 +++++++++ lib/l10n/intl_en.arb | 7 +- 8 files changed, 174 insertions(+), 5 deletions(-) diff --git a/lib/core/app_router.dart b/lib/core/app_router.dart index 9dc8612f..c5d173d2 100644 --- a/lib/core/app_router.dart +++ b/lib/core/app_router.dart @@ -228,7 +228,7 @@ class AppRouter { return '/auth-screen'; } } catch (e) { - rethrow; + return '/feed-screen'; } }, ), diff --git a/lib/features/profile/repositories/profile_repository.dart b/lib/features/profile/repositories/profile_repository.dart index 75879d15..05399457 100644 --- a/lib/features/profile/repositories/profile_repository.dart +++ b/lib/features/profile/repositories/profile_repository.dart @@ -203,4 +203,12 @@ class ProfileRepository { return false; } } + + Future deleteAccount() async { + try { + await profileService.deleteAccount(); + } catch (e) { + rethrow; + } + } } diff --git a/lib/features/profile/services/profile_service.dart b/lib/features/profile/services/profile_service.dart index 06af37b9..a1208526 100644 --- a/lib/features/profile/services/profile_service.dart +++ b/lib/features/profile/services/profile_service.dart @@ -119,4 +119,7 @@ abstract class ProfileService { Future tryVerifyEmailByCode({ @Body() required String code, }); + + @DELETE('/api/profile/delete_my_account') + Future deleteAccount(); } 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 d3995d6a..3cb047f6 100644 --- a/lib/features/settings/presentation/bloc/settings_cubit/settings_cubit.dart +++ b/lib/features/settings/presentation/bloc/settings_cubit/settings_cubit.dart @@ -67,4 +67,13 @@ class SettingsCubit extends Cubit { await init(); } } + + Future deleteAccount() async { + try { + await profileRepository.deleteAccount(); + return true; + } catch (e) { + return false; + } + } } diff --git a/lib/features/settings/presentation/pages/settings_screen.dart b/lib/features/settings/presentation/pages/settings_screen.dart index 67460955..af99f901 100644 --- a/lib/features/settings/presentation/pages/settings_screen.dart +++ b/lib/features/settings/presentation/pages/settings_screen.dart @@ -1,12 +1,19 @@ +import 'package:eatvio_app/core/app_router.dart'; import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/theme/custom_text_style.dart'; +import 'package:eatvio_app/core/widgets/bottom_sheet_handler_bar.dart'; +import 'package:eatvio_app/core/widgets/bottom_sheet_with_blur.dart'; +import 'package:eatvio_app/core/widgets/custom_outlined_button.dart'; import 'package:eatvio_app/core/widgets/data_error_widget.dart'; import 'package:eatvio_app/core/widgets/loader.dart'; +import 'package:eatvio_app/core/widgets/show_error_dialog.dart'; import 'package:eatvio_app/features/consultant/become_consultant/models/consultation_request_model.dart'; import 'package:eatvio_app/features/settings/entities/settings_entity.dart'; import 'package:eatvio_app/features/settings/presentation/bloc/settings_cubit/settings_cubit.dart'; import 'package:eatvio_app/features/settings/presentation/bloc/settings_cubit/settings_state.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -60,7 +67,7 @@ class _SettingsScreenState extends State { _aboutEatvio(state), const SizedBox(height: 12), _becomeConsultantButton(state), - _deleteAccountButton(state), + _deleteAccountButton(state, settingsCubit), ], ), ), @@ -428,11 +435,20 @@ class _SettingsScreenState extends State { return const SizedBox.shrink(); } - Padding _deleteAccountButton(SettingsStateLoaded state) { + Padding _deleteAccountButton(SettingsStateLoaded state, SettingsCubit settingsCubit) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: InkWell( - onTap: () {}, + onTap: () async { + showBottomSheetWithBlur( + userRootNavigator: true, + context: context, + height: null, + child: DeleteAccountBottomSheet( + cubit: settingsCubit, + ), + ); + }, borderRadius: BorderRadius.circular(20), child: Ink( height: 52, @@ -495,3 +511,73 @@ class _SettingsScreenState extends State { ); } } + +class DeleteAccountBottomSheet extends StatefulWidget { + const DeleteAccountBottomSheet({super.key, required this.cubit}); + final SettingsCubit cubit; + + @override + State createState() => _DeleteAccountBottomSheet(); +} + +class _DeleteAccountBottomSheet extends State { + bool? isFailure; + @override + Widget build(BuildContext context) { + final s = S.of(context); + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox( + height: 8, + ), + const BottomSheetHandlerBar(), + const SizedBox( + height: 20, + ), + Text(s.accountDeleting, style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)), + const SizedBox( + height: 16, + ), + Text( + s.youShureYouWantToDeleteAccount, + style: TextStyles.textSMedium.copyWith(color: appTheme.black), + textAlign: TextAlign.center, + ), + const SizedBox( + height: 8, + ), + Text( + s.itCantBeRecovered, + style: TextStyles.textSMedium.copyWith(color: appTheme.black), + textAlign: TextAlign.center, + ), + const SizedBox( + height: 8, + ), + CustomOutlinedButton( + text: s.delete, + height: 52, + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), + buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.blueA400)), + onPressed: () async { + final result = await widget.cubit.deleteAccount(); + if (result == true) { + AppRouter.navigatorKey.currentContext?.go('/auth-screen'); + return; + } + AppRouter.navigatorKey.currentContext?.pop(); + if (!context.mounted) return; + showErrorDialog(context, description: s.cannotDeleteAccount); + }, + ), + const SizedBox( + height: 40, + ), + ], + ), + ); + } +} diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart index 73db6f27..360b4288 100644 --- a/lib/generated/intl/messages_en.dart +++ b/lib/generated/intl/messages_en.dart @@ -51,6 +51,9 @@ class MessageLookup extends MessageLookupByLibrary { "about_diet": MessageLookupByLibrary.simpleMessage("Об этой диете"), "about_plan": MessageLookupByLibrary.simpleMessage("Об этом плане"), "about_recipe": MessageLookupByLibrary.simpleMessage("Об этом рецепте"), + "accountDeleting": MessageLookupByLibrary.simpleMessage( + "Удаление аккаунта", + ), "active": MessageLookupByLibrary.simpleMessage("Активен"), "active_consultations_count": MessageLookupByLibrary.simpleMessage( "Кол-во активных консультаций", @@ -124,6 +127,9 @@ class MessageLookup extends MessageLookupByLibrary { ), "cancel_diet": MessageLookupByLibrary.simpleMessage("Отменить диету?"), "cancel_order": MessageLookupByLibrary.simpleMessage("Отменить заказ"), + "cannotDeleteAccount": MessageLookupByLibrary.simpleMessage( + "Не получилось удалить ваш аккаунт", + ), "carb_type": MessageLookupByLibrary.simpleMessage("Мучное"), "carb_type_desc": MessageLookupByLibrary.simpleMessage( "Если вы любите мучные продукты, но хотите придерживаться диеты, вам подходит диета с добавленим мучных блюд на основе цельнозерновых вариантов. В них больше сложных углеводов и клетчатки, что способствует длительному чувству сытости и поддерживает здоровье пищеварительной системы.", @@ -256,6 +262,7 @@ class MessageLookup extends MessageLookupByLibrary { ), "days": MessageLookupByLibrary.simpleMessage("дней"), "deception": MessageLookupByLibrary.simpleMessage("Обман"), + "delete": MessageLookupByLibrary.simpleMessage("Удалить"), "deleteComment": MessageLookupByLibrary.simpleMessage( "Удалить комментарий", ), @@ -411,6 +418,9 @@ class MessageLookup extends MessageLookupByLibrary { "invited_people_count": MessageLookupByLibrary.simpleMessage( "Количество приглашенных", ), + "itCantBeRecovered": MessageLookupByLibrary.simpleMessage( + "Его нельзя будет восстановить", + ), "label": MessageLookupByLibrary.simpleMessage("Название"), "language": MessageLookupByLibrary.simpleMessage("Язык"), "last_name": MessageLookupByLibrary.simpleMessage("Фамилия"), @@ -780,6 +790,9 @@ class MessageLookup extends MessageLookupByLibrary { "youShureYouWantToCancelOrder": MessageLookupByLibrary.simpleMessage( "Вы уверены, что хотите отменить заказ?", ), + "youShureYouWantToDeleteAccount": MessageLookupByLibrary.simpleMessage( + "Вы уверены, что хотите удалить аккаунт?", + ), "you_can_not_open_this_day": MessageLookupByLibrary.simpleMessage( "Вы не можете перейти в этот день", ), diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index 5a4e420e..7df3cbcc 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -3414,6 +3414,51 @@ class S { args: [], ); } + + /// `Удаление аккаунта` + String get accountDeleting { + return Intl.message( + 'Удаление аккаунта', + name: 'accountDeleting', + desc: '', + args: [], + ); + } + + /// `Вы уверены, что хотите удалить аккаунт?` + String get youShureYouWantToDeleteAccount { + return Intl.message( + 'Вы уверены, что хотите удалить аккаунт?', + name: 'youShureYouWantToDeleteAccount', + desc: '', + args: [], + ); + } + + /// `Его нельзя будет восстановить` + String get itCantBeRecovered { + return Intl.message( + 'Его нельзя будет восстановить', + name: 'itCantBeRecovered', + desc: '', + args: [], + ); + } + + /// `Удалить` + String get delete { + return Intl.message('Удалить', name: 'delete', desc: '', args: []); + } + + /// `Не получилось удалить ваш аккаунт` + String get cannotDeleteAccount { + return Intl.message( + 'Не получилось удалить ваш аккаунт', + name: 'cannotDeleteAccount', + desc: '', + args: [], + ); + } } class AppLocalizationDelegate extends LocalizationsDelegate { diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 6a8fb162..452e2d5a 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -405,7 +405,12 @@ "allowAccess": "Разрешить доступ" , "copy": "Скопировать", "reply": "Ответить", - "deleteComment": "Удалить комментарий" + "deleteComment": "Удалить комментарий", + "accountDeleting": "Удаление аккаунта", + "youShureYouWantToDeleteAccount": "Вы уверены, что хотите удалить аккаунт?", + "itCantBeRecovered": "Его нельзя будет восстановить", + "delete": "Удалить", + "cannotDeleteAccount": "Не получилось удалить ваш аккаунт" } From 02b621d85a64e5c7e9a2a4f38d9daf9fb527ba82 Mon Sep 17 00:00:00 2001 From: Username1548 Date: Wed, 16 Jul 2025 19:56:54 +0700 Subject: [PATCH 028/134] version update --- ios/Runner.xcodeproj/project.pbxproj | 6 +++--- pubspec.yaml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index d2269b71..f129a677 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -406,7 +406,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - MARKETING_VERSION = 1.0.50; + MARKETING_VERSION = 1.2.3; PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044"; @@ -556,7 +556,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - MARKETING_VERSION = 1.0.50; + MARKETING_VERSION = 1.2.3; PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044"; @@ -594,7 +594,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - MARKETING_VERSION = 1.0.50; + MARKETING_VERSION = 1.2.3; PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044"; diff --git a/pubspec.yaml b/pubspec.yaml index 74b00964..5367764e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: eatvio_app description: A new Flutter project. -version: 1.2.2+52 +version: 1.2.3+54 publish_to: 'none' environment: From 27a0bcd6cec180ebce645a6858d8dc8498a41a12 Mon Sep 17 00:00:00 2001 From: Username1548 Date: Wed, 16 Jul 2025 22:02:53 +0700 Subject: [PATCH 029/134] fix: fixed analyzer issue --- lib/features/chat/presentation/pages/photo_view_screen.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/features/chat/presentation/pages/photo_view_screen.dart b/lib/features/chat/presentation/pages/photo_view_screen.dart index 886b815a..1c17d40e 100644 --- a/lib/features/chat/presentation/pages/photo_view_screen.dart +++ b/lib/features/chat/presentation/pages/photo_view_screen.dart @@ -1,5 +1,3 @@ -import 'dart:io'; - import 'package:cached_network_image/cached_network_image.dart'; import 'package:dismissible_page/dismissible_page.dart'; import 'package:eatvio_app/core/const.dart'; From 955daf4933a314ad533609dac43106e3801206bd Mon Sep 17 00:00:00 2001 From: Username1548 Date: Mon, 21 Jul 2025 19:52:33 +0700 Subject: [PATCH 030/134] feat(testFlight): updated uploading script --- .gitea/workflows/test-flight-cd.yaml | 17 +++++++++++++---- ios/fastlane/Fastfile | 5 +++++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/.gitea/workflows/test-flight-cd.yaml b/.gitea/workflows/test-flight-cd.yaml index 9d810d8e..73dc6975 100644 --- a/.gitea/workflows/test-flight-cd.yaml +++ b/.gitea/workflows/test-flight-cd.yaml @@ -10,14 +10,18 @@ jobs: steps: - name: Checkout uses: actions/checkout@v2 - + + - name: Decode API Key File + run: echo "$AUTH_API_KEY" | base64 --decode > AuthKey.p8 + env: + AUTH_API_KEY: ${{ vars.AUTH_API_KEY }} - name: Unlock login keychain 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: Flutter build run: | flutter clean @@ -33,4 +37,9 @@ jobs: MATCH_PASSWORD: ${{ vars.IOS_RUNNER_CI_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 }} \ No newline at end of file + FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: ${{ vars.FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD }} + APP_STORE_API_KEY_ID: ${{ vars.APP_STORE_API_KEY_ID }} + APP_STORE_API_KEY_ISSUER_ID: ${{ vars.APP_STORE_ISSUER_ID }} + + - name: Delete API Key + run: rm -f AuthKey.p8 diff --git a/ios/fastlane/Fastfile b/ios/fastlane/Fastfile index 4073b355..5ddd51bb 100644 --- a/ios/fastlane/Fastfile +++ b/ios/fastlane/Fastfile @@ -18,6 +18,11 @@ default_platform(:ios) platform :ios do desc "Push a new beta build to TestFlight" lane :beta do + app_store_connect_api_key( + key_id: ENV["APP_STORE_API_KEY_KEY_ID"], + issuer_id: ENV["APP_STORE_API_KEY_ISSUER_ID"], + key_filepath: "AuthKey.p8" + ) match(type: "appstore") increment_build_number(xcodeproj: "Runner.xcodeproj") build_app(workspace: "Runner.xcworkspace", scheme: "Runner", export_method: "app-store", codesigning_identity: "Apple Distribution") From 44a938ea29846ad019a984c11d18b688d118d9b9 Mon Sep 17 00:00:00 2001 From: Username1548 Date: Mon, 21 Jul 2025 20:03:22 +0700 Subject: [PATCH 031/134] feat(testFlight): updated script --- .gitea/workflows/test-flight-cd.yaml | 4 ++-- ios/fastlane/Fastfile | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.gitea/workflows/test-flight-cd.yaml b/.gitea/workflows/test-flight-cd.yaml index 73dc6975..5b16e027 100644 --- a/.gitea/workflows/test-flight-cd.yaml +++ b/.gitea/workflows/test-flight-cd.yaml @@ -12,7 +12,7 @@ jobs: uses: actions/checkout@v2 - name: Decode API Key File - run: echo "$AUTH_API_KEY" | base64 --decode > AuthKey.p8 + run: echo "$AUTH_API_KEY" | base64 --decode > ios/AuthKey.p8 env: AUTH_API_KEY: ${{ vars.AUTH_API_KEY }} @@ -42,4 +42,4 @@ jobs: APP_STORE_API_KEY_ISSUER_ID: ${{ vars.APP_STORE_ISSUER_ID }} - name: Delete API Key - run: rm -f AuthKey.p8 + run: rm -f ios/AuthKey.p8 diff --git a/ios/fastlane/Fastfile b/ios/fastlane/Fastfile index 5ddd51bb..0a4a23f1 100644 --- a/ios/fastlane/Fastfile +++ b/ios/fastlane/Fastfile @@ -21,7 +21,7 @@ platform :ios do app_store_connect_api_key( key_id: ENV["APP_STORE_API_KEY_KEY_ID"], issuer_id: ENV["APP_STORE_API_KEY_ISSUER_ID"], - key_filepath: "AuthKey.p8" + key_filepath: "ios/AuthKey.p8" ) match(type: "appstore") increment_build_number(xcodeproj: "Runner.xcodeproj") From 5db29ae681380d2eaeabef2b3fe21a9ae4565843 Mon Sep 17 00:00:00 2001 From: Username1548 Date: Mon, 21 Jul 2025 20:10:23 +0700 Subject: [PATCH 032/134] feat(testFlight): updated script --- ios/fastlane/Fastfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios/fastlane/Fastfile b/ios/fastlane/Fastfile index 0a4a23f1..5ddd51bb 100644 --- a/ios/fastlane/Fastfile +++ b/ios/fastlane/Fastfile @@ -21,7 +21,7 @@ platform :ios do app_store_connect_api_key( key_id: ENV["APP_STORE_API_KEY_KEY_ID"], issuer_id: ENV["APP_STORE_API_KEY_ISSUER_ID"], - key_filepath: "ios/AuthKey.p8" + key_filepath: "AuthKey.p8" ) match(type: "appstore") increment_build_number(xcodeproj: "Runner.xcodeproj") From ec05b0c8fb87b26c5c6ca82c24f03d108f6e6498 Mon Sep 17 00:00:00 2001 From: Username1548 Date: Mon, 21 Jul 2025 20:14:02 +0700 Subject: [PATCH 033/134] feat(testFlight): updated script --- ios/fastlane/Fastfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios/fastlane/Fastfile b/ios/fastlane/Fastfile index 5ddd51bb..5489e7b2 100644 --- a/ios/fastlane/Fastfile +++ b/ios/fastlane/Fastfile @@ -19,7 +19,7 @@ platform :ios do desc "Push a new beta build to TestFlight" lane :beta do app_store_connect_api_key( - key_id: ENV["APP_STORE_API_KEY_KEY_ID"], + key_id: ENV["APP_STORE_API_KEY_ID"], issuer_id: ENV["APP_STORE_API_KEY_ISSUER_ID"], key_filepath: "AuthKey.p8" ) From 969f8d37af56e11bd55467257771fea5274946f5 Mon Sep 17 00:00:00 2001 From: Username1548 Date: Fri, 25 Jul 2025 19:39:02 +0700 Subject: [PATCH 034/134] feat(diary): fixed logic of review viewing, feat(complaints): added validation for complaints --- ios/Runner.xcodeproj/project.pbxproj | 18 +-- .../comments_actions_bottom_sheet.dart | 11 +- lib/core/const.dart | 1 + .../complaining_bottom_sheet.dart | 128 ++++++++++-------- .../widgets/consultation_card_widget.dart | 1 - .../diary/presentation/bloc/diary_cubit.dart | 5 +- .../diary/repositories/diary_repository.dart | 9 +- 7 files changed, 100 insertions(+), 73 deletions(-) diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index c72f1ce3..afaf999f 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -386,8 +386,8 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 53; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; @@ -409,7 +409,7 @@ MARKETING_VERSION = 1.2.3; PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; @@ -536,8 +536,8 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 53; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; @@ -559,7 +559,7 @@ MARKETING_VERSION = 1.2.3; PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -574,8 +574,8 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 53; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; @@ -597,7 +597,7 @@ MARKETING_VERSION = 1.2.3; PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; diff --git a/lib/core/comments/presentation/comments_actions_bottom_sheet.dart b/lib/core/comments/presentation/comments_actions_bottom_sheet.dart index deeea5ff..5506215d 100644 --- a/lib/core/comments/presentation/comments_actions_bottom_sheet.dart +++ b/lib/core/comments/presentation/comments_actions_bottom_sheet.dart @@ -23,7 +23,7 @@ void showCommentsActionsBottomSheet( isScrollControlled: true, backgroundColor: Colors.transparent, builder: (context) => Container( - margin: EdgeInsets.only(left: 8, right: 8, bottom: MediaQuery.of(context).padding.bottom), + margin: EdgeInsets.only(left: 8, right: 8, bottom: MediaQuery.of(context).padding.bottom + 20), decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), color: appTheme.whiteA700, @@ -189,11 +189,12 @@ class _ActionWidget extends StatelessWidget { padding: const EdgeInsets.symmetric(vertical: 12), child: Row( children: [ - Text( - text, - style: TextStyles.textLMedium.copyWith(color: appTheme.black), + Expanded( + child: Text( + text, + style: TextStyles.textLMedium.copyWith(color: appTheme.black), + ), ), - const Spacer(), SvgPicture.asset(iconPath) ], ), diff --git a/lib/core/const.dart b/lib/core/const.dart index 618a9866..31c47e9c 100644 --- a/lib/core/const.dart +++ b/lib/core/const.dart @@ -1,4 +1,5 @@ String baseUrl = 'https://eatvio.ru'; +// String baseUrl = 'https://dev2.eatvio.com'; const List durationValues = [7, 14, 21]; diff --git a/lib/features/complaining/presentation/complaining_bottom_sheet.dart b/lib/features/complaining/presentation/complaining_bottom_sheet.dart index e3f42616..aa5e1b90 100644 --- a/lib/features/complaining/presentation/complaining_bottom_sheet.dart +++ b/lib/features/complaining/presentation/complaining_bottom_sheet.dart @@ -9,6 +9,7 @@ import 'package:eatvio_app/core/widgets/custom_text_form_field.dart'; import 'package:eatvio_app/core/widgets/loader.dart'; import 'package:eatvio_app/features/complaining/presentation/cubit/complaining_cubit.dart'; import 'package:eatvio_app/features/complaining/presentation/cubit/complaining_state.dart'; +import 'package:eatvio_app/features/create_diet/presentation/widgets/error_widget.dart'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -194,63 +195,74 @@ class _LawViolationWidgetState extends State<_LawViolationWidget> { @override Widget build(BuildContext context) { final s = S.of(context); - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - const BottomSheetHandlerBar(), - const SizedBox(height: 20), - Text( - s.lawViolation, - style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), - ), - const SizedBox(height: 8), - SizedBox( - width: MediaQuery.sizeOf(context).width * 0.7, - child: Text( - s.what_do_you_want_to_complaint_about, - textAlign: TextAlign.center, - style: TextStyles.textMMedium.copyWith(color: appTheme.black), - ), - ), - const SizedBox(height: 24), - ...tags.mapIndexed((index, element) { - return Row( - children: [ - Checkbox( - activeColor: appTheme.blueA400, - side: BorderSide(width: 2, color: tags[index].$1 ? appTheme.blueA400 : appTheme.blueGray), - value: element.$1, - onChanged: (isChecked) { - setState(() { - if (isChecked == null) return; - if (index == tags.length - 1 && !isChecked) { - context.read().toggleDescription(); - } - tags[index] = (isChecked, element.$2); - final activeTags = tags.where((elem) => elem.$1).map((e) => e.$2).toList(); - context.read().setTags(activeTags); - }); + return BlocBuilder( + builder: (context, state) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + const BottomSheetHandlerBar(), + const SizedBox(height: 20), + Text( + s.lawViolation, + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), + ), + const SizedBox(height: 8), + SizedBox( + width: MediaQuery.sizeOf(context).width * 0.7, + child: Text( + s.what_do_you_want_to_complaint_about, + textAlign: TextAlign.center, + style: TextStyles.textMMedium.copyWith(color: appTheme.black), + ), + ), + const SizedBox(height: 24), + ...tags.mapIndexed((index, element) { + return Row( + children: [ + Checkbox( + activeColor: appTheme.blueA400, + side: BorderSide(width: 2, color: tags[index].$1 ? appTheme.blueA400 : appTheme.blueGray), + value: element.$1, + onChanged: (isChecked) { + setState(() { + if (isChecked == null) return; + if (index == tags.length - 1 && !isChecked) { + context.read().toggleDescription(); + } + tags[index] = (isChecked, element.$2); + final activeTags = tags.where((elem) => elem.$1).map((e) => e.$2).toList(); + context.read().setTags(activeTags); + }); + }, + ), + Text( + element.$2, + style: TextStyles.textLSemibold.copyWith(color: appTheme.black), + ), + ], + ); + }), + if (tags.last.$1) ...[ + const SizedBox(height: 12), + CustomTextFormField( + hintText: s.type_here_your_complaint, + hasError: (state.complainInfo.description?.length ?? 10) < 10, + onChanged: (value) { + context.read().setDescription(value); }, ), - Text( - element.$2, - style: TextStyles.textLSemibold.copyWith(color: appTheme.black), - ), + if ((state.complainInfo.description?.length ?? 10) < 10) ...[ + const SizedBox(height: 4), + const ValidationErrorWidget(error: 'Минимум 10 символов'), + ] ], - ); - }), - if (tags.last.$1) ...[ - const SizedBox(height: 12), - CustomTextFormField( - hintText: s.type_here_your_complaint, - onChanged: (value) { - context.read().setDescription(value); - }, - ), - ], - const SizedBox(height: 20), - const _ComplainButton(), - ], + const SizedBox(height: 20), + _ComplainButton( + isDisabled: (state.complainInfo.description?.length ?? 0) < 10, + ), + ], + ); + }, ); } } @@ -515,8 +527,8 @@ class _ComplaintFailedWidget extends StatelessWidget { } class _ComplainButton extends StatelessWidget { - const _ComplainButton(); - + const _ComplainButton({this.isDisabled = false}); + final bool isDisabled; @override Widget build(BuildContext context) { final s = S.of(context); @@ -534,6 +546,10 @@ class _ComplainButton extends StatelessWidget { backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), ), onPressed: () async { + if (isDisabled) { + context.read().setDescription(''); + return; + } await context.read().complain(); }, ); diff --git a/lib/features/consultations/presentation/widgets/consultation_card_widget.dart b/lib/features/consultations/presentation/widgets/consultation_card_widget.dart index 5173ab2d..04a0b7fb 100644 --- a/lib/features/consultations/presentation/widgets/consultation_card_widget.dart +++ b/lib/features/consultations/presentation/widgets/consultation_card_widget.dart @@ -302,7 +302,6 @@ class _ConsultationCardWidgetState extends State { 'diaryId': lastActiveDay.$2.id, 'currentDiaryDay': lastActiveDay.$1, }); - GoRouter.of(context).push(UserDiaryScreen.path, extra: lastActiveDay.$2.id); return; } showErrorDialog(context, diff --git a/lib/features/diary/presentation/bloc/diary_cubit.dart b/lib/features/diary/presentation/bloc/diary_cubit.dart index 45394cbe..9fd27c39 100644 --- a/lib/features/diary/presentation/bloc/diary_cubit.dart +++ b/lib/features/diary/presentation/bloc/diary_cubit.dart @@ -179,8 +179,11 @@ class DiaryCubit extends Cubit { int? currentPlanDay; String? planName; if (diaryDay.chosenPlanId != null) { - consultantReview = await diaryRepository.getDiaryComments(planId: diaryDay.chosenPlanId!, diaryId: diaryDay.id); final chosenPlanModel = await chatRepository.getChosenPlan(id: diaryDay.chosenPlanId!); + if (diaryDay.typeOfDiary.diaryState == DiaryStateEnum.lastDayCommented || diaryDay.typeOfDiary.diaryState == DiaryStateEnum.commented) { + await diaryRepository.viewCommentOfDiaryDayByConsultant(diaryId: diaryDay.id); + } + consultantReview = await diaryRepository.getDiaryComments(planId: diaryDay.chosenPlanId!, diaryId: diaryDay.id); planDuration = chosenPlanModel.plan.duration; planName = chosenPlanModel.plan.name; currentPlanDay = consultantReview.currentPlanDay; diff --git a/lib/features/diary/repositories/diary_repository.dart b/lib/features/diary/repositories/diary_repository.dart index 14a9576b..2ac30803 100644 --- a/lib/features/diary/repositories/diary_repository.dart +++ b/lib/features/diary/repositories/diary_repository.dart @@ -292,10 +292,17 @@ class DiaryRepository { required int diaryId, }) async { try { - await diaryService.viewCommentOfDiaryDayByConsultant(diaryId: diaryId); return await diaryService.getDiaryComments(planId: planId, diaryId: diaryId); } catch (e) { rethrow; } } + + Future viewCommentOfDiaryDayByConsultant({required int diaryId}) async { + try { + await diaryService.viewCommentOfDiaryDayByConsultant(diaryId: diaryId); + } catch (e) { + rethrow; + } + } } From f0c94c4f82f401e3c8b4c4ed597d154f61b8d5b7 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Mon, 28 Jul 2025 13:58:48 +0300 Subject: [PATCH 035/134] fix: testflight --- ios/Runner.xcodeproj/project.pbxproj | 32 +++++++++++++++++----------- ios/Runner/Info.plist | 2 +- ios/fastlane/Fastfile | 5 ----- ios/fastlane/report.xml | 10 +++++---- 4 files changed, 27 insertions(+), 22 deletions(-) diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index afaf999f..23adc916 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -213,10 +213,14 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); + inputPaths = ( + ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; @@ -283,10 +287,14 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", ); + inputPaths = ( + ); name = "[CP] Copy Pods Resources"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; @@ -386,9 +394,9 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 53; + CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = 62; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -409,7 +417,7 @@ MARKETING_VERSION = 1.2.3; PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; + PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; @@ -536,9 +544,9 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 53; + CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = 62; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -559,7 +567,7 @@ MARKETING_VERSION = 1.2.3; PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; + PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -574,9 +582,9 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 53; + CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = 62; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -597,7 +605,7 @@ MARKETING_VERSION = 1.2.3; PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; + PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index fcaddf14..f5570627 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -21,7 +21,7 @@ CFBundleSignature ???? CFBundleVersion - 53 + 62 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/ios/fastlane/Fastfile b/ios/fastlane/Fastfile index 5489e7b2..4073b355 100644 --- a/ios/fastlane/Fastfile +++ b/ios/fastlane/Fastfile @@ -18,11 +18,6 @@ default_platform(:ios) platform :ios do desc "Push a new beta build to TestFlight" lane :beta do - app_store_connect_api_key( - key_id: ENV["APP_STORE_API_KEY_ID"], - issuer_id: ENV["APP_STORE_API_KEY_ISSUER_ID"], - key_filepath: "AuthKey.p8" - ) match(type: "appstore") increment_build_number(xcodeproj: "Runner.xcodeproj") build_app(workspace: "Runner.xcworkspace", scheme: "Runner", export_method: "app-store", codesigning_identity: "Apple Distribution") diff --git a/ios/fastlane/report.xml b/ios/fastlane/report.xml index c86e2b65..0d039029 100644 --- a/ios/fastlane/report.xml +++ b/ios/fastlane/report.xml @@ -5,22 +5,24 @@ - + - + - + - + + + From 43495f407e83dcef9537ed905c47d0ce71920fe2 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Mon, 28 Jul 2025 11:02:17 +0000 Subject: [PATCH 036/134] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20.gitea/workflows/test-flight-cd.yaml?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/test-flight-cd.yaml | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/.gitea/workflows/test-flight-cd.yaml b/.gitea/workflows/test-flight-cd.yaml index 5b16e027..9d810d8e 100644 --- a/.gitea/workflows/test-flight-cd.yaml +++ b/.gitea/workflows/test-flight-cd.yaml @@ -10,18 +10,14 @@ jobs: steps: - name: Checkout uses: actions/checkout@v2 - - - name: Decode API Key File - run: echo "$AUTH_API_KEY" | base64 --decode > ios/AuthKey.p8 - env: - AUTH_API_KEY: ${{ vars.AUTH_API_KEY }} + - name: Unlock login keychain 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: Flutter build run: | flutter clean @@ -37,9 +33,4 @@ jobs: MATCH_PASSWORD: ${{ vars.IOS_RUNNER_CI_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 }} - APP_STORE_API_KEY_ID: ${{ vars.APP_STORE_API_KEY_ID }} - APP_STORE_API_KEY_ISSUER_ID: ${{ vars.APP_STORE_ISSUER_ID }} - - - name: Delete API Key - run: rm -f ios/AuthKey.p8 + FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: ${{ vars.FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD }} \ No newline at end of file From d037b6aaa71c4008a7f22832e08eedae26ab5ebb Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Mon, 28 Jul 2025 13:10:43 +0000 Subject: [PATCH 037/134] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20.gitea/workflows/test-flight-cd.yaml?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .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 9d810d8e..ba101d47 100644 --- a/.gitea/workflows/test-flight-cd.yaml +++ b/.gitea/workflows/test-flight-cd.yaml @@ -31,6 +31,7 @@ jobs: LANG: en_US.UTF-8 LC_ALL: en_US.UTF-8 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 }} \ No newline at end of file From 20e71a77d833e33ca6c8395e51e900a6e9b5960f Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Mon, 28 Jul 2025 16:14:43 +0300 Subject: [PATCH 038/134] fix: testflight --- .gitea/workflows/test-flight-cd.yaml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.gitea/workflows/test-flight-cd.yaml b/.gitea/workflows/test-flight-cd.yaml index 9d810d8e..fe4fafcb 100644 --- a/.gitea/workflows/test-flight-cd.yaml +++ b/.gitea/workflows/test-flight-cd.yaml @@ -26,11 +26,13 @@ jobs: flutter pub run build_runner build --delete-conflicting-outputs cd ios pod update - fastlane beta + bundle install + bundle exec fastlane beta env: LANG: en_US.UTF-8 LC_ALL: en_US.UTF-8 MATCH_PASSWORD: ${{ vars.IOS_RUNNER_CI_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 }} \ No newline at end of file + FASTLANE_PASSWORD: ${{ vars.FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD }} + \ No newline at end of file From aacbc0ce5abd7b0c903ce0aaf1adfb15a4863a5e Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Mon, 28 Jul 2025 16:18:34 +0300 Subject: [PATCH 039/134] fix: tf --- .gitea/workflows/test-flight-cd.yaml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.gitea/workflows/test-flight-cd.yaml b/.gitea/workflows/test-flight-cd.yaml index fe4fafcb..ba101d47 100644 --- a/.gitea/workflows/test-flight-cd.yaml +++ b/.gitea/workflows/test-flight-cd.yaml @@ -26,13 +26,12 @@ jobs: flutter pub run build_runner build --delete-conflicting-outputs cd ios pod update - bundle install - bundle exec fastlane beta + 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_GIT_URL: http://gitea.office.local:3000/mso7nueh/fastlane FASTLANE_USER: eatvio2023@icloud.com - FASTLANE_PASSWORD: ${{ vars.FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD }} - \ No newline at end of file + FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: ${{ vars.FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD }} \ No newline at end of file From 09a1ec0947b3eaa38ca6e942fb210951e76bef68 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Wed, 30 Jul 2025 12:34:27 +0300 Subject: [PATCH 040/134] fix: reg, verify, loaders, grey screen when like --- .../diary/presentation/bloc/diary_cubit.dart | 1 + .../diary/presentation/bloc/diary_state.dart | 1 + .../widgets/product_loader_widget.dart | 26 ++ .../widgets/products_list_widget.dart | 6 + .../profile/models/profile_model.dart | 1 + .../presentation/pages/profile_screen.dart | 16 +- .../registration_cubit.dart | 3 +- .../registration_state.dart | 1 + .../pages/registration_screen.dart | 265 ++++++++++-------- .../presentation/pages/verify_screen.dart | 21 +- .../repositories/registration_repository.dart | 56 +++- .../bloc/settings_cubit/settings_cubit.dart | 6 +- .../services/profile_service_test.dart | 1 + 13 files changed, 260 insertions(+), 144 deletions(-) create mode 100644 lib/features/diary/presentation/widgets/product_loader_widget.dart diff --git a/lib/features/diary/presentation/bloc/diary_cubit.dart b/lib/features/diary/presentation/bloc/diary_cubit.dart index 9fd27c39..1bbfbbc7 100644 --- a/lib/features/diary/presentation/bloc/diary_cubit.dart +++ b/lib/features/diary/presentation/bloc/diary_cubit.dart @@ -243,6 +243,7 @@ class DiaryCubit extends Cubit { required int quantity, int? productNutritionalValueId, }) async { + emit((state as DiaryStateLoaded).copyWith(isLoadingAddingProductInMeal: true)); await diaryRepository.addPortionToMeal( diaryId: diaryId, mealId: mealId, diff --git a/lib/features/diary/presentation/bloc/diary_state.dart b/lib/features/diary/presentation/bloc/diary_state.dart index d5e8e39d..4779e2ea 100644 --- a/lib/features/diary/presentation/bloc/diary_state.dart +++ b/lib/features/diary/presentation/bloc/diary_state.dart @@ -29,6 +29,7 @@ abstract class DiaryState with _$DiaryState { @Default(false) bool isDiaryCommented, @Default(false) bool isWakeUpAndRetireEnabled, @Default(false) bool isMoodEnabled, + @Default(false) bool isLoadingAddingProductInMeal, }) = DiaryStateLoaded; const factory DiaryState.loadedTomorrowWithDiet({ diff --git a/lib/features/diary/presentation/widgets/product_loader_widget.dart b/lib/features/diary/presentation/widgets/product_loader_widget.dart new file mode 100644 index 00000000..7b679354 --- /dev/null +++ b/lib/features/diary/presentation/widgets/product_loader_widget.dart @@ -0,0 +1,26 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +class ProductLoaderWidget extends StatefulWidget { + const ProductLoaderWidget({ + super.key, + }); + + @override + State createState() => _ProductLoaderWidgetState(); +} + +class _ProductLoaderWidgetState extends State { + @override + Widget build(BuildContext context) { + return ListTile( + horizontalTitleGap: 0, + contentPadding: const EdgeInsets.only(right: 24, left: 12), + title: const CupertinoActivityIndicator(), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + tileColor: const Color(0xFFF6FAFF), + ); + } +} diff --git a/lib/features/diary/presentation/widgets/products_list_widget.dart b/lib/features/diary/presentation/widgets/products_list_widget.dart index 13bbe7fc..6882af08 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/product_loader_widget.dart'; import 'package:eatvio_app/features/diary/presentation/widgets/product_widget.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -25,6 +26,11 @@ class _ProductsListWidgetState extends State { ProductWidget(portion: state.diary.meals[mealIndex].portions[i]), i < state.diary.meals[mealIndex].portions.length - 1 ? const SizedBox(height: 8) : const SizedBox.shrink(), ], + if (state.isLoadingAddingProductInMeal) + const Padding( + padding: EdgeInsets.only(top: 8), + child: ProductLoaderWidget(), + ), ], ); } diff --git a/lib/features/profile/models/profile_model.dart b/lib/features/profile/models/profile_model.dart index 72e91d0d..f798021f 100644 --- a/lib/features/profile/models/profile_model.dart +++ b/lib/features/profile/models/profile_model.dart @@ -14,6 +14,7 @@ abstract class ProfileModel with _$ProfileModel { @JsonKey(name: "email") required String email, @JsonKey(name: "rating_place") int? ratingPlace, @JsonKey(name: "email_verified_at") DateTime? emailVerifiedAt, + @JsonKey(name: "phone_confirmation_state") required String phoneConfirmationState, @JsonKey(name: "created_at") DateTime? createdAt, @JsonKey(name: "updated_at") DateTime? updatedAt, @JsonKey(name: "is_public_profile", fromJson: parseBool) required bool isPublicProfile, diff --git a/lib/features/profile/presentation/pages/profile_screen.dart b/lib/features/profile/presentation/pages/profile_screen.dart index 07ab1bd2..68213da5 100644 --- a/lib/features/profile/presentation/pages/profile_screen.dart +++ b/lib/features/profile/presentation/pages/profile_screen.dart @@ -138,7 +138,7 @@ class _ProfileScreenState extends State { crossAxisAlignment: CrossAxisAlignment.center, children: [ _header(state), - if (state.profile.roles.contains('consultant')) + if (state.profile.roles.contains('consultant') && state.consultantInfo != null) Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -202,10 +202,10 @@ 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), - if (state.profile.roles.contains('consultant')) _education(state), - if (state.profile.roles.contains('consultant')) _diplomasAndCertificates(state), - if (state.profile.roles.contains('consultant')) _activePlans(state), + if (state.profile.roles.contains('consultant') && state.consultantInfo != null) _socialLinks(state), + if (state.profile.roles.contains('consultant') && state.consultantInfo != null) _education(state), + if (state.profile.roles.contains('consultant') && state.consultantInfo != null) _diplomasAndCertificates(state), + if (state.profile.roles.contains('consultant') && state.consultantInfo != null) _activePlans(state), _blogPosts(state), ], ), @@ -216,7 +216,9 @@ class _ProfileScreenState extends State { Scaffold _myProfileScreen(ProfileStateLoaded state) { return Scaffold( backgroundColor: Colors.grey.shade200, - appBar: AppBar(), + appBar: AppBar( + toolbarHeight: widget.author == null ? 0 : null, + ), body: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -644,7 +646,7 @@ class _ProfileScreenState extends State { } SingleChildRenderObjectWidget _socialLinks(ProfileStateLoaded state) { - if (!state.profile.roles.contains('consultant')) { + if (!state.profile.roles.contains('consultant') && state.consultantInfo != null) { return const SizedBox.shrink(); } return Padding( diff --git a/lib/features/registration/presentation/bloc/registration_cubit/registration_cubit.dart b/lib/features/registration/presentation/bloc/registration_cubit/registration_cubit.dart index fa2948bc..c9426674 100644 --- a/lib/features/registration/presentation/bloc/registration_cubit/registration_cubit.dart +++ b/lib/features/registration/presentation/bloc/registration_cubit/registration_cubit.dart @@ -60,10 +60,11 @@ class RegistrationCubit extends Cubit { ), ); await TokenStorage.saveTokenInStorage(token); - AppRouter.navigatorKey.currentContext?.pushNamed('verifyEmailScreen'); await sl().registerPushToken(); + AppRouter.navigatorKey.currentContext?.replaceNamed('verifyEmailScreen'); } catch (e, st) { log(e.toString(), stackTrace: st); + emit(RegistrationState(sheet: state.sheet, error: e.toString())); } } diff --git a/lib/features/registration/presentation/bloc/registration_cubit/registration_state.dart b/lib/features/registration/presentation/bloc/registration_cubit/registration_state.dart index e4347d20..6a280864 100644 --- a/lib/features/registration/presentation/bloc/registration_cubit/registration_state.dart +++ b/lib/features/registration/presentation/bloc/registration_cubit/registration_state.dart @@ -7,5 +7,6 @@ part 'registration_state.freezed.dart'; abstract class RegistrationState with _$RegistrationState { const factory RegistrationState({ required UserRegistrationSheet sheet, + String? error, }) = _RegistrationState; } diff --git a/lib/features/registration/presentation/pages/registration_screen.dart b/lib/features/registration/presentation/pages/registration_screen.dart index 69ae62d5..86514be8 100644 --- a/lib/features/registration/presentation/pages/registration_screen.dart +++ b/lib/features/registration/presentation/pages/registration_screen.dart @@ -43,142 +43,161 @@ class _RegistrationScreenState extends State { Widget build(BuildContext context) { return BlocProvider( create: (BuildContext context) => registrationCubit, - child: BlocBuilder( - builder: (BuildContext context, RegistrationState state) { - return Scaffold( - floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, - floatingActionButton: Row( - children: [ - Container( - height: 48, - width: MediaQuery.of(context).size.width * 0.2, - padding: const EdgeInsets.only(left: 16, right: 4), - child: FloatingActionButton.extended( - heroTag: 'backButtonRegistration', - highlightElevation: 0, - focusElevation: 0, - elevation: 0, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), - ), - backgroundColor: const Color(0xFFEAF2FD), - onPressed: () async { - setState(() { - pageController.previousPage( - duration: const Duration(milliseconds: 350), - curve: Curves.easeInOut, - ); - }); - }, - label: Icon( - Icons.arrow_back_ios_new_rounded, - color: appTheme.blueA400, - ), + child: BlocListener( + listener: (context, state) { + if (state.error != null && state.error!.isNotEmpty) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Ошибка при регистрации'), + content: Text(state.error!), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('OK'), ), - ), - Container( - height: 48, - width: MediaQuery.of(context).size.width * 0.8, - padding: const EdgeInsets.only(left: 4, right: 16), - child: FloatingActionButton.extended( - heroTag: 'nextButtonRegistration', - highlightElevation: 0, - elevation: 0, - focusElevation: 0, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), - ), - backgroundColor: appTheme.blueA400, - onPressed: () async { - if (await canGoNext(state, context)) { + ], + ), + ); + } + }, + child: BlocBuilder( + builder: (BuildContext context, RegistrationState state) { + return Scaffold( + floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, + floatingActionButton: Row( + children: [ + Container( + height: 48, + width: MediaQuery.of(context).size.width * 0.2, + padding: const EdgeInsets.only(left: 16, right: 4), + child: FloatingActionButton.extended( + heroTag: 'backButtonRegistration', + highlightElevation: 0, + focusElevation: 0, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + backgroundColor: const Color(0xFFEAF2FD), + onPressed: () async { setState(() { - pageController.nextPage( + pageController.previousPage( duration: const Duration(milliseconds: 350), curve: Curves.easeInOut, ); }); - } - }, - label: Text( - 'Продолжить', - style: theme.textTheme.titleSmall!.copyWith(color: Colors.white), + }, + label: Icon( + Icons.arrow_back_ios_new_rounded, + color: appTheme.blueA400, + ), ), ), - ), - ], - ), - appBar: AppBar( - automaticallyImplyLeading: false, - leading: Builder( - builder: (context) => IconButton( - icon: const Icon(Icons.close_rounded), - onPressed: () => GoRouter.of(context).pop(), - ), - ), - centerTitle: true, - title: SvgPicture.asset( - ImageConstant.imgLogoEatvio, - height: 48, - fit: BoxFit.contain, - ), - actions: [ - Padding( - padding: const EdgeInsets.only(right: 16), - child: Text('шаг $step из 15'), - ), - ], - bottom: PreferredSize( - preferredSize: Size(MediaQuery.of(context).size.width, 32), - child: Padding( - padding: const EdgeInsets.all(16), - child: TweenAnimationBuilder( - duration: const Duration(milliseconds: 350), - curve: Curves.easeInOut, - tween: Tween( - begin: 1 / 15, - end: progressValue, + Container( + height: 48, + width: MediaQuery.of(context).size.width * 0.8, + padding: const EdgeInsets.only(left: 4, right: 16), + child: FloatingActionButton.extended( + heroTag: 'nextButtonRegistration', + highlightElevation: 0, + elevation: 0, + focusElevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + backgroundColor: appTheme.blueA400, + onPressed: () async { + if (await canGoNext(state, context)) { + setState(() { + pageController.nextPage( + duration: const Duration(milliseconds: 350), + curve: Curves.easeInOut, + ); + }); + } + }, + label: Text( + 'Продолжить', + style: theme.textTheme.titleSmall!.copyWith(color: Colors.white), + ), ), - builder: (context, value, _) => LinearProgressIndicator( - value: value, - borderRadius: BorderRadius.circular(16), - minHeight: 8, - backgroundColor: Colors.grey.shade100, - valueColor: AlwaysStoppedAnimation(appTheme.blueA400), + ), + ], + ), + appBar: AppBar( + automaticallyImplyLeading: false, + leading: Builder( + builder: (context) => IconButton( + icon: const Icon(Icons.close_rounded), + onPressed: () => GoRouter.of(context).pop(), + ), + ), + centerTitle: true, + title: SvgPicture.asset( + ImageConstant.imgLogoEatvio, + height: 48, + fit: BoxFit.contain, + ), + actions: [ + Padding( + padding: const EdgeInsets.only(right: 16), + child: Text('шаг $step из 15'), + ), + ], + bottom: PreferredSize( + preferredSize: Size(MediaQuery.of(context).size.width, 32), + child: Padding( + padding: const EdgeInsets.all(16), + child: TweenAnimationBuilder( + duration: const Duration(milliseconds: 350), + curve: Curves.easeInOut, + tween: Tween( + begin: 1 / 15, + end: progressValue, + ), + builder: (context, value, _) => LinearProgressIndicator( + value: value, + borderRadius: BorderRadius.circular(16), + minHeight: 8, + backgroundColor: Colors.grey.shade100, + valueColor: AlwaysStoppedAnimation(appTheme.blueA400), + ), ), ), ), ), - ), - body: PageView( - physics: const NeverScrollableScrollPhysics(), - controller: pageController, - onPageChanged: (index) { - setState(() { - step = index + 1; - progressValue = step / 15; - }); - }, - children: [ - // LanguageSelectScreen(sheet: state.sheet), - PhoneScreen(sheet: state.sheet), - CredentialsScreen(sheet: state.sheet), - NameScreen(sheet: state.sheet), - GenderScreen(sheet: state.sheet), - DateOfBirthScreen(sheet: state.sheet), - CityOfLivingScreen(sheet: state.sheet), - BodyCircumferencesScreen(sheet: state.sheet), - WeightScreen(sheet: state.sheet), - HeightScreen(sheet: state.sheet), - BloodPressureScreen(sheet: state.sheet), - AllergiesScreen(sheet: state.sheet), - MedicineIntakeScreen(sheet: state.sheet), - FavoriteSportsScreen(sheet: state.sheet), - FavoriteFoodsScreen(sheet: state.sheet), - AdditionalInformationScreen(sheet: state.sheet), - ], - ), - ); - }, + body: PageView( + physics: const NeverScrollableScrollPhysics(), + controller: pageController, + onPageChanged: (index) { + setState(() { + step = index + 1; + progressValue = step / 15; + }); + }, + children: [ + // LanguageSelectScreen(sheet: state.sheet), + PhoneScreen(sheet: state.sheet), + CredentialsScreen(sheet: state.sheet), + NameScreen(sheet: state.sheet), + GenderScreen(sheet: state.sheet), + DateOfBirthScreen(sheet: state.sheet), + CityOfLivingScreen(sheet: state.sheet), + BodyCircumferencesScreen(sheet: state.sheet), + WeightScreen(sheet: state.sheet), + HeightScreen(sheet: state.sheet), + BloodPressureScreen(sheet: state.sheet), + AllergiesScreen(sheet: state.sheet), + MedicineIntakeScreen(sheet: state.sheet), + FavoriteSportsScreen(sheet: state.sheet), + FavoriteFoodsScreen(sheet: state.sheet), + AdditionalInformationScreen(sheet: state.sheet), + ], + ), + ); + }, + ), ), ); } diff --git a/lib/features/registration/presentation/pages/verify_screen.dart b/lib/features/registration/presentation/pages/verify_screen.dart index 7f8aede5..637dba5b 100644 --- a/lib/features/registration/presentation/pages/verify_screen.dart +++ b/lib/features/registration/presentation/pages/verify_screen.dart @@ -1,5 +1,6 @@ import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/core/widgets/loader.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:lottie/lottie.dart'; @@ -19,6 +20,7 @@ class VerifyScreen extends StatefulWidget { class _VerifyScreenState extends State { late final WebViewController _controller; + bool _isLoading = true; @override void initState() { @@ -27,6 +29,11 @@ class _VerifyScreenState extends State { ..setJavaScriptMode(JavaScriptMode.unrestricted) ..setNavigationDelegate( NavigationDelegate( + onPageFinished: (url) { + setState(() { + _isLoading = false; + }); + }, onNavigationRequest: (NavigationRequest request) async { if (request.url.contains('user_detailz') && !request.url.contains('change-phone-verify') || request.url.contains('email/verify')) { GoRouter.of(context).pop(); @@ -105,11 +112,17 @@ class _VerifyScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - toolbarHeight: 0, - ), + appBar: AppBar(toolbarHeight: 0), body: SafeArea( - child: WebViewWidget(controller: _controller), + child: Stack( + children: [ + WebViewWidget(controller: _controller), + if (_isLoading) + const Center( + child: Loader(), + ), + ], + ), ), ); } diff --git a/lib/features/registration/repositories/registration_repository.dart b/lib/features/registration/repositories/registration_repository.dart index 0c3008e0..a34bf1b0 100644 --- a/lib/features/registration/repositories/registration_repository.dart +++ b/lib/features/registration/repositories/registration_repository.dart @@ -1,7 +1,5 @@ - import 'package:dio/dio.dart'; import 'package:eatvio_app/core/locator_service.dart'; -import 'package:eatvio_app/core/utils/toasts.dart'; import 'package:eatvio_app/features/registration/models/user_registration_model.dart'; import 'package:eatvio_app/features/registration/models/location_model.dart'; import 'package:eatvio_app/features/registration/services/registration_service.dart'; @@ -14,13 +12,55 @@ class RegistrationRepository { Future tryRegister(UserRegistrationSheet sheet) async { try { final response = await registrationService.register(sheet: sheet.toJson()); - try { - return (response['token']!); - } catch (e) { - showErrorToast(response.toString()); - rethrow; + if (response.containsKey('token')) { + return response['token']!; + } else { + // Если нет токена, но есть message или errors + String errorMsg = ''; + if (response.containsKey('message')) { + errorMsg += '${response['message']!}\n'; + } + if (response.containsKey('errors')) { + final errors = response['errors']; + if (errors != null && errors is Map) { + (errors as Map).forEach((key, value) { + if (value is List && value.isNotEmpty) { + errorMsg += '${value.join('\n')}\n'; + } + }); + } + } + if (errorMsg.isNotEmpty) { + throw errorMsg.trim(); + } else { + throw 'Ошибка регистрации: $response'; + } + } + } on DioException catch (e) { + if (e.response != null && e.response?.statusCode == 422) { + final data = e.response?.data; + String errorMsg = ''; + if (data is Map) { + if (data.containsKey('message')) { + errorMsg += data['message'] + '\n'; + } + if (data.containsKey('errors')) { + final errors = data['errors']; + if (errors != null && errors is Map) { + (errors).forEach((key, value) { + if (value is List && value.isNotEmpty) { + errorMsg += '${value.join('\n')}\n'; + } + }); + } + } + } + if (errorMsg.isNotEmpty) { + throw errorMsg.trim(); + } else { + throw 'Ошибка регистрации: $data'; + } } - } catch (e) { rethrow; } } 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 3cb047f6..ae498824 100644 --- a/lib/features/settings/presentation/bloc/settings_cubit/settings_cubit.dart +++ b/lib/features/settings/presentation/bloc/settings_cubit/settings_cubit.dart @@ -22,7 +22,11 @@ class SettingsCubit extends Cubit { final profile = await profileRepository.getMyProfile(); late final ConsultationRequestModel? consultantInfo; try { - consultantInfo = await consultantRepository.getMyRequestStatus(profile.slug); + if (profile.phoneConfirmationState.contains('confirmed')) { + consultantInfo = await consultantRepository.getMyRequestStatus(profile.slug); + } else { + consultantInfo = null; + } } catch (e) { consultantInfo = null; } diff --git a/test/features/profile/services/profile_service_test.dart b/test/features/profile/services/profile_service_test.dart index b2c51b01..106da096 100644 --- a/test/features/profile/services/profile_service_test.dart +++ b/test/features/profile/services/profile_service_test.dart @@ -19,6 +19,7 @@ void main() { id: 0, name: 'Alexander Avanesyan', email: 'mso7nueh@mail.ru', + phoneConfirmationState: '', isPublicProfile: true, isEntitySubscribedByMe: false, isBlockedByMe: false, From a134c2043b903a7d437546b438ccd7d171804a4f Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Wed, 30 Jul 2025 14:33:28 +0300 Subject: [PATCH 041/134] upd ver pack --- android/app/build.gradle | 2 +- pubspec.yaml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index a5377cc6..274f5a87 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -29,7 +29,7 @@ android { defaultConfig { applicationId = "com.eatvioapp.app" - minSdk = flutter.minSdkVersion + minSdk = 23 targetSdk = flutter.targetSdkVersion versionCode = flutter.versionCode versionName = flutter.versionName diff --git a/pubspec.yaml b/pubspec.yaml index 4223ded7..17edab5a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -61,8 +61,8 @@ dependencies: sentry_flutter: ^8.14.2 connectivity_plus: ^6.1.4 visibility_detector: ^0.4.0+2 - firebase_core: ^3.15.0 - firebase_messaging: ^15.2.8 + firebase_core: ^4.0.0 + firebase_messaging: ^16.0.0 http_parser: ^4.1.2 path: ^1.9.1 collection: ^1.19.1 @@ -77,7 +77,7 @@ dev_dependencies: build_runner: ^2.5.4 freezed: ^3.0.6 json_serializable: ^6.9.5 - retrofit_generator: ^9.2.0 + retrofit_generator: ^10.0.1 mockito: ^5.4.6 flutter_test: From 190e1781eee8c958fed460e738b2ebde91182b1d Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Wed, 30 Jul 2025 15:41:43 +0300 Subject: [PATCH 042/134] fix: recipe create --- lib/core/const.dart | 1 - lib/features/create_recipe/reporitories/recipe_repository.dart | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/core/const.dart b/lib/core/const.dart index 31c47e9c..618a9866 100644 --- a/lib/core/const.dart +++ b/lib/core/const.dart @@ -1,5 +1,4 @@ String baseUrl = 'https://eatvio.ru'; -// String baseUrl = 'https://dev2.eatvio.com'; const List durationValues = [7, 14, 21]; diff --git a/lib/features/create_recipe/reporitories/recipe_repository.dart b/lib/features/create_recipe/reporitories/recipe_repository.dart index f0f860e9..289233a2 100644 --- a/lib/features/create_recipe/reporitories/recipe_repository.dart +++ b/lib/features/create_recipe/reporitories/recipe_repository.dart @@ -57,7 +57,7 @@ class RecipeRepository { 'type': recipe.type, 'state': 'active', 'title': recipe.title, - 'tags': recipe.tags, + 'tags[]': recipe.tags ?? [], 'description_md': recipe.description, 'video_url': recipe.videoUrl, 'ingredients': ingredientsMap, From 7192db31e45ecb0d1be350d2d7b7bd3410e8f8d1 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Thu, 31 Jul 2025 14:27:01 +0300 Subject: [PATCH 043/134] fix: upd ver --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 17edab5a..10001259 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: eatvio_app description: A new Flutter project. -version: 1.2.4+54 +version: 1.2.6+60 publish_to: 'none' environment: From 8a26fa0990b9a405d1ae6ffcc18e58233595ae90 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Thu, 31 Jul 2025 14:55:35 +0300 Subject: [PATCH 044/134] fix: upd ver sdk --- .gitea/workflows/test.yml | 2 +- pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml index 75372b09..fd291367 100644 --- a/.gitea/workflows/test.yml +++ b/.gitea/workflows/test.yml @@ -3,7 +3,7 @@ name: Eatvio App Android Test on: [ pull_request ] env: - flutter_version: "3.32.5" + flutter_version: "3.32.8" java_version: "21.x" flutter_channel: "stable" my_local_cache_dir: "/opt/hostedtoolcache/" diff --git a/pubspec.yaml b/pubspec.yaml index 10001259..bda9d6a9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,7 +4,7 @@ version: 1.2.6+60 publish_to: 'none' environment: - sdk: '>=3.3.4 <4.0.0' + sdk: '>=3.8.0 <4.0.0' dependencies: flutter: From f5d27ed056035f2e6bc8abf5525e041fa70e02f2 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Fri, 1 Aug 2025 10:53:28 +0300 Subject: [PATCH 045/134] feat(plans): hide for ios --- lib/core/const.dart | 1 + .../feed/presentation/pages/feed_screen.dart | 182 +++--- .../feed/presentation/widgets/chip_group.dart | 9 +- .../presentation/pages/profile_screen.dart | 547 ++++-------------- 4 files changed, 184 insertions(+), 555 deletions(-) diff --git a/lib/core/const.dart b/lib/core/const.dart index 618a9866..f22f79aa 100644 --- a/lib/core/const.dart +++ b/lib/core/const.dart @@ -1,4 +1,5 @@ String baseUrl = 'https://eatvio.ru'; +bool hidePlans = true; const List durationValues = [7, 14, 21]; diff --git a/lib/features/feed/presentation/pages/feed_screen.dart b/lib/features/feed/presentation/pages/feed_screen.dart index b6869040..ce29b884 100644 --- a/lib/features/feed/presentation/pages/feed_screen.dart +++ b/lib/features/feed/presentation/pages/feed_screen.dart @@ -1,4 +1,5 @@ import 'package:easy_debounce/easy_debounce.dart'; +import 'package:eatvio_app/core/const.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'; @@ -32,24 +33,12 @@ class FeedScreen extends StatelessWidget { Widget build(BuildContext context) { return MultiBlocProvider( providers: [ - BlocProvider( - create: (context) => sl()..loadRecipesList(), - ), - BlocProvider( - create: (context) => sl()..getProfile(), - ), - BlocProvider( - create: (context) => sl()..loadFeedBlogPostList(), - ), - BlocProvider( - create: (context) => sl()..loadDiets(), - ), - BlocProvider( - create: (context) => sl()..loadPlans(), - ), - BlocProvider( - create: (context) => sl()..getConsultants(), - ), + 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()), ], child: const FeedScreenContent(), ); @@ -69,7 +58,8 @@ class _FeedScreenContentState extends State { final dietsController = TextEditingController(); final plansController = TextEditingController(); final consultantsController = TextEditingController(); - final List chipsLabelList = ['Все', 'Рецепты', 'Диеты', 'Планы питания', 'Консультанты']; + + late final List chipsLabelList; int _selectedChipIndex = 0; final blogPostsScrollController = ScrollController(); final recipesScrollController = ScrollController(); @@ -78,12 +68,13 @@ class _FeedScreenContentState extends State { final consultantsScrollController = ScrollController(); @override - void initState() { + void initState() { + // Initialize chipsLabelList based on hidePlans setting + chipsLabelList = hidePlans ? ['Все', 'Рецепты', 'Диеты', 'Консультанты'] : ['Все', 'Рецепты', 'Диеты', 'Планы питания', 'Консультанты']; + consultantsScrollController.addListener(_onScrollConsultants); super.initState(); - } - - + } void _onScrollConsultants() { if (consultantsScrollController.offset >= consultantsScrollController.position.maxScrollExtent && !consultantsScrollController.position.outOfRange) {} @@ -108,45 +99,36 @@ class _FeedScreenContentState extends State { heroTag: 'recipesTagsFilter', backgroundColor: appTheme.blueA400, shape: const CircleBorder(), - child: const Icon( - Icons.filter_list_rounded, - color: Colors.white, - ), + child: const Icon(Icons.filter_list_rounded, color: Colors.white), onPressed: () async { final recipesCubit = context.read(); await showModalBottomSheet( useRootNavigator: true, context: context, - builder: (context) => TagsModalWidget( - recipesCubit: recipesCubit, - ), + builder: (context) => TagsModalWidget(recipesCubit: recipesCubit), ); recipesCubit.loadRecipesList(); }, ) : null, - appBar: AppBar( - backgroundColor: Colors.white, - toolbarHeight: 0, - ), + appBar: AppBar(backgroundColor: Colors.white, toolbarHeight: 0), backgroundColor: Colors.grey.shade200, body: NestedScrollView( controller: switch (_selectedChipIndex) { 0 => blogPostsScrollController, 1 => recipesScrollController, 2 => dietsScrollController, - 3 => plansScrollController, - 4 => consultantsScrollController, - _ => blogPostsScrollController + 3 when !hidePlans => plansScrollController, + 3 when hidePlans => consultantsScrollController, + 4 when !hidePlans => consultantsScrollController, + _ => blogPostsScrollController, }, headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { return [ BlocBuilder( builder: (context, state) { return SliverAppBar( - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(bottom: Radius.circular(12)), - ), + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(bottom: Radius.circular(12))), backgroundColor: Colors.white, foregroundColor: Colors.white, automaticallyImplyLeading: false, @@ -172,78 +154,71 @@ class _FeedScreenContentState extends State { searchController: switch (_selectedChipIndex) { 1 => recipesController, 2 => dietsController, - 3 => plansController, - 4 => consultantsController, + 3 when !hidePlans => plansController, + 3 when hidePlans => consultantsController, + 4 when !hidePlans => consultantsController, _ => postsController, }, onChanged: (query) { if (query.length > 2) { - EasyDebounce.debounce( - 'searchFeed', - const Duration(milliseconds: 500), - () { - switch (_selectedChipIndex) { - case 0: - context.read().loadFeedBlogPostList(query: query); - break; - case 1: - context.read().loadRecipesList(query: query); - break; - case 2: - context.read().loadDiets(query: query); - break; - case 3: - context.read().loadPlans(query: query); - break; - case 4: - context.read().getConsultants(); - break; - } - }, - ); + EasyDebounce.debounce('searchFeed', const Duration(milliseconds: 500), () { + switch (_selectedChipIndex) { + case 0: + context.read().loadFeedBlogPostList(query: query); + break; + case 1: + context.read().loadRecipesList(query: query); + break; + case 2: + context.read().loadDiets(query: query); + break; + case 3 when !hidePlans: + context.read().loadPlans(query: query); + break; + case 3 when hidePlans: + case 4 when !hidePlans: + context.read().getConsultants(); + break; + } + }); } else { - EasyDebounce.debounce( - 'searchFeed', - const Duration(milliseconds: 500), - () { - switch (_selectedChipIndex) { - case 0: - context.read().loadFeedBlogPostList(); - break; - case 1: - context.read().loadRecipesList(); - break; - case 2: - context.read().loadDiets(); - break; - case 3: - context.read().loadPlans(); - break; - case 4: - context.read().getConsultants(); - break; - } - }, - ); + EasyDebounce.debounce('searchFeed', const Duration(milliseconds: 500), () { + switch (_selectedChipIndex) { + case 0: + context.read().loadFeedBlogPostList(); + break; + case 1: + context.read().loadRecipesList(); + break; + case 2: + context.read().loadDiets(); + break; + case 3 when !hidePlans: + context.read().loadPlans(); + break; + case 3 when hidePlans: + case 4 when !hidePlans: + context.read().getConsultants(); + break; + } + }); } }, ), const SizedBox(width: 12), switch (_selectedChipIndex) { - 1 => AddItemButton( - onTap: () => GoRouter.of(context).pushNamed(RecipeCreateScreen.name), - ), + 1 => AddItemButton(onTap: () => GoRouter.of(context).pushNamed(RecipeCreateScreen.name)), 2 => AddItemButton( - onTap: state.profile?.roles.contains('consultant') ?? false - ? () async { - await GoRouter.of(context).pushNamed(CreateDietScreen.name); - } - : null, - ), - 3 => AddItemButton( - onTap: state.profile?.roles.contains('consultant') ?? false ? () => GoRouter.of(context).pushNamed(CreatePlanScreen.name) : null, - ), - _ => const AddItemButton() + onTap: state.profile?.roles.contains('consultant') ?? false + ? () async { + await GoRouter.of(context).pushNamed(CreateDietScreen.name); + } + : null, + ), + 3 when !hidePlans => AddItemButton( + onTap: state.profile?.roles.contains('consultant') ?? false ? () => GoRouter.of(context).pushNamed(CreatePlanScreen.name) : null, + ), + _ => const AddItemButton(), }, ], ), @@ -256,8 +231,9 @@ class _FeedScreenContentState extends State { 0 => const BlogPosts(), 1 => const Recipes(), 2 => const Diets(), - 3 => const Plans(), - 4 => const Consultants(), + 3 when !hidePlans => const Plans(), + 3 when hidePlans => const Consultants(), + 4 when !hidePlans => const Consultants(), _ => Center(child: Text(chipsLabelList[_selectedChipIndex])), }, ), diff --git a/lib/features/feed/presentation/widgets/chip_group.dart b/lib/features/feed/presentation/widgets/chip_group.dart index a8d88da1..183a5a76 100644 --- a/lib/features/feed/presentation/widgets/chip_group.dart +++ b/lib/features/feed/presentation/widgets/chip_group.dart @@ -2,12 +2,7 @@ import 'package:eatvio_app/features/feed/presentation/widgets/choice_chip_widget import 'package:flutter/widgets.dart'; class ChipGroup extends StatelessWidget { - const ChipGroup({ - super.key, - required this.selectedChipIndex, - required this.chipsLabelList, - required this.setSelectedChipIndex, - }); + const ChipGroup({super.key, required this.selectedChipIndex, required this.chipsLabelList, required this.setSelectedChipIndex}); final int selectedChipIndex; final List chipsLabelList; final void Function(int) setSelectedChipIndex; @@ -22,7 +17,7 @@ class ChipGroup extends StatelessWidget { child: Row( children: [ ...List.generate( - 5, + chipsLabelList.length, (index) => Padding( padding: const EdgeInsets.only(right: 8.0), child: ChoiceChipWidget( diff --git a/lib/features/profile/presentation/pages/profile_screen.dart b/lib/features/profile/presentation/pages/profile_screen.dart index 68213da5..211543a9 100644 --- a/lib/features/profile/presentation/pages/profile_screen.dart +++ b/lib/features/profile/presentation/pages/profile_screen.dart @@ -45,10 +45,7 @@ import 'package:url_launcher/url_launcher.dart'; class ProfileScreen extends StatefulWidget { final Author? author; - const ProfileScreen({ - super.key, - this.author, - }); + const ProfileScreen({super.key, this.author}); @override State createState() => _ProfileScreenState(); @@ -91,7 +88,7 @@ class _ProfileScreenState extends State { } if (isFirstFetch) { canSub = !state.profile.isEntitySubscribedByMe; - if (state.profile.roles.contains('consultant')) { + if (state.profile.roles.contains('consultant') && !hidePlans) { plansCubit.getPlans(state.profile.slug); } isFirstFetch = false; @@ -109,10 +106,7 @@ class _ProfileScreenState extends State { ), ); } else { - return Scaffold( - appBar: AppBar(), - body: const Loader(), - ); + return Scaffold(appBar: AppBar(), body: const Loader()); } }, ), @@ -130,9 +124,7 @@ class _ProfileScreenState extends State { Container( decoration: const BoxDecoration( color: Colors.white, - borderRadius: BorderRadius.vertical( - bottom: Radius.circular(16), - ), + borderRadius: BorderRadius.vertical(bottom: Radius.circular(16)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.center, @@ -147,28 +139,15 @@ class _ProfileScreenState extends State { const SizedBox(width: 16), const Text( 'Консультант', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: Color( - 0xFF323F4B, - ), - ), + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF323F4B)), ), const SizedBox(width: 12), Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - decoration: BoxDecoration( - color: const Color(0xFFF6FAFF), - borderRadius: BorderRadius.circular(12), - ), + decoration: BoxDecoration(color: const Color(0xFFF6FAFF), borderRadius: BorderRadius.circular(12)), child: Text( '${state.consultantInfo!.ratingPlace}-й в рейтинге', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: appTheme.blueA400, - ), + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: appTheme.blueA400), ), ), ], @@ -177,17 +156,10 @@ class _ProfileScreenState extends State { const Row( children: [ SizedBox(width: 16), - Icon( - Icons.done_rounded, - color: Colors.green, - ), + Icon(Icons.done_rounded, color: Colors.green), Text( 'Медицинское образование', - style: TextStyle( - color: Color(0xFF323F4B), - fontSize: 14, - fontWeight: FontWeight.w600, - ), + style: TextStyle(color: Color(0xFF323F4B), fontSize: 14, fontWeight: FontWeight.w600), ), ], ), @@ -205,7 +177,7 @@ class _ProfileScreenState extends State { if (state.profile.roles.contains('consultant') && state.consultantInfo != null) _socialLinks(state), if (state.profile.roles.contains('consultant') && state.consultantInfo != null) _education(state), if (state.profile.roles.contains('consultant') && state.consultantInfo != null) _diplomasAndCertificates(state), - if (state.profile.roles.contains('consultant') && state.consultantInfo != null) _activePlans(state), + if (state.profile.roles.contains('consultant') && state.consultantInfo != null && !hidePlans) _activePlans(state), _blogPosts(state), ], ), @@ -216,9 +188,7 @@ class _ProfileScreenState extends State { Scaffold _myProfileScreen(ProfileStateLoaded state) { return Scaffold( backgroundColor: Colors.grey.shade200, - appBar: AppBar( - toolbarHeight: widget.author == null ? 0 : null, - ), + appBar: AppBar(toolbarHeight: widget.author == null ? 0 : null), body: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -226,9 +196,7 @@ class _ProfileScreenState extends State { Container( decoration: const BoxDecoration( color: Colors.white, - borderRadius: BorderRadius.vertical( - bottom: Radius.circular(16), - ), + borderRadius: BorderRadius.vertical(bottom: Radius.circular(16)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.center, @@ -243,28 +211,15 @@ class _ProfileScreenState extends State { const SizedBox(width: 16), const Text( 'Консультант', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: Color( - 0xFF323F4B, - ), - ), + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF323F4B)), ), const SizedBox(width: 12), Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - decoration: BoxDecoration( - color: const Color(0xFFF6FAFF), - borderRadius: BorderRadius.circular(12), - ), + decoration: BoxDecoration(color: const Color(0xFFF6FAFF), borderRadius: BorderRadius.circular(12)), child: Text( '${state.consultantInfo!.ratingPlace}-й в рейтинге', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: appTheme.blueA400, - ), + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: appTheme.blueA400), ), ), ], @@ -273,17 +228,10 @@ class _ProfileScreenState extends State { const Row( children: [ SizedBox(width: 16), - Icon( - Icons.done_rounded, - color: Colors.green, - ), + Icon(Icons.done_rounded, color: Colors.green), Text( 'Медицинское образование', - style: TextStyle( - color: Color(0xFF323F4B), - fontSize: 14, - fontWeight: FontWeight.w600, - ), + style: TextStyle(color: Color(0xFF323F4B), fontSize: 14, fontWeight: FontWeight.w600), ), ], ), @@ -299,7 +247,7 @@ class _ProfileScreenState extends State { 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')) _activePlans(state), + if (state.profile.roles.contains('consultant') && !hidePlans) _activePlans(state), _newBlogPostButton(), _blogPosts(state), ], @@ -309,7 +257,7 @@ class _ProfileScreenState extends State { } Widget _activePlans(ProfileStateLoaded state) { - if (!state.profile.roles.contains('consultant')) { + if (!state.profile.roles.contains('consultant') || hidePlans) { return const SizedBox.shrink(); } return BlocBuilder( @@ -328,12 +276,7 @@ class _ProfileScreenState extends State { children: [ Text( 'Активные планы', - style: TextStyle( - color: Color(0xFF323F4B), - fontSize: 24, - height: 1, - fontWeight: FontWeight.w600, - ), + style: TextStyle(color: Color(0xFF323F4B), fontSize: 24, height: 1, fontWeight: FontWeight.w600), ), ], ), @@ -343,10 +286,7 @@ class _ProfileScreenState extends State { height: 781, child: PageView.builder( itemBuilder: (BuildContext context, int index) { - return PlanWidget( - plan: state.plansList[index], - index: index, - ); + return PlanWidget(plan: state.plansList[index], index: index); }, onPageChanged: (bullet) { setState(() { @@ -370,17 +310,11 @@ class _ProfileScreenState extends State { Container _bullets(int currentBullet, int bulletCount) { return Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(20), - ), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20)), child: Row( mainAxisSize: MainAxisSize.min, children: [ - for (int i = 0; i < bulletCount; i++) ...[ - _bullet(i == currentBullet), - i < bulletCount - 1 ? const SizedBox(width: 4) : const SizedBox.shrink(), - ], + for (int i = 0; i < bulletCount; i++) ...[_bullet(i == currentBullet), i < bulletCount - 1 ? const SizedBox(width: 4) : const SizedBox.shrink()], ], ), ); @@ -391,10 +325,7 @@ class _ProfileScreenState extends State { width: isActive ? 10 : 8, height: isActive ? 10 : 8, duration: const Duration(milliseconds: 150), - decoration: BoxDecoration( - color: isActive ? appTheme.blueA400 : const Color(0xFFD7D9DC), - shape: BoxShape.circle, - ), + decoration: BoxDecoration(color: isActive ? appTheme.blueA400 : const Color(0xFFD7D9DC), shape: BoxShape.circle), ); } @@ -411,25 +342,15 @@ class _ProfileScreenState extends State { borderRadius: BorderRadius.circular(8), child: Ink( padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: const Color(0xFFF6FAFF), - borderRadius: BorderRadius.circular(8), - ), + decoration: BoxDecoration(color: const Color(0xFFF6FAFF), borderRadius: BorderRadius.circular(8)), child: const Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon( - Icons.add_circle_outline_rounded, - color: Color(0xFF2F80ED), - ), + Icon(Icons.add_circle_outline_rounded, color: Color(0xFF2F80ED)), SizedBox(width: 12), Text( 'Новая запись', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - color: Color(0xFF2F80ED), - ), + style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Color(0xFF2F80ED)), ), ], ), @@ -454,12 +375,7 @@ class _ProfileScreenState extends State { children: [ const Text( 'Дипломы\nи сертификаты', - style: TextStyle( - color: Color(0xFF323F4B), - fontSize: 24, - height: 1, - fontWeight: FontWeight.w600, - ), + style: TextStyle(color: Color(0xFF323F4B), fontSize: 24, height: 1, fontWeight: FontWeight.w600), ), GestureDetector( onTap: () { @@ -467,11 +383,7 @@ class _ProfileScreenState extends State { }, child: Text( widget.author == null || widget.author!.authorIsMe ? 'Редактировать' : '', - style: const TextStyle( - color: Color(0xFF2F80ED), - fontSize: 16, - fontWeight: FontWeight.w500, - ), + style: const TextStyle(color: Color(0xFF2F80ED), fontSize: 16, fontWeight: FontWeight.w500), ), ), ], @@ -491,17 +403,14 @@ class _ProfileScreenState extends State { child: Row( children: [ if (state.consultantInfo!.photos.isNotEmpty) - ...List.generate( - state.consultantInfo!.photos.length * 2 - 1, - (index) { - if (index.isEven) { - final photo = state.consultantInfo!.photos[index ~/ 2]; - return _diplomaImage(photo.url); - } else { - return const SizedBox(width: 8); - } - }, - ), + ...List.generate(state.consultantInfo!.photos.length * 2 - 1, (index) { + if (index.isEven) { + final photo = state.consultantInfo!.photos[index ~/ 2]; + return _diplomaImage(photo.url); + } else { + return const SizedBox(width: 8); + } + }), ], ), ), @@ -515,24 +424,13 @@ class _ProfileScreenState extends State { GestureDetector _diplomaImage(String url) { return GestureDetector( onTap: () { - context.pushTransparentRoute( - rootNavigator: true, - PhotoViewScreen( - imageUrls: [url], - index: 0, - ), - ); + context.pushTransparentRoute(rootNavigator: true, PhotoViewScreen(imageUrls: [url], index: 0)); }, child: Hero( tag: url, child: ClipRRect( borderRadius: BorderRadius.circular(8), - child: CachedNetworkImage( - imageUrl: url, - fit: BoxFit.fitHeight, - width: MediaQuery.of(context).size.width / 3 - (60 / 3), - height: 160, - ), + child: CachedNetworkImage(imageUrl: url, fit: BoxFit.fitHeight, width: MediaQuery.of(context).size.width / 3 - (60 / 3), height: 160), ), ), ); @@ -555,11 +453,7 @@ class _ProfileScreenState extends State { children: [ const Text( 'Образование', - style: TextStyle( - color: Color(0xFF323F4B), - fontSize: 24, - fontWeight: FontWeight.w600, - ), + style: TextStyle(color: Color(0xFF323F4B), fontSize: 24, fontWeight: FontWeight.w600), ), GestureDetector( onTap: () { @@ -567,11 +461,7 @@ class _ProfileScreenState extends State { }, child: Text( widget.author == null || widget.author!.authorIsMe ? 'Редактировать' : '', - style: const TextStyle( - color: Color(0xFF2F80ED), - fontSize: 16, - fontWeight: FontWeight.w500, - ), + style: const TextStyle(color: Color(0xFF2F80ED), fontSize: 16, fontWeight: FontWeight.w500), ), ), ], @@ -590,8 +480,10 @@ class _ProfileScreenState extends State { ListView.separated( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), - itemBuilder: (context, index) => _educationBlock(state.consultantInfo!.educationModel.completedCourses![index].values.first.title, - state.consultantInfo!.educationModel.completedCourses![index].values.first.date), + itemBuilder: (context, index) => _educationBlock( + state.consultantInfo!.educationModel.completedCourses![index].values.first.title, + state.consultantInfo!.educationModel.completedCourses![index].values.first.date, + ), separatorBuilder: (context, index) => const SizedBox(height: 8), itemCount: state.consultantInfo!.educationModel.completedCourses!.length, ), @@ -621,22 +513,12 @@ class _ProfileScreenState extends State { children: [ Text( title, - style: const TextStyle( - height: 1, - color: Color(0xFF323F4B), - fontWeight: FontWeight.w500, - fontSize: 16, - ), + style: const TextStyle(height: 1, color: Color(0xFF323F4B), fontWeight: FontWeight.w500, fontSize: 16), ), const SizedBox(height: 4), Text( formattedDate, - style: const TextStyle( - height: 1, - color: Color(0xFF6D6F71), - fontWeight: FontWeight.w500, - fontSize: 14, - ), + style: const TextStyle(height: 1, color: Color(0xFF6D6F71), fontWeight: FontWeight.w500, fontSize: 14), ), ], ), @@ -660,12 +542,7 @@ class _ProfileScreenState extends State { children: [ _socialLinkButton( () { - launchUrl( - Uri.parse( - state.consultantInfo!.educationModel.vkLink ?? '', - ), - mode: LaunchMode.externalApplication, - ); + launchUrl(Uri.parse(state.consultantInfo!.educationModel.vkLink ?? ''), mode: LaunchMode.externalApplication); }, 'assets/icons/vk.svg', state.consultantInfo!.educationModel.vkLink!.isEmpty ? true : false, @@ -673,12 +550,7 @@ class _ProfileScreenState extends State { const SizedBox(width: 12), _socialLinkButton( () { - launchUrl( - Uri.parse( - state.consultantInfo!.educationModel.tgLink ?? '', - ), - mode: LaunchMode.externalApplication, - ); + launchUrl(Uri.parse(state.consultantInfo!.educationModel.tgLink ?? ''), mode: LaunchMode.externalApplication); }, 'assets/icons/telegram.svg', state.consultantInfo!.educationModel.tgLink!.isEmpty ? true : false, @@ -686,12 +558,7 @@ class _ProfileScreenState extends State { const SizedBox(width: 12), _socialLinkButton( () { - launchUrl( - Uri.parse( - state.consultantInfo!.educationModel.personalSiteLink ?? '', - ), - mode: LaunchMode.externalApplication, - ); + launchUrl(Uri.parse(state.consultantInfo!.educationModel.personalSiteLink ?? ''), mode: LaunchMode.externalApplication); }, 'assets/icons/web.svg', state.consultantInfo!.educationModel.personalSiteLink!.isEmpty ? true : false, @@ -709,10 +576,7 @@ class _ProfileScreenState extends State { borderRadius: BorderRadius.circular(20), child: Ink( width: MediaQuery.of(context).size.width / 3 - (56 / 3), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - color: const Color(0xFFF6F6F6), - ), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: const Color(0xFFF6F6F6)), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 16), child: SvgPicture.asset( @@ -734,20 +598,13 @@ class _ProfileScreenState extends State { child: Ink( padding: const EdgeInsets.all(8), width: MediaQuery.of(context).size.width, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - color: Colors.white, - ), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(12), color: Colors.white), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Достижения', - style: TextStyle( - color: Color(0xFF6D6F71), - fontSize: 14, - fontWeight: FontWeight.w600, - ), + style: TextStyle(color: Color(0xFF6D6F71), fontSize: 14, fontWeight: FontWeight.w600), ), const SizedBox(height: 8), SingleChildScrollView( @@ -759,51 +616,25 @@ class _ProfileScreenState extends State { ? Tooltip( message: state.profile.badges[i].description, triggerMode: TooltipTriggerMode.tap, - textStyle: const TextStyle( - color: Colors.black, - fontSize: 16, - fontWeight: FontWeight.w500, - ), + textStyle: const TextStyle(color: Colors.black, fontSize: 16, fontWeight: FontWeight.w500), decoration: BoxDecoration( color: Colors.white, 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, + 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), ) : Tooltip( message: state.profile.badges[i].description, margin: const EdgeInsets.symmetric(horizontal: 32), triggerMode: TooltipTriggerMode.tap, - textStyle: const TextStyle( - color: Colors.black, - fontSize: 16, - fontWeight: FontWeight.w500, - ), + textStyle: const TextStyle(color: Colors.black, fontSize: 16, fontWeight: FontWeight.w500), decoration: BoxDecoration( color: Colors.white, 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, + 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), ), i < state.profile.badges.length - 1 ? const SizedBox(width: 8) : const SizedBox.shrink(), ], @@ -823,21 +654,12 @@ class _ProfileScreenState extends State { child: Row( children: [ InkWell( - onTap: () => GoRouter.of(context).pushNamed( - 'subsScreen', - extra: SubType.subscriptions, - pathParameters: { - 'slug': state.profile.slug, - }, - ), + onTap: () => GoRouter.of(context).pushNamed('subsScreen', extra: SubType.subscriptions, pathParameters: {'slug': state.profile.slug}), borderRadius: BorderRadius.circular(12), child: Ink( padding: const EdgeInsets.all(8), width: MediaQuery.of(context).size.width / 2 - 6, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - color: Colors.white, - ), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(12), color: Colors.white), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.end, @@ -847,47 +669,27 @@ class _ProfileScreenState extends State { children: [ const Text( 'Подписки', - style: TextStyle( - color: Color(0xFF6D6F71), - fontSize: 14, - fontWeight: FontWeight.w600, - ), + style: TextStyle(color: Color(0xFF6D6F71), fontSize: 14, fontWeight: FontWeight.w600), ), Text( '${state.profile.subscriptions.length}', - style: const TextStyle( - color: Color(0xFF323F4B), - fontSize: 22, - fontWeight: FontWeight.w700, - ), + style: const TextStyle(color: Color(0xFF323F4B), fontSize: 22, fontWeight: FontWeight.w700), ), ], ), - Padding( - padding: const EdgeInsets.only(right: 8), - child: _miniAvatars(state.profile.subscriptions), - ), + Padding(padding: const EdgeInsets.only(right: 8), child: _miniAvatars(state.profile.subscriptions)), ], ), ), ), const SizedBox(width: 4), InkWell( - onTap: () => GoRouter.of(context).pushNamed( - 'subsScreen', - extra: SubType.subscribers, - pathParameters: { - 'slug': state.profile.slug, - }, - ), + onTap: () => GoRouter.of(context).pushNamed('subsScreen', extra: SubType.subscribers, pathParameters: {'slug': state.profile.slug}), borderRadius: BorderRadius.circular(12), child: Ink( padding: const EdgeInsets.all(8), width: MediaQuery.of(context).size.width / 2 - 6, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - color: Colors.white, - ), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(12), color: Colors.white), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.end, @@ -897,26 +699,15 @@ class _ProfileScreenState extends State { children: [ const Text( 'Подписчики', - style: TextStyle( - color: Color(0xFF6D6F71), - fontSize: 14, - fontWeight: FontWeight.w600, - ), + style: TextStyle(color: Color(0xFF6D6F71), fontSize: 14, fontWeight: FontWeight.w600), ), Text( '${state.profile.subscribers.length}', - style: const TextStyle( - color: Color(0xFF323F4B), - fontSize: 22, - fontWeight: FontWeight.w700, - ), + style: const TextStyle(color: Color(0xFF323F4B), fontSize: 22, fontWeight: FontWeight.w700), ), ], ), - Padding( - padding: const EdgeInsets.only(right: 8), - child: _miniAvatars(state.profile.subscribers), - ), + Padding(padding: const EdgeInsets.only(right: 8), child: _miniAvatars(state.profile.subscribers)), ], ), ), @@ -934,13 +725,7 @@ class _ProfileScreenState extends State { for (int i = 0; i < maxLength; i++) Align( widthFactor: 0.8, - child: CircleAvatar( - radius: 14, - foregroundImage: NetworkImage( - urls[i], - ), - backgroundImage: const AssetImage('assets/images/no_avatar.png'), - ), + child: CircleAvatar(radius: 14, foregroundImage: NetworkImage(urls[i]), backgroundImage: const AssetImage('assets/images/no_avatar.png')), ), ], ); @@ -951,10 +736,7 @@ class _ProfileScreenState extends State { padding: const EdgeInsets.symmetric(vertical: 16), shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), - itemBuilder: (context, index) => ProfileBlogPostWidget( - index: index, - profileCubit: profileCubit, - ), + itemBuilder: (context, index) => ProfileBlogPostWidget(index: index, profileCubit: profileCubit), separatorBuilder: (context, index) => const SizedBox(height: 8), itemCount: state.blogPosts.length, ); @@ -968,13 +750,7 @@ class _ProfileScreenState extends State { return Container( alignment: Alignment.centerLeft, padding: const EdgeInsets.all(16), - child: Text( - text, - style: TextStyle( - color: Colors.black.withValues(alpha: 0.4), - height: 1, - ), - ), + child: Text(text, style: TextStyle(color: Colors.black.withValues(alpha: 0.4), height: 1)), ); } @@ -986,13 +762,7 @@ class _ProfileScreenState extends State { PopupMenuButton( onSelected: (value) async { if (value == 'view') { - context.pushTransparentRoute( - rootNavigator: true, - PhotoViewScreen( - imageUrls: [baseUrl + state.profile.profilePhotoUrl], - index: 0, - ), - ); + context.pushTransparentRoute(rootNavigator: true, PhotoViewScreen(imageUrls: [baseUrl + state.profile.profilePhotoUrl], index: 0)); } if (value == 'replace') { showCupertinoModalPopup( @@ -1019,13 +789,7 @@ class _ProfileScreenState extends State { context: context, ); }, - child: const Row( - children: [ - Icon(Icons.photo_camera), - SizedBox(width: 8), - Text('Фото'), - ], - ), + child: const Row(children: [Icon(Icons.photo_camera), SizedBox(width: 8), Text('Фото')]), ), CupertinoActionSheetAction( onPressed: () async { @@ -1045,13 +809,7 @@ class _ProfileScreenState extends State { context: context, ); }, - child: const Row( - children: [ - Icon(Icons.photo), - SizedBox(width: 8), - Text('Галерея'), - ], - ), + child: const Row(children: [Icon(Icons.photo), SizedBox(width: 8), Text('Галерея')]), ), ], cancelButton: CupertinoActionSheetAction( @@ -1069,20 +827,9 @@ class _ProfileScreenState extends State { } }, itemBuilder: (context) => [ - const PopupMenuItem( - value: 'view', - child: Text('Посмотреть'), - ), - if (widget.author == null || widget.author!.authorIsMe) - const PopupMenuItem( - value: 'replace', - child: Text('Заменить'), - ), - if (widget.author == null || widget.author!.authorIsMe) - const PopupMenuItem( - value: 'delete', - child: Text('Удалить'), - ), + const PopupMenuItem(value: 'view', child: Text('Посмотреть')), + if (widget.author == null || widget.author!.authorIsMe) const PopupMenuItem(value: 'replace', child: Text('Заменить')), + if (widget.author == null || widget.author!.authorIsMe) const PopupMenuItem(value: 'delete', child: Text('Удалить')), ], child: Hero( tag: state.profile.profilePhotoUrl, @@ -1101,12 +848,7 @@ class _ProfileScreenState extends State { children: [ Text( '${state.profileDetails.firstName} ${state.profileDetails.lastName}', - style: const TextStyle( - color: Colors.black, - fontWeight: FontWeight.w600, - fontSize: 16, - height: 1, - ), + style: const TextStyle(color: Colors.black, fontWeight: FontWeight.w600, fontSize: 16, height: 1), ), const SizedBox(height: 8), GestureDetector( @@ -1124,11 +866,7 @@ class _ProfileScreenState extends State { children: [ const Text( 'Подробнее', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w700, - color: Color(0xFF323F4B), - ), + style: TextStyle(fontSize: 20, fontWeight: FontWeight.w700, color: Color(0xFF323F4B)), ), const SizedBox(height: 16), _additional('Дата рожденя: ', state.profileDetails.birthdate), @@ -1148,19 +886,11 @@ class _ProfileScreenState extends State { }, child: const Row( children: [ - Icon( - Icons.info_outline, - size: 20, - color: Color(0xFF6D6F71), - ), + Icon(Icons.info_outline, size: 20, color: Color(0xFF6D6F71)), SizedBox(width: 4), Text( 'Дополнительно', - style: TextStyle( - color: Color(0xFF6D6F71), - fontSize: 14, - fontWeight: FontWeight.w500, - ), + style: TextStyle(color: Color(0xFF6D6F71), fontSize: 14, fontWeight: FontWeight.w500), ), ], ), @@ -1173,10 +903,7 @@ class _ProfileScreenState extends State { if (value == 'share') { 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('$baseUrl/ru/profile/${state.profile.slug}'), sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size), ); } else if (value == 'block') { isFirstFetch = true; @@ -1198,33 +925,15 @@ class _ProfileScreenState extends State { showModalBottomSheet( context: context, isScrollControlled: true, - builder: (context) => ComplainingBottomSheet( - complainedContentType: ComplainedContent.profile, - slug: widget.author?.authorSlug, - ), + builder: (context) => ComplainingBottomSheet(complainedContentType: ComplainedContent.profile, slug: widget.author?.authorSlug), ); } }, itemBuilder: (context) => [ - const PopupMenuItem( - value: 'share', - child: Text('Поделиться'), - ), - if (widget.author != null) - const PopupMenuItem( - value: 'complain', - child: Text('Пожаловаться'), - ), - if (widget.author != null && !state.profile.isBlockedByMe) - const PopupMenuItem( - value: 'block', - child: Text('Заблокировать'), - ), - if (widget.author != null && state.profile.isBlockedByMe) - const PopupMenuItem( - value: 'unblock', - child: Text('Разблокировать'), - ), + const PopupMenuItem(value: 'share', child: Text('Поделиться')), + if (widget.author != null) const PopupMenuItem(value: 'complain', child: Text('Пожаловаться')), + if (widget.author != null && !state.profile.isBlockedByMe) const PopupMenuItem(value: 'block', child: Text('Заблокировать')), + if (widget.author != null && state.profile.isBlockedByMe) const PopupMenuItem(value: 'unblock', child: Text('Разблокировать')), ], child: const Icon(Icons.more_horiz_rounded), ), @@ -1238,19 +947,11 @@ class _ProfileScreenState extends State { children: [ Text( title, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - color: Color(0xFF6D6F71), - ), + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500, color: Color(0xFF6D6F71)), ), Text( value, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - color: Color(0xFF323F4B), - ), + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500, color: Color(0xFF323F4B)), ), ], ); @@ -1266,16 +967,8 @@ class _ProfileScreenState extends State { alignment: Alignment.center, padding: const EdgeInsets.all(8), width: (MediaQuery.of(context).size.width - 40) / 2, - decoration: BoxDecoration( - color: appTheme.blueA400.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(16), - ), - child: Text( - 'Сообщение', - style: TextStyle( - color: appTheme.blueA400, - ), - ), + decoration: BoxDecoration(color: appTheme.blueA400.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(16)), + child: Text('Сообщение', style: TextStyle(color: appTheme.blueA400)), ), ), const SizedBox(width: 8), @@ -1290,16 +983,8 @@ class _ProfileScreenState extends State { alignment: Alignment.center, padding: const EdgeInsets.all(8), width: (MediaQuery.of(context).size.width - 40) / 2, - decoration: BoxDecoration( - color: !canSub ? Colors.grey.shade200 : appTheme.blueA400, - borderRadius: BorderRadius.circular(16), - ), - child: Text( - !canSub ? 'Отписаться' : 'Подписаться', - style: TextStyle( - color: !canSub ? Colors.black : Colors.white, - ), - ), + decoration: BoxDecoration(color: !canSub ? Colors.grey.shade200 : appTheme.blueA400, borderRadius: BorderRadius.circular(16)), + child: Text(!canSub ? 'Отписаться' : 'Подписаться', style: TextStyle(color: !canSub ? Colors.black : Colors.white)), ), ), const SizedBox(width: 16), @@ -1318,33 +1003,17 @@ class _ProfileScreenState extends State { SingleChildScrollView( padding: const EdgeInsets.symmetric(horizontal: 16), scrollDirection: Axis.horizontal, - child: Row( - children: [ - _choiceChipWidget(0), - const SizedBox(width: 8), - _choiceChipWidget(1), - const SizedBox(width: 8), - _choiceChipWidget(2), - ], - ), + child: Row(children: [_choiceChipWidget(0), const SizedBox(width: 8), _choiceChipWidget(1), const SizedBox(width: 8), _choiceChipWidget(2)]), ), Builder( builder: (context) { switch (_selectedChipIndex) { case 0: - return WeightChartWidget( - weightPoints: weightPoints, - profileCubit: profileCubit, - ); + return WeightChartWidget(weightPoints: weightPoints, profileCubit: profileCubit); case 1: - return CircumferenceChartWidget( - circumferencePoints: circumferencePoints, - profileCubit: profileCubit, - ); + return CircumferenceChartWidget(circumferencePoints: circumferencePoints, profileCubit: profileCubit); case 2: - return ImtChartWidget( - weightPoints: weightPoints, - ); + return ImtChartWidget(weightPoints: weightPoints); default: return const SizedBox.shrink(); } @@ -1377,16 +1046,10 @@ class _ProfileScreenState 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: 8, vertical: 4), label: Text(chipsIntervalLabelList[index]), - labelStyle: TextStyle( - color: appTheme.gray600, - fontWeight: FontWeight.w500, - fontSize: 12, - ), + labelStyle: TextStyle(color: appTheme.gray600, fontWeight: FontWeight.w500, fontSize: 12), selected: _selectedIntervalChipIndex == index, onSelected: (bool selected) { profileCubit.getMyProfile(period: chipsIntervalList[index]); @@ -1403,16 +1066,10 @@ class _ProfileScreenState 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: 8, vertical: 4), label: Text(chipsLabelList[index]), - labelStyle: TextStyle( - color: index == _selectedChipIndex ? Colors.white : appTheme.gray600, - fontWeight: FontWeight.w500, - fontSize: 12, - ), + labelStyle: TextStyle(color: index == _selectedChipIndex ? Colors.white : appTheme.gray600, fontWeight: FontWeight.w500, fontSize: 12), selected: _selectedChipIndex == index, backgroundColor: appTheme.gray100, onSelected: (bool selected) { From 49b597d5ae6edd21c9363389f5f08d1026684405 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Fri, 1 Aug 2025 11:30:58 +0300 Subject: [PATCH 046/134] fix: update versions --- android/app/build.gradle | 6 ++--- android/gradle.properties | 2 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- android/settings.gradle | 2 +- pubspec.yaml | 26 +++++++++---------- 5 files changed, 19 insertions(+), 19 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 274f5a87..c9fe6d27 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -19,12 +19,12 @@ android { ndkVersion = "27.0.12077973" compileOptions { - sourceCompatibility = JavaVersion.VERSION_21 - targetCompatibility = JavaVersion.VERSION_21 + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 } kotlinOptions { - jvmTarget = 21 + jvmTarget = JavaVersion.VERSION_11.toString() } defaultConfig { diff --git a/android/gradle.properties b/android/gradle.properties index 25971708..f018a618 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,3 +1,3 @@ -org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError android.useAndroidX=true android.enableJetifier=true diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index f587a472..ac3b4792 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip diff --git a/android/settings.gradle b/android/settings.gradle index 833c476b..29eab0d3 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -22,7 +22,7 @@ plugins { // START: FlutterFire Configuration id "com.google.gms.google-services" version "4.4.2" apply false // END: FlutterFire Configuration - id "org.jetbrains.kotlin.android" version "2.0.21" apply false + id "org.jetbrains.kotlin.android" version "2.1.0" apply false } include ":app" diff --git a/pubspec.yaml b/pubspec.yaml index bda9d6a9..fe27d891 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -18,28 +18,28 @@ dependencies: cached_network_image: ^3.4.1 flutter_svg: ^2.2.0 equatable: ^2.0.7 - freezed_annotation: ^3.0.0 + freezed_annotation: ^3.1.0 json_annotation: ^4.9.0 validators: ^3.0.0 path_provider: ^2.1.5 carousel_slider_plus: ^7.1.1 - flutter_form_builder: ^10.0.1 + flutter_form_builder: ^10.1.0 image_picker: ^1.1.2 flutter_typeahead: ^5.2.0 go_router: ^16.0.0 keyboard_dismisser: ^3.0.0 - retrofit: ^4.5.0 - get_it: ^8.0.3 + retrofit: ^4.7.0 + get_it: ^8.1.0 dio: ^5.8.0+1 dart_pusher_channels: ^1.2.3 photo_view: ^0.15.0 dismissible_page: ^1.0.2 - url_launcher: ^6.3.1 + url_launcher: ^6.3.2 easy_debounce: ^2.0.3 badges: ^3.1.2 google_fonts: ^6.2.1 - syncfusion_flutter_charts: ^30.1.38 - syncfusion_flutter_gauges: ^30.1.38 + syncfusion_flutter_charts: ^30.1.42 + syncfusion_flutter_gauges: ^30.1.42 table_calendar: ^3.2.0 flutter_keyboard_visibility: ^6.0.0 share_plus: ^11.0.0 @@ -58,7 +58,7 @@ dependencies: http: ^1.4.0 flutter_native_splash: ^2.4.6 webview_flutter: ^4.13.0 - sentry_flutter: ^8.14.2 + sentry_flutter: ^9.5.0 connectivity_plus: ^6.1.4 visibility_detector: ^0.4.0+2 firebase_core: ^4.0.0 @@ -70,15 +70,15 @@ dependencies: version: ^3.0.2 package_info_plus: ^8.3.0 device_info_plus: ^11.5.0 - permission_handler: ^12.0.0+1 + permission_handler: ^12.0.1 qr_code_scanner_plus: ^2.0.10+1 dev_dependencies: - build_runner: ^2.5.4 - freezed: ^3.0.6 - json_serializable: ^6.9.5 + build_runner: ^2.6.0 + freezed: ^3.2.0 + json_serializable: ^6.10.0 retrofit_generator: ^10.0.1 - mockito: ^5.4.6 + mockito: ^5.5.0 flutter_test: sdk: flutter From 83a39835fa6a7aaf48c2a5dbe0017cfb9a95f8c2 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Fri, 1 Aug 2025 10:34:39 +0000 Subject: [PATCH 047/134] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20ios/fastlane/Fastfile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ios/fastlane/Fastfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios/fastlane/Fastfile b/ios/fastlane/Fastfile index 4073b355..ad6e4c04 100644 --- a/ios/fastlane/Fastfile +++ b/ios/fastlane/Fastfile @@ -18,7 +18,7 @@ default_platform(:ios) platform :ios do desc "Push a new beta build to TestFlight" lane :beta do - match(type: "appstore") + match(type: "appstore", readonly: true) increment_build_number(xcodeproj: "Runner.xcodeproj") build_app(workspace: "Runner.xcworkspace", scheme: "Runner", export_method: "app-store", codesigning_identity: "Apple Distribution") upload_to_testflight From 6f1368f3c698890ae9f9bad4c00cc3c1691db275 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Fri, 1 Aug 2025 11:11:21 +0000 Subject: [PATCH 048/134] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20.gitea/workflows/test-flight-cd.yaml?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/test-flight-cd.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitea/workflows/test-flight-cd.yaml b/.gitea/workflows/test-flight-cd.yaml index ba101d47..9f2a756e 100644 --- a/.gitea/workflows/test-flight-cd.yaml +++ b/.gitea/workflows/test-flight-cd.yaml @@ -20,6 +20,7 @@ jobs: - name: Flutter build run: | + echo "${API_APPSTORE_KEY}" | base64 --decode > ./AuthKey.p8 flutter clean flutter pub get flutter pub upgrade @@ -34,4 +35,5 @@ jobs: 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 }} \ No newline at end of file + FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: ${{ vars.FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD }} + API_APPSTORE_KEY: ${{ vars.API_APPSTORE_KEY }} \ No newline at end of file From a4d0fd0108d3efb75638aef852324cccb6e5358d Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Fri, 1 Aug 2025 11:12:46 +0000 Subject: [PATCH 049/134] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20ios/fastlane/Fastfile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ios/fastlane/Fastfile | 1 + 1 file changed, 1 insertion(+) diff --git a/ios/fastlane/Fastfile b/ios/fastlane/Fastfile index ad6e4c04..9340117d 100644 --- a/ios/fastlane/Fastfile +++ b/ios/fastlane/Fastfile @@ -21,6 +21,7 @@ platform :ios do match(type: "appstore", readonly: true) increment_build_number(xcodeproj: "Runner.xcodeproj") build_app(workspace: "Runner.xcworkspace", scheme: "Runner", export_method: "app-store", codesigning_identity: "Apple Distribution") + app_store_connect_api_key(key_id: "325JS9JCZH", issuer_id: "dfa27d1d-2bb8-4d4d-847e-0b32f9b880b3", key_filepath: "./AuthKey.p8") upload_to_testflight end end From f78ccd01b6e2086ecebb5db8897003e1cc6da133 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Fri, 1 Aug 2025 11:18:13 +0000 Subject: [PATCH 050/134] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20.gitea/workflows/test-flight-cd.yaml?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/test-flight-cd.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitea/workflows/test-flight-cd.yaml b/.gitea/workflows/test-flight-cd.yaml index 9f2a756e..4320ecc5 100644 --- a/.gitea/workflows/test-flight-cd.yaml +++ b/.gitea/workflows/test-flight-cd.yaml @@ -20,7 +20,7 @@ jobs: - name: Flutter build run: | - echo "${API_APPSTORE_KEY}" | base64 --decode > ./AuthKey.p8 + echo "${API_APPSTORE_KEY}" | base64 --decode > ios/AuthKey.p8 flutter clean flutter pub get flutter pub upgrade From 3e7202d65e541e608223724a0af2589bae8c4368 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Fri, 1 Aug 2025 14:28:04 +0300 Subject: [PATCH 051/134] upd ver --- 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 23adc916..9a27847e 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -396,7 +396,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 62; + CURRENT_PROJECT_VERSION = 64; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -546,7 +546,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 62; + CURRENT_PROJECT_VERSION = 64; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -584,7 +584,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 62; + CURRENT_PROJECT_VERSION = 64; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index f5570627..2341e63f 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -21,7 +21,7 @@ CFBundleSignature ???? CFBundleVersion - 62 + 64 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/pubspec.yaml b/pubspec.yaml index fe27d891..057a2d2d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: eatvio_app description: A new Flutter project. -version: 1.2.6+60 +version: 1.2.6+64 publish_to: 'none' environment: From c718875b490c8b07eb5311ff8e8c7b4e4722c85a Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Fri, 1 Aug 2025 14:33:52 +0300 Subject: [PATCH 052/134] upd ver --- pubspec.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pubspec.yaml b/pubspec.yaml index 057a2d2d..0dd1a2da 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -61,8 +61,8 @@ dependencies: sentry_flutter: ^9.5.0 connectivity_plus: ^6.1.4 visibility_detector: ^0.4.0+2 - firebase_core: ^4.0.0 - firebase_messaging: ^16.0.0 + firebase_core: ^3.15.2 + firebase_messaging: ^15.2.10 http_parser: ^4.1.2 path: ^1.9.1 collection: ^1.19.1 From 3cc7809210ad5411f879eeb102eccf4232e38317 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Fri, 1 Aug 2025 16:31:05 +0300 Subject: [PATCH 053/134] fix: hidePlans to env --- .gitea/workflows/test.yml | 2 +- ios/fastlane/Fastfile | 8 +++++++- lib/core/const.dart | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml index fd291367..2aeef03a 100644 --- a/.gitea/workflows/test.yml +++ b/.gitea/workflows/test.yml @@ -119,7 +119,7 @@ jobs: run: flutter test - name: Build apk - run: flutter build apk --release + run: flutter build apk --release --dart-define=HIDE_PLANS=false - name: Rename and move apk run: | diff --git a/ios/fastlane/Fastfile b/ios/fastlane/Fastfile index 9340117d..7dff33a3 100644 --- a/ios/fastlane/Fastfile +++ b/ios/fastlane/Fastfile @@ -20,7 +20,13 @@ platform :ios do lane :beta do match(type: "appstore", readonly: true) increment_build_number(xcodeproj: "Runner.xcodeproj") - build_app(workspace: "Runner.xcworkspace", scheme: "Runner", export_method: "app-store", codesigning_identity: "Apple Distribution") + build_app( + workspace: "Runner.xcworkspace", + scheme: "Runner", + export_method: "app-store", + codesigning_identity: "Apple Distribution", + build_args: "--dart-define=HIDE_PLANS=true" + ) app_store_connect_api_key(key_id: "325JS9JCZH", issuer_id: "dfa27d1d-2bb8-4d4d-847e-0b32f9b880b3", key_filepath: "./AuthKey.p8") upload_to_testflight end diff --git a/lib/core/const.dart b/lib/core/const.dart index f22f79aa..ad841863 100644 --- a/lib/core/const.dart +++ b/lib/core/const.dart @@ -1,5 +1,5 @@ String baseUrl = 'https://eatvio.ru'; -bool hidePlans = true; +bool hidePlans = const bool.fromEnvironment('HIDE_PLANS', defaultValue: false); const List durationValues = [7, 14, 21]; From 3e5cc4f8d7012a5b036c24293ff8fc0a273ace2c Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Fri, 1 Aug 2025 16:52:03 +0300 Subject: [PATCH 054/134] feat(env): Issue-588 --- .gitignore | 1 + ENV_SETUP.md | 82 +++++++++++++++++++ lib/core/const.dart | 4 +- .../presentation/dev_tools_screen.dart | 42 +++++----- lib/features/dev_tools/test_cases.dart | 5 +- .../internet_connection_checker_screen.dart | 31 ++----- .../repositories/registration_repository.dart | 3 +- .../presentation/pages/support_screen.dart | 73 +++-------------- .../presentation/update_screen.dart | 43 ++-------- lib/firebase_options.dart | 38 ++++----- lib/main.dart | 18 ++-- pubspec.yaml | 1 + 12 files changed, 168 insertions(+), 173 deletions(-) create mode 100644 ENV_SETUP.md diff --git a/.gitignore b/.gitignore index bbb74115..9d5ba6c3 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ *.mocks.dart android/build/ ios/XCBuildData/ +.env diff --git a/ENV_SETUP.md b/ENV_SETUP.md new file mode 100644 index 00000000..f57b9796 --- /dev/null +++ b/ENV_SETUP.md @@ -0,0 +1,82 @@ +# Настройка переменных окружения + +Этот проект использует переменные окружения для хранения секретных данных и конфигурации. + +## Установка + +1. Скопируйте файл `env.example` в `.env`: +```bash +cp env.example .env +``` + +2. Отредактируйте файл `.env` и замените значения на ваши собственные секреты. + +## Переменные окружения + +### 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 + +### Telegram Support +- `TELEGRAM_SUPPORT_URL` - URL поддержки в Telegram + +### 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) + +## Безопасность + +⚠️ **ВАЖНО**: Файл `.env` добавлен в `.gitignore` и не должен попадать в репозиторий. Все секретные данные должны храниться только в этом файле. + +## Использование в коде + +Для доступа к переменным окружения используйте класс `EnvConfig`: + +```dart +import 'package:eatvio_app/core/env_config.dart'; + +String baseUrl = EnvConfig.baseUrl; +String sentryDsn = EnvConfig.sentryDsn; +``` + +## Миграция + +Все секреты были перенесены из кода в переменные окружения: + +1. **Sentry DSN** - из `main.dart` +2. **Firebase конфигурация** - из `firebase_options.dart` +3. **API URLs** - из `const.dart` +4. **URL для проверки соединения** - из `internet_connection_checker_screen.dart` +5. **URL для обновления приложения** - из `update_screen.dart` +6. **URL поддержки Telegram** - из `support_screen.dart` +7. **API для валидации email** - из `registration_repository.dart` +8. **Тестовые учетные данные** - из `test_cases.dart` \ No newline at end of file diff --git a/lib/core/const.dart b/lib/core/const.dart index ad841863..efed7d49 100644 --- a/lib/core/const.dart +++ b/lib/core/const.dart @@ -1,4 +1,6 @@ -String baseUrl = 'https://eatvio.ru'; +import 'package:eatvio_app/core/env_config.dart'; + +String baseUrl = EnvConfig.baseUrl; bool hidePlans = const bool.fromEnvironment('HIDE_PLANS', defaultValue: false); const List durationValues = [7, 14, 21]; diff --git a/lib/features/dev_tools/presentation/dev_tools_screen.dart b/lib/features/dev_tools/presentation/dev_tools_screen.dart index a4ff7730..4922a87d 100644 --- a/lib/features/dev_tools/presentation/dev_tools_screen.dart +++ b/lib/features/dev_tools/presentation/dev_tools_screen.dart @@ -9,6 +9,7 @@ import 'package:eatvio_app/core/utils/toasts.dart'; import 'package:eatvio_app/core/widgets/custom_outlined_button.dart'; import 'package:eatvio_app/core/widgets/custom_text_form_field.dart'; import 'package:eatvio_app/features/dev_tools/test_cases.dart'; +import 'package:eatvio_app/core/env_config.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -97,10 +98,15 @@ class _DevToolsScreenContentState extends State { padding: const EdgeInsets.symmetric(horizontal: 10), child: Column( children: [ - Padding(padding: const EdgeInsets.all(8.0), child: Row(children: [Text('BaseUrl: $baseUrlForTest')])), + Padding( + padding: const EdgeInsets.all(8.0), + child: Row(children: [Text('BaseUrl: $baseUrlForTest')]), + ), Row( children: [ - Expanded(child: CustomTextFormField(controller: controller, hintText: 'type new base url')), + Expanded( + child: CustomTextFormField(controller: controller, hintText: 'type new base url'), + ), IconButton( onPressed: () { setState(() { @@ -116,13 +122,13 @@ class _DevToolsScreenContentState extends State { ), const SizedBox(height: 10), CustomOutlinedButton( - text: baseUrl == 'https://dev2.eatvio.com' ? 'Set production base url' : 'Set dev2 base url', + text: baseUrl == EnvConfig.devBaseUrl ? 'Set production base url' : 'Set dev2 base url', onPressed: () { setState(() { - if (baseUrl == 'https://eatvio.ru') { - baseUrlForTest = 'https://dev2.eatvio.com'; + if (baseUrl == EnvConfig.baseUrl) { + baseUrlForTest = EnvConfig.devBaseUrl; } else { - baseUrlForTest = 'https://eatvio.ru'; + baseUrlForTest = EnvConfig.baseUrl; } baseUrl = baseUrlForTest.toLowerCase(); sl().options.baseUrl = baseUrl; @@ -132,7 +138,7 @@ class _DevToolsScreenContentState extends State { buttonStyle: const ButtonStyle(), ), SizedBox(height: baseUrlForTest != baseUrl ? 40 : 10), - if (baseUrl.toLowerCase() != 'https://eatvio.ru') + if (baseUrl.toLowerCase() != EnvConfig.baseUrl.toLowerCase()) ListView.builder( shrinkWrap: true, itemCount: testCases.length, @@ -175,12 +181,7 @@ class _DevToolsScreenContentState extends State { builder: (context) => AlertDialog( title: const Text('Test Failed'), content: SingleChildScrollView(child: Text(result.error ?? 'No error details.')), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text('OK'), - ), - ], + actions: [TextButton(onPressed: () => Navigator.of(context).pop(), child: const Text('OK'))], ), ); } @@ -197,12 +198,7 @@ class _DevToolsScreenContentState extends State { builder: (context) => AlertDialog( title: const Text('Test Failed'), content: SingleChildScrollView(child: Text('$e\n$stack')), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text('OK'), - ), - ], + actions: [TextButton(onPressed: () => Navigator.of(context).pop(), child: const Text('OK'))], ), ); } @@ -216,10 +212,10 @@ class _DevToolsScreenContentState extends State { TestState.fail => Container(width: 20, height: 20, color: Colors.red), TestState.notRun => Container(width: 20, height: 20, color: Colors.grey), TestState.loading => const SizedBox( - width: 20, - height: 20, - child: SizedBox(height: 20, width: 20, child: Center(child: CircularProgressIndicator())), - ), + width: 20, + height: 20, + child: SizedBox(height: 20, width: 20, child: Center(child: CircularProgressIndicator())), + ), }, ], ), diff --git a/lib/features/dev_tools/test_cases.dart b/lib/features/dev_tools/test_cases.dart index eb72a1e7..3d43bd2a 100644 --- a/lib/features/dev_tools/test_cases.dart +++ b/lib/features/dev_tools/test_cases.dart @@ -35,6 +35,7 @@ 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:eatvio_app/core/env_config.dart'; class TestResult { final bool success; @@ -477,7 +478,9 @@ class AuthServiceTest implements TestCases { final dio = sl(); final authService = AuthService(dio, baseUrl: baseUrl); // Use dummy credentials, expecting failure but no crash - await authService.getTokenByCredentials(credentials: const CredentialsModel(email: 'reshetnick.mike+user1@gmail.com', password: '1234567890')); + await authService.getTokenByCredentials( + credentials: CredentialsModel(email: EnvConfig.testEmail, password: EnvConfig.testPassword), + ); return TestResult(false, 'Should not succeed'); } catch (e) { return TestResult(true, 'Exception expected for dummy credentials'); diff --git a/lib/features/internet_connection_checker/internet_connection_checker_screen.dart b/lib/features/internet_connection_checker/internet_connection_checker_screen.dart index 3aa1bc34..576eb891 100644 --- a/lib/features/internet_connection_checker/internet_connection_checker_screen.dart +++ b/lib/features/internet_connection_checker/internet_connection_checker_screen.dart @@ -10,6 +10,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:flutter_svg/svg.dart'; import 'package:http/http.dart' as http; +import 'package:eatvio_app/core/env_config.dart'; class InternetConnectionCheckerScreen extends StatefulWidget { const InternetConnectionCheckerScreen({super.key, required this.child}); @@ -66,12 +67,7 @@ class _InternetConnectionCheckerScreenState extends State emailIsReachable(String email) async { try { - final response = await sl().getUri(Uri.parse('https://api.validate.email/validate?email=$email')); + final response = await sl().getUri(Uri.parse('${EnvConfig.emailValidationApiUrl}?email=$email')); if (response.data['result']['reachable'] != 'invalid') { return true; } else { diff --git a/lib/features/support/presentation/pages/support_screen.dart b/lib/features/support/presentation/pages/support_screen.dart index b550040f..ce5b843a 100644 --- a/lib/features/support/presentation/pages/support_screen.dart +++ b/lib/features/support/presentation/pages/support_screen.dart @@ -6,6 +6,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:url_launcher/url_launcher.dart'; +import 'package:eatvio_app/core/env_config.dart'; class SupportScreen extends StatelessWidget { const SupportScreen({super.key}); @@ -15,48 +16,28 @@ class SupportScreen extends StatelessWidget { final s = S.of(context); return Scaffold( - appBar: AppBar( - title: const Text('Поддержка'), - ), + appBar: AppBar(title: const Text('Поддержка')), body: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Padding( padding: EdgeInsets.all(16), - child: Text( - 'Если у вас есть вопросы по работе нашего приложения, вы можете задать их, перейдя по ссылке:', - style: TextStyle( - fontSize: 16, - ), - ), + child: Text('Если у вас есть вопросы по работе нашего приложения, вы можете задать их, перейдя по ссылке:', style: TextStyle(fontSize: 16)), ), InkWell( onTap: () { - launchUrl( - Uri.parse('https://t.me/eatvio'), - mode: LaunchMode.externalApplication, - ); + launchUrl(Uri.parse(EnvConfig.telegramSupportUrl), mode: LaunchMode.externalApplication); }, borderRadius: BorderRadius.circular(16), child: Ink( width: double.infinity, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(16), - ), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(16)), padding: const EdgeInsets.all(16), child: Row( children: [ - SvgPicture.asset( - 'assets/icons/telegram.svg', - colorFilter: const ColorFilter.mode(Colors.black, BlendMode.srcIn), - ), + SvgPicture.asset('assets/icons/telegram.svg', colorFilter: const ColorFilter.mode(Colors.black, BlendMode.srcIn)), const SizedBox(width: 12), - const Text( - 'Telegram: t.me/eatvio', - style: TextStyle( - fontSize: 16, - ), - ), + const Text('Telegram: t.me/eatvio', style: TextStyle(fontSize: 16)), ], ), ), @@ -69,21 +50,14 @@ class SupportScreen extends StatelessWidget { }, borderRadius: BorderRadius.circular(16), child: Ink( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(16), - ), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(16)), width: double.infinity, padding: const EdgeInsets.all(16), child: const Row( children: [ Icon(Icons.alternate_email_rounded), SizedBox(width: 12), - Text( - 'E-mail: help@eatv.io', - style: TextStyle( - fontSize: 16, - ), - ), + Text('E-mail: help@eatv.io', style: TextStyle(fontSize: 16)), ], ), ), @@ -92,13 +66,7 @@ class SupportScreen extends StatelessWidget { const SizedBox(height: 16), const Padding( padding: EdgeInsets.symmetric(horizontal: 16), - child: Text( - 'Правовые документы', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w500, - ), - ), + child: Text('Правовые документы', style: TextStyle(fontSize: 20, fontWeight: FontWeight.w500)), ), const SizedBox(height: 12), InkWell( @@ -106,12 +74,7 @@ class SupportScreen extends StatelessWidget { borderRadius: BorderRadius.circular(16), child: const Padding( padding: EdgeInsets.symmetric(horizontal: 16, vertical: 4), - child: Text( - 'Публичная офёрта', - style: TextStyle( - fontSize: 16, - ), - ), + child: Text('Публичная офёрта', style: TextStyle(fontSize: 16)), ), ), const SizedBox(height: 8), @@ -120,12 +83,7 @@ class SupportScreen extends StatelessWidget { borderRadius: BorderRadius.circular(16), child: const Padding( padding: EdgeInsets.symmetric(horizontal: 16, vertical: 4), - child: Text( - 'Политика конфиденциальности', - style: TextStyle( - fontSize: 16, - ), - ), + child: Text('Политика конфиденциальности', style: TextStyle(fontSize: 16)), ), ), const SizedBox(height: 8), @@ -134,12 +92,7 @@ class SupportScreen extends StatelessWidget { borderRadius: BorderRadius.circular(16), child: const Padding( padding: EdgeInsets.symmetric(horizontal: 16, vertical: 4), - child: Text( - 'О компании', - style: TextStyle( - fontSize: 16, - ), - ), + child: Text('О компании', style: TextStyle(fontSize: 16)), ), ), ], diff --git a/lib/features/update_screen/presentation/update_screen.dart b/lib/features/update_screen/presentation/update_screen.dart index 186987c2..68ec9640 100644 --- a/lib/features/update_screen/presentation/update_screen.dart +++ b/lib/features/update_screen/presentation/update_screen.dart @@ -6,6 +6,7 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:url_launcher/url_launcher.dart'; import '../../../../core/theme/theme_helper.dart'; +import '../../../../core/env_config.dart'; class UpdateScreen extends StatelessWidget { const UpdateScreen({super.key}); @@ -17,16 +18,7 @@ class UpdateScreen extends StatelessWidget { body: Stack( clipBehavior: Clip.none, children: [ - Positioned( - left: -20, - top: -20, - right: -20, - bottom: -20, - child: Image.asset( - 'assets/images/bg_welcome.png', - fit: BoxFit.cover, - ), - ), + Positioned(left: -20, top: -20, right: -20, bottom: -20, child: Image.asset('assets/images/bg_welcome.png', fit: BoxFit.cover)), Positioned.fill( child: SafeArea( child: Padding( @@ -48,47 +40,26 @@ class UpdateScreen extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 16), - const Text( - 'Пора обновиться! ⚡', - style: TextStyle( - fontSize: 28, - fontWeight: FontWeight.w800, - height: 1, - ), - ), + const Text('Пора обновиться! ⚡', style: TextStyle(fontSize: 28, fontWeight: FontWeight.w800, height: 1)), const SizedBox(height: 20), const Text( 'Мы выпустили важное обновление\nс новыми функциями и исправлениями.\nЧтобы продолжить пользоваться\nприложением без сбоев, обновите\nего прямо сейчас.', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - height: 1.15, - ), + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500, height: 1.15), ), const SizedBox(height: 32), InkWell( onTap: () async { - final Uri url = Platform.isAndroid - ? Uri.parse('https://www.rustore.ru/catalog/app/com.eatvioapp.app') - : Uri.parse('https://apps.apple.com/app/idYOUR_APP_ID'); + final Uri url = Platform.isAndroid ? Uri.parse(EnvConfig.rustoreUrl) : Uri.parse(EnvConfig.appStoreUrl); await launchUrl(url); }, borderRadius: BorderRadius.circular(8), child: Ink( height: 52, - decoration: BoxDecoration( - color: appTheme.blueA400, - borderRadius: BorderRadius.circular(8), - ), + decoration: BoxDecoration(color: appTheme.blueA400, borderRadius: BorderRadius.circular(8)), child: const Center( child: Text( 'Обновить', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - height: 1, - color: Colors.white, - ), + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, height: 1, color: Colors.white), ), ), ), diff --git a/lib/firebase_options.dart b/lib/firebase_options.dart index dcf61a5d..6450332e 100644 --- a/lib/firebase_options.dart +++ b/lib/firebase_options.dart @@ -1,8 +1,8 @@ // File generated by FlutterFire CLI. // ignore_for_file: type=lint import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; -import 'package:flutter/foundation.dart' - show defaultTargetPlatform, kIsWeb, TargetPlatform; +import 'package:flutter/foundation.dart' show defaultTargetPlatform, kIsWeb, TargetPlatform; +import 'package:eatvio_app/core/env_config.dart'; /// Default [FirebaseOptions] for use with your Firebase apps. /// @@ -43,28 +43,26 @@ class DefaultFirebaseOptions { 'you can reconfigure this by running the FlutterFire CLI again.', ); default: - throw UnsupportedError( - 'DefaultFirebaseOptions are not supported for this platform.', - ); + throw UnsupportedError('DefaultFirebaseOptions are not supported for this platform.'); } } - static const FirebaseOptions android = FirebaseOptions( - apiKey: 'AIzaSyBUe1UqAAMfgMcd4hWdRoLF1Os-yn-HE9k', - appId: '1:613980912922:android:4de95e5f2d50ba69ecc0a3', - messagingSenderId: '613980912922', - projectId: 'eatvio-3467b', - databaseURL: 'https://eatvio-3467b-default-rtdb.europe-west1.firebasedatabase.app', - storageBucket: 'eatvio-3467b.firebasestorage.app', + static FirebaseOptions get android => FirebaseOptions( + apiKey: EnvConfig.firebaseAndroidApiKey, + appId: EnvConfig.firebaseAndroidAppId, + messagingSenderId: EnvConfig.firebaseMessagingSenderId, + projectId: EnvConfig.firebaseProjectId, + databaseURL: EnvConfig.firebaseDatabaseUrl, + storageBucket: EnvConfig.firebaseStorageBucket, ); - static const FirebaseOptions ios = FirebaseOptions( - apiKey: 'AIzaSyBnE8tQjL9_1HuXrTnqNy6qbXpoenO36yQ', - appId: '1:613980912922:ios:c557d140a1212e63ecc0a3', - messagingSenderId: '613980912922', - projectId: 'eatvio-3467b', - databaseURL: 'https://eatvio-3467b-default-rtdb.europe-west1.firebasedatabase.app', - storageBucket: 'eatvio-3467b.firebasestorage.app', - iosBundleId: 'com.eatvioapp.app', + static FirebaseOptions get ios => FirebaseOptions( + apiKey: EnvConfig.firebaseIosApiKey, + appId: EnvConfig.firebaseIosAppId, + messagingSenderId: EnvConfig.firebaseMessagingSenderId, + projectId: EnvConfig.firebaseProjectId, + databaseURL: EnvConfig.firebaseDatabaseUrl, + storageBucket: EnvConfig.firebaseStorageBucket, + iosBundleId: EnvConfig.firebaseIosBundleId, ); } diff --git a/lib/main.dart b/lib/main.dart index 3fd39e7f..8e2796b0 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -19,22 +19,24 @@ 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'; 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(); - await SentryFlutter.init( - (options) { - options.dsn = 'http://ba7f2653067776f3034e6e89850e85a1@sentry.md-desk.ru:9000/2'; - options.tracesSampleRate = 1.0; - options.debug = false; - }, - appRunner: () => runApp(const MyApp()), - ); + await SentryFlutter.init((options) { + options.dsn = EnvConfig.sentryDsn; + options.tracesSampleRate = 1.0; + options.debug = false; + }, appRunner: () => runApp(const MyApp())); } class MyApp extends StatelessWidget { diff --git a/pubspec.yaml b/pubspec.yaml index 0dd1a2da..ef434489 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -72,6 +72,7 @@ dependencies: device_info_plus: ^11.5.0 permission_handler: ^12.0.1 qr_code_scanner_plus: ^2.0.10+1 + flutter_dotenv: ^5.1.0 dev_dependencies: build_runner: ^2.6.0 From 5b616a9dc1df95f9254cea0e51a40c0048cb6b20 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Fri, 1 Aug 2025 16:52:53 +0300 Subject: [PATCH 055/134] feat(env): Issue-588 --- lib/core/env_config.dart | 42 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 lib/core/env_config.dart diff --git a/lib/core/env_config.dart b/lib/core/env_config.dart new file mode 100644 index 00000000..70af4567 --- /dev/null +++ b/lib/core/env_config.dart @@ -0,0 +1,42 @@ +import 'package:flutter_dotenv/flutter_dotenv.dart'; + +class EnvConfig { + static String get baseUrl => dotenv.env['BASE_URL'] ?? 'https://eatvio.ru'; + static String get devBaseUrl => dotenv.env['DEV_BASE_URL'] ?? 'https://dev2.eatvio.com'; + + // Sentry Configuration + static String get sentryDsn => dotenv.env['SENTRY_DSN'] ?? 'http://ba7f2653067776f3034e6e89850e85a1@sentry.md-desk.ru:9000/2'; + + // Firebase Configuration + static String get firebaseProjectId => dotenv.env['FIREBASE_PROJECT_ID'] ?? 'eatvio-3467b'; + static String get firebaseDatabaseUrl => dotenv.env['FIREBASE_DATABASE_URL'] ?? 'https://eatvio-3467b-default-rtdb.europe-west1.firebasedatabase.app'; + static String get firebaseStorageBucket => dotenv.env['FIREBASE_STORAGE_BUCKET'] ?? 'eatvio-3467b.firebasestorage.app'; + static String get firebaseMessagingSenderId => dotenv.env['FIREBASE_MESSAGING_SENDER_ID'] ?? '613980912922'; + + // Firebase Android Configuration + static String get firebaseAndroidApiKey => dotenv.env['FIREBASE_ANDROID_API_KEY'] ?? 'AIzaSyBUe1UqAAMfgMcd4hWdRoLF1Os-yn-HE9k'; + static String get firebaseAndroidAppId => dotenv.env['FIREBASE_ANDROID_APP_ID'] ?? '1:613980912922:android:4de95e5f2d50ba69ecc0a3'; + + // Firebase iOS Configuration + static String get firebaseIosApiKey => dotenv.env['FIREBASE_IOS_API_KEY'] ?? 'AIzaSyBnE8tQjL9_1HuXrTnqNy6qbXpoenO36yQ'; + static String get firebaseIosAppId => dotenv.env['FIREBASE_IOS_APP_ID'] ?? '1:613980912922:ios:c557d140a1212e63ecc0a3'; + static String get firebaseIosBundleId => dotenv.env['FIREBASE_IOS_BUNDLE_ID'] ?? 'com.eatvioapp.app'; + + // App Store URLs + static String get appStoreUrl => dotenv.env['APP_STORE_URL'] ?? 'https://apps.apple.com/app/idYOUR_APP_ID'; + static String get rustoreUrl => dotenv.env['RUSTORE_URL'] ?? 'https://www.rustore.ru/catalog/app/com.eatvioapp.app'; + + // Telegram Support + static String get telegramSupportUrl => dotenv.env['TELEGRAM_SUPPORT_URL'] ?? 'https://t.me/eatvio'; + + // Test Credentials (for development only) + static String get testEmail => dotenv.env['TEST_EMAIL'] ?? 'reshetnick.mike+user1@gmail.com'; + static String get testPassword => dotenv.env['TEST_PASSWORD'] ?? '1234567890'; + + // Email Validation API + static String get emailValidationApiUrl => dotenv.env['EMAIL_VALIDATION_API_URL'] ?? 'https://api.validate.email/validate'; + + // Connection Check URLs + static String get googleCheckUrl => dotenv.env['GOOGLE_CHECK_URL'] ?? 'https://google.com'; + static String get yandexCheckUrl => dotenv.env['YANDEX_CHECK_URL'] ?? 'https://yandex.ru'; +} From 681c11fed9ef4d434160e30c7ee368cb3e3fb34a Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Fri, 1 Aug 2025 16:58:48 +0300 Subject: [PATCH 056/134] feat(env): Issue-588 --- ENV_SETUP.md | 6 ++++-- lib/core/env_config.dart | 1 + .../support/presentation/pages/support_screen.dart | 13 +++++++------ 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/ENV_SETUP.md b/ENV_SETUP.md index f57b9796..0873fb1c 100644 --- a/ENV_SETUP.md +++ b/ENV_SETUP.md @@ -39,8 +39,9 @@ cp env.example .env - `APP_STORE_URL` - URL App Store - `RUSTORE_URL` - URL RuStore -### Telegram Support +### Support URLs - `TELEGRAM_SUPPORT_URL` - URL поддержки в Telegram +- `SUPPORT_EMAIL` - email для поддержки ### Test Credentials (только для разработки) - `TEST_EMAIL` - тестовый email @@ -79,4 +80,5 @@ String sentryDsn = EnvConfig.sentryDsn; 5. **URL для обновления приложения** - из `update_screen.dart` 6. **URL поддержки Telegram** - из `support_screen.dart` 7. **API для валидации email** - из `registration_repository.dart` -8. **Тестовые учетные данные** - из `test_cases.dart` \ No newline at end of file +8. **Тестовые учетные данные** - из `test_cases.dart` +9. **Email поддержки** - из `support_screen.dart` \ No newline at end of file diff --git a/lib/core/env_config.dart b/lib/core/env_config.dart index 70af4567..23c18f62 100644 --- a/lib/core/env_config.dart +++ b/lib/core/env_config.dart @@ -28,6 +28,7 @@ class EnvConfig { // Telegram Support static String get telegramSupportUrl => dotenv.env['TELEGRAM_SUPPORT_URL'] ?? 'https://t.me/eatvio'; + static String get supportEmail => dotenv.env['SUPPORT_EMAIL'] ?? 'help@eatv.io'; // Test Credentials (for development only) static String get testEmail => dotenv.env['TEST_EMAIL'] ?? 'reshetnick.mike+user1@gmail.com'; diff --git a/lib/features/support/presentation/pages/support_screen.dart b/lib/features/support/presentation/pages/support_screen.dart index ce5b843a..3fac3447 100644 --- a/lib/features/support/presentation/pages/support_screen.dart +++ b/lib/features/support/presentation/pages/support_screen.dart @@ -7,6 +7,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:eatvio_app/core/env_config.dart'; +import 'package:eatvio_app/core/const.dart'; class SupportScreen extends StatelessWidget { const SupportScreen({super.key}); @@ -37,7 +38,7 @@ class SupportScreen extends StatelessWidget { children: [ SvgPicture.asset('assets/icons/telegram.svg', colorFilter: const ColorFilter.mode(Colors.black, BlendMode.srcIn)), const SizedBox(width: 12), - const Text('Telegram: t.me/eatvio', style: TextStyle(fontSize: 16)), + Text('Telegram: ${EnvConfig.telegramSupportUrl.replaceFirst('https://', '')}', style: const TextStyle(fontSize: 16)), ], ), ), @@ -45,7 +46,7 @@ class SupportScreen extends StatelessWidget { const SizedBox(height: 8), InkWell( onTap: () { - Clipboard.setData(const ClipboardData(text: 'help@eatv.io')); + Clipboard.setData(ClipboardData(text: EnvConfig.supportEmail)); showInfoToast(s.mail_was_copied); }, borderRadius: BorderRadius.circular(16), @@ -53,11 +54,11 @@ class SupportScreen extends StatelessWidget { decoration: BoxDecoration(borderRadius: BorderRadius.circular(16)), width: double.infinity, padding: const EdgeInsets.all(16), - child: const Row( + child: Row( children: [ - Icon(Icons.alternate_email_rounded), - SizedBox(width: 12), - Text('E-mail: help@eatv.io', style: TextStyle(fontSize: 16)), + const Icon(Icons.alternate_email_rounded), + const SizedBox(width: 12), + Text('E-mail: ${EnvConfig.supportEmail}', style: const TextStyle(fontSize: 16)), ], ), ), From 039c09962ab441354dce2636fbc6d31074611447 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Mon, 4 Aug 2025 12:40:21 +0300 Subject: [PATCH 057/134] fix: env --- .gitignore | 1 + .../widgets/consultation_card_widget.dart | 209 ++++++---------- .../presentation/pages/support_screen.dart | 1 - .../cubit/user_details_cubit.dart | 16 +- .../presentation/user_details_screen.dart | 224 ++++-------------- lib/main.dart | 6 + pubspec.yaml | 1 + 7 files changed, 128 insertions(+), 330 deletions(-) diff --git a/.gitignore b/.gitignore index 9d5ba6c3..b448cede 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ android/build/ ios/XCBuildData/ .env +env.example diff --git a/lib/features/consultations/presentation/widgets/consultation_card_widget.dart b/lib/features/consultations/presentation/widgets/consultation_card_widget.dart index 04a0b7fb..efebd74f 100644 --- a/lib/features/consultations/presentation/widgets/consultation_card_widget.dart +++ b/lib/features/consultations/presentation/widgets/consultation_card_widget.dart @@ -14,10 +14,7 @@ import 'package:go_router/go_router.dart'; class ConsultationCardWidget extends StatefulWidget { final ConsultationModel consultationsModel; - const ConsultationCardWidget({ - super.key, - required this.consultationsModel, - }); + const ConsultationCardWidget({super.key, required this.consultationsModel}); @override State createState() => _ConsultationCardWidgetState(); @@ -32,13 +29,7 @@ class _ConsultationCardWidgetState extends State { 'Дневник заполнен, прокомментирован консультантом и после этого клиент просмотрел комментарии.', ]; - final List legendColors = const [ - Colors.white, - Color(0xFFF0C497), - Color(0xFFFF9500), - Color(0xFF2F80ED), - Color(0xFF4CD964), - ]; + final List legendColors = const [Colors.white, Color(0xFFF0C497), Color(0xFFFF9500), Color(0xFF2F80ED), Color(0xFF4CD964)]; late final List<(int, DiaryModel)> givenDaysWithDayNumber; final List> legendStates = [ @@ -46,7 +37,7 @@ class _ConsultationCardWidgetState extends State { [DiaryStateEnum.started, DiaryStateEnum.lastDayStarted], [DiaryStateEnum.finished, DiaryStateEnum.lastDayFinished], [DiaryStateEnum.commented, DiaryStateEnum.lastDayCommented], - [DiaryStateEnum.reviewedByUser] + [DiaryStateEnum.reviewedByUser], ]; @override void initState() { @@ -72,32 +63,19 @@ class _ConsultationCardWidgetState extends State { children: [ Row( children: [ - CircleAvatar( - radius: 30, - backgroundImage: NetworkImage(baseUrl + widget.consultationsModel.userProfilePhotoUrl), - ), + CircleAvatar(radius: 30, backgroundImage: NetworkImage(baseUrl + widget.consultationsModel.userProfilePhotoUrl)), const SizedBox(width: 12), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( widget.consultationsModel.userName, - style: const TextStyle( - height: 1, - color: Color(0xFF323F4B), - fontSize: 16, - fontWeight: FontWeight.w600, - ), + style: const TextStyle(height: 1, color: Color(0xFF323F4B), fontSize: 16, fontWeight: FontWeight.w600), ), const SizedBox(height: 4), Text( widget.consultationsModel.planTitle, - style: const TextStyle( - height: 1, - color: Color(0xFF88949F), - fontSize: 14, - fontWeight: FontWeight.w500, - ), + style: const TextStyle(height: 1, color: Color(0xFF88949F), fontSize: 14, fontWeight: FontWeight.w500), ), ], ), @@ -127,12 +105,7 @@ class _ConsultationCardWidgetState extends State { children: [ Text( 'День ${widget.consultationsModel.currentDiaryDay} из ${widget.consultationsModel.duration}', - style: const TextStyle( - height: 1, - color: Color(0xFF323F4B), - fontSize: 16, - fontWeight: FontWeight.w500, - ), + style: const TextStyle(height: 1, color: Color(0xFF323F4B), fontSize: 16, fontWeight: FontWeight.w500), ), GestureDetector( onTap: () { @@ -149,12 +122,7 @@ class _ConsultationCardWidgetState extends State { children: [ const Text( 'Легенда', - style: TextStyle( - height: 1, - fontSize: 20, - fontWeight: FontWeight.w700, - color: Color(0xFF323F4B), - ), + style: TextStyle(height: 1, fontSize: 20, fontWeight: FontWeight.w700, color: Color(0xFF323F4B)), ), const SizedBox(height: 16), ListView.separated( @@ -170,11 +138,7 @@ class _ConsultationCardWidgetState extends State { ), ); }, - child: const Icon( - Icons.help_outline_rounded, - color: Color(0xFF323F4B), - size: 20, - ), + child: const Icon(Icons.help_outline_rounded, color: Color(0xFF323F4B), size: 20), ), ], ), @@ -185,7 +149,8 @@ class _ConsultationCardWidgetState extends State { children: List.generate(widget.consultationsModel.duration, (index) { final day = givenDaysWithDayNumber.firstWhereOrNull((e) => e.$1 == index); final hasActivity = day != null && day.$2.state != DiaryStateEnum.init; - final canConsultantOpenThisDay = day != null && + final canConsultantOpenThisDay = + day != null && (day.$2.state != DiaryStateEnum.init && day.$2.state != DiaryStateEnum.started && day.$2.state != DiaryStateEnum.lastDayStarted); return Padding( padding: const EdgeInsets.only(right: 12), @@ -195,81 +160,74 @@ class _ConsultationCardWidgetState extends State { InkWell( onTap: () { if (!hasActivity || !canConsultantOpenThisDay) { - showErrorDialog(context, - title: s.you_can_not_open_this_day, description: '${widget.consultationsModel.userName} ${s.has_not_filled_it_yet}'); + showErrorDialog( + context, + title: s.you_can_not_open_this_day, + description: '${widget.consultationsModel.userName} ${s.has_not_filled_it_yet}', + ); return; } - GoRouter.of(context).push(UserDiaryScreen.path, extra: { - 'diaryId': day.$2.id, - 'currentDiaryDay': index + 1, - }); + GoRouter.of(context).push(UserDiaryScreen.path, extra: {'diaryId': day.$2.id, 'currentDiaryDay': index + 1}); }, borderRadius: BorderRadius.circular(100), child: Ink( decoration: BoxDecoration( - color: hasActivity ? appTheme.dividerColor : appTheme.dividerColor.withValues(alpha: 0.4), - borderRadius: BorderRadius.circular(100)), + color: hasActivity ? appTheme.dividerColor : appTheme.dividerColor.withValues(alpha: 0.4), + borderRadius: BorderRadius.circular(100), + ), width: 58, height: 32, child: Center( child: Text( (index + 1).toString(), - style: TextStyles.textMSemibold.copyWith( - color: hasActivity ? appTheme.gray : appTheme.gray.withValues(alpha: 0.4), - ), + style: TextStyles.textMSemibold.copyWith(color: hasActivity ? appTheme.gray : appTheme.gray.withValues(alpha: 0.4)), ), ), ), ), if (hasActivity) ...[ - const SizedBox( - height: 4, - ), + const SizedBox(height: 4), switch (day.$2.state) { DiaryStateEnum.init => const SizedBox.shrink(), DiaryStateEnum.started => Container( - width: 8, - height: 8, - decoration: BoxDecoration(shape: BoxShape.circle, color: legendColors[1]), - ), + width: 8, + height: 8, + decoration: BoxDecoration(shape: BoxShape.circle, color: legendColors[1]), + ), DiaryStateEnum.lastDayStarted => Container( - width: 8, - height: 8, - decoration: BoxDecoration(shape: BoxShape.circle, color: legendColors[1]), - ), + width: 8, + height: 8, + decoration: BoxDecoration(shape: BoxShape.circle, color: legendColors[1]), + ), DiaryStateEnum.finished => Container( - width: 8, - height: 8, - decoration: BoxDecoration(shape: BoxShape.circle, color: legendColors[2]), - ), + width: 8, + height: 8, + decoration: BoxDecoration(shape: BoxShape.circle, color: legendColors[2]), + ), DiaryStateEnum.lastDayFinished => Container( - width: 8, - height: 8, - decoration: BoxDecoration(shape: BoxShape.circle, color: legendColors[2]), - ), + width: 8, + height: 8, + decoration: BoxDecoration(shape: BoxShape.circle, color: legendColors[2]), + ), DiaryStateEnum.commented => Container( - width: 8, - height: 8, - decoration: BoxDecoration(shape: BoxShape.circle, color: legendColors[3]), - ), + width: 8, + height: 8, + decoration: BoxDecoration(shape: BoxShape.circle, color: legendColors[3]), + ), DiaryStateEnum.lastDayCommented => Container( - width: 8, - height: 8, - decoration: BoxDecoration(shape: BoxShape.circle, color: legendColors[3]), - ), + width: 8, + height: 8, + decoration: BoxDecoration(shape: BoxShape.circle, color: legendColors[3]), + ), DiaryStateEnum.reviewedByUser => Container( - width: 8, - height: 8, - decoration: BoxDecoration(shape: BoxShape.circle, color: legendColors[4]), - ), + width: 8, + height: 8, + decoration: BoxDecoration(shape: BoxShape.circle, color: legendColors[4]), + ), }, - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), ] else - const SizedBox( - height: 24, - ), + const SizedBox(height: 24), ], ), ); @@ -280,11 +238,7 @@ class _ConsultationCardWidgetState extends State { const SizedBox(height: 12), Text( '${widget.consultationsModel.consultationOptionPrice.toStringAsFixed(0)} ₽', - style: const TextStyle( - color: Color(0xFF019A3E), - fontSize: 20, - fontWeight: FontWeight.w600, - ), + style: const TextStyle(color: Color(0xFF019A3E), fontSize: 20, fontWeight: FontWeight.w600), ), const SizedBox(height: 16), Row( @@ -292,36 +246,33 @@ class _ConsultationCardWidgetState extends State { InkWell( borderRadius: BorderRadius.circular(20), onTap: () { + print(givenDaysWithDayNumber); + print(DiaryStateEnum.finished); + print(DiaryStateEnum.lastDayFinished); + print(DiaryStateEnum.commented); + print(DiaryStateEnum.lastDayCommented); + print(DiaryStateEnum.reviewedByUser); final lastActiveDay = givenDaysWithDayNumber - .where( - (e) => e.$2.state == DiaryStateEnum.finished || e.$2.state == DiaryStateEnum.lastDayFinished, - ) + .where((e) => e.$2.state == DiaryStateEnum.finished || e.$2.state == DiaryStateEnum.lastDayFinished) .lastOrNull; if (lastActiveDay != null) { - GoRouter.of(context).push(UserDiaryScreen.path, extra: { - 'diaryId': lastActiveDay.$2.id, - 'currentDiaryDay': lastActiveDay.$1, - }); + GoRouter.of(context).push(UserDiaryScreen.path, extra: {'diaryId': lastActiveDay.$2.id, 'currentDiaryDay': lastActiveDay.$1}); return; } - showErrorDialog(context, - title: s.you_can_not_open_this_day, description: '${widget.consultationsModel.userName} ${s.has_not_filled_it_yet}'); + showErrorDialog( + context, + title: s.you_can_not_open_this_day, + description: '${widget.consultationsModel.userName} ${s.has_not_filled_it_yet}', + ); }, child: Ink( width: MediaQuery.of(context).size.width - 32 - 52 - 8, padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - color: appTheme.blueA400, - ), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: appTheme.blueA400), child: const Center( child: Text( 'Комментировать отчёт', - style: TextStyle( - color: Colors.white, - fontSize: 16, - fontWeight: FontWeight.w600, - ), + style: TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.w600), ), ), ), @@ -333,16 +284,8 @@ class _ConsultationCardWidgetState extends State { child: Ink( height: 52, width: 52, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - color: const Color(0xFFF6F6F6), - ), - child: const Center( - child: Icon( - Icons.chat_outlined, - size: 24, - ), - ), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: const Color(0xFFF6F6F6)), + child: const Center(child: Icon(Icons.chat_outlined, size: 24)), ), ), ], @@ -360,21 +303,13 @@ class _ConsultationCardWidgetState extends State { Container( height: 20, width: 20, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: color, - ), + decoration: BoxDecoration(shape: BoxShape.circle, color: color), ), const SizedBox(width: 12), Flexible( child: Text( text, - style: const TextStyle( - height: 1, - fontSize: 16, - fontWeight: FontWeight.w500, - color: Color(0xFF323F4B), - ), + style: const TextStyle(height: 1, fontSize: 16, fontWeight: FontWeight.w500, color: Color(0xFF323F4B)), ), ), ], diff --git a/lib/features/support/presentation/pages/support_screen.dart b/lib/features/support/presentation/pages/support_screen.dart index 3fac3447..c6f7719a 100644 --- a/lib/features/support/presentation/pages/support_screen.dart +++ b/lib/features/support/presentation/pages/support_screen.dart @@ -7,7 +7,6 @@ import 'package:flutter/services.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:eatvio_app/core/env_config.dart'; -import 'package:eatvio_app/core/const.dart'; class SupportScreen extends StatelessWidget { const SupportScreen({super.key}); diff --git a/lib/features/user_details/presentation/cubit/user_details_cubit.dart b/lib/features/user_details/presentation/cubit/user_details_cubit.dart index c6eb36b7..0ce0bd93 100644 --- a/lib/features/user_details/presentation/cubit/user_details_cubit.dart +++ b/lib/features/user_details/presentation/cubit/user_details_cubit.dart @@ -20,19 +20,11 @@ class UserDetailsCubit extends Cubit { } } - void getChartData({ - String period = '1', - required String slug, - }) async { + void getChartData({String period = '1', required String slug}) async { try { - final weightPoints = await profileRepository.getWeight(period: period); - final circumferencePoints = await profileRepository.getCircumferences(period: period); - emit( - state.copyWith( - weightPoints: weightPoints, - circumferencePoints: circumferencePoints, - ), - ); + final weightPoints = await profileRepository.getWeight(period: period, slug: slug); + final circumferencePoints = await profileRepository.getCircumferences(period: period, slug: slug); + emit(state.copyWith(weightPoints: weightPoints, circumferencePoints: circumferencePoints)); } catch (e, stackTrace) { log(e.toString(), stackTrace: stackTrace); emit(state.copyWith(error: e.toString(), isLoading: false)); diff --git a/lib/features/user_details/presentation/user_details_screen.dart b/lib/features/user_details/presentation/user_details_screen.dart index f79b990e..4907e3dd 100644 --- a/lib/features/user_details/presentation/user_details_screen.dart +++ b/lib/features/user_details/presentation/user_details_screen.dart @@ -31,9 +31,7 @@ class UserDetailsScreen extends StatelessWidget { Widget build(BuildContext context) { return BlocProvider( create: (context) => sl()..getUserDetails(slug), - child: UserDetailsContent( - slug: slug, - ), + child: UserDetailsContent(slug: slug), ); } } @@ -79,27 +77,15 @@ class _UserDetailsContentState extends State { actions: [ PortalTarget( visible: isMoreOptionsVisible, - portalFollower: MoreOptions( - path: 'profile/${widget.slug}', - complainedContentType: ComplainedContent.profile, - slug: widget.slug, - ), - anchor: const Aligned( - shiftToWithinBound: AxisFlag(x: true, y: true), - follower: Alignment.topRight, - target: Alignment.bottomRight, - ), + portalFollower: MoreOptions(path: 'profile/${widget.slug}', complainedContentType: ComplainedContent.profile, slug: widget.slug), + anchor: const Aligned(shiftToWithinBound: AxisFlag(x: true, y: true), follower: Alignment.topRight, target: Alignment.bottomRight), child: IconButton( onPressed: () { setState(() { isMoreOptionsVisible = !isMoreOptionsVisible; }); }, - icon: SvgPicture.asset( - 'assets/icons/more_button.svg', - width: 20, - height: 20, - ), + icon: SvgPicture.asset('assets/icons/more_button.svg', width: 20, height: 20), ), ), ], @@ -124,25 +110,16 @@ class _UserDetailsContentState extends State { initiallyExpanded: true, title: s.general_info, children: [ - Text( - s.date_of_birth, - style: TextStyles.textSMedium.copyWith(color: appTheme.black), - ), + Text(s.date_of_birth, style: TextStyles.textSMedium.copyWith(color: appTheme.black)), const SizedBox(height: 4), CustomTextFormField( readOnly: true, controller: TextEditingController(text: DateTime.parse(profileDetails.birthdate).format('dd.MM.yyyy')), ), const SizedBox(height: 16), - Text( - s.sex, - style: TextStyles.textSMedium.copyWith(color: appTheme.black), - ), + Text(s.sex, style: TextStyles.textSMedium.copyWith(color: appTheme.black)), const SizedBox(height: 4), - CustomTextFormField( - readOnly: true, - controller: TextEditingController(text: profileDetails.gender.parseGender()), - ), + CustomTextFormField(readOnly: true, controller: TextEditingController(text: profileDetails.gender.parseGender())), const SizedBox(height: 16), ], ), @@ -155,15 +132,9 @@ class _UserDetailsContentState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - s.height, - style: TextStyles.textSMedium.copyWith(color: appTheme.black), - ), + Text(s.height, style: TextStyles.textSMedium.copyWith(color: appTheme.black)), const SizedBox(height: 4), - CustomTextFormField( - readOnly: true, - controller: TextEditingController(text: profileDetails.height.toString()), - ), + CustomTextFormField(readOnly: true, controller: TextEditingController(text: profileDetails.height.toString())), ], ), ), @@ -172,15 +143,9 @@ class _UserDetailsContentState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - s.weight, - style: TextStyles.textSMedium.copyWith(color: appTheme.black), - ), + Text(s.weight, style: TextStyles.textSMedium.copyWith(color: appTheme.black)), const SizedBox(height: 4), - CustomTextFormField( - readOnly: true, - controller: TextEditingController(text: profileDetails.weight.toStringAsFixed(0)), - ), + CustomTextFormField(readOnly: true, controller: TextEditingController(text: profileDetails.weight.toStringAsFixed(0))), ], ), ), @@ -193,15 +158,9 @@ class _UserDetailsContentState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - s.chest_circumference, - style: TextStyles.textSMedium.copyWith(color: appTheme.black), - ), + Text(s.chest_circumference, style: TextStyles.textSMedium.copyWith(color: appTheme.black)), const SizedBox(height: 4), - CustomTextFormField( - readOnly: true, - controller: TextEditingController(text: profileDetails.chestCircumference.toString()), - ), + CustomTextFormField(readOnly: true, controller: TextEditingController(text: profileDetails.chestCircumference.toString())), ], ), ), @@ -210,15 +169,9 @@ class _UserDetailsContentState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - s.waist_circumference, - style: TextStyles.textSMedium.copyWith(color: appTheme.black), - ), + Text(s.waist_circumference, style: TextStyles.textSMedium.copyWith(color: appTheme.black)), const SizedBox(height: 4), - CustomTextFormField( - readOnly: true, - controller: TextEditingController(text: profileDetails.waistCircumference.toString()), - ), + CustomTextFormField(readOnly: true, controller: TextEditingController(text: profileDetails.waistCircumference.toString())), ], ), ), @@ -227,15 +180,9 @@ class _UserDetailsContentState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - s.hip_circumference, - style: TextStyles.textSMedium.copyWith(color: appTheme.black), - ), + Text(s.hip_circumference, style: TextStyles.textSMedium.copyWith(color: appTheme.black)), const SizedBox(height: 4), - CustomTextFormField( - readOnly: true, - controller: TextEditingController(text: profileDetails.hipCircumference.toString()), - ), + CustomTextFormField(readOnly: true, controller: TextEditingController(text: profileDetails.hipCircumference.toString())), ], ), ), @@ -253,15 +200,9 @@ class _UserDetailsContentState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - s.upper, - style: TextStyles.textSMedium.copyWith(color: appTheme.black), - ), + Text(s.upper, style: TextStyles.textSMedium.copyWith(color: appTheme.black)), const SizedBox(height: 4), - CustomTextFormField( - readOnly: true, - controller: TextEditingController(text: profileDetails.bloodPressureUp.toString()), - ), + CustomTextFormField(readOnly: true, controller: TextEditingController(text: profileDetails.bloodPressureUp.toString())), ], ), ), @@ -270,10 +211,7 @@ class _UserDetailsContentState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - s.lower, - style: TextStyles.textSMedium.copyWith(color: appTheme.black), - ), + Text(s.lower, style: TextStyles.textSMedium.copyWith(color: appTheme.black)), const SizedBox(height: 4), CustomTextFormField( readOnly: true, @@ -291,15 +229,9 @@ class _UserDetailsContentState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - s.has_hypo, - style: TextStyles.textSMedium.copyWith(color: appTheme.black), - ), + 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.toString())), ], ), ), @@ -308,15 +240,9 @@ class _UserDetailsContentState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - s.has_hyper, - style: TextStyles.textSMedium.copyWith(color: appTheme.black), - ), + 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.toString())), ], ), ), @@ -328,60 +254,27 @@ class _UserDetailsContentState extends State { ExpandableBouble( title: s.other_info, children: [ - Text( - s.has_alergy, - style: TextStyles.textSMedium.copyWith(color: appTheme.black), - ), + Text(s.has_alergy, style: TextStyles.textSMedium.copyWith(color: appTheme.black)), const SizedBox(height: 4), - CustomTextFormField( - readOnly: true, - controller: TextEditingController(text: profileDetails.aboutAlergy), - ), + CustomTextFormField(readOnly: true, controller: TextEditingController(text: profileDetails.aboutAlergy)), const SizedBox(height: 16), - Text( - s.takes_pills, - style: TextStyles.textSMedium.copyWith(color: appTheme.black), - ), + Text(s.takes_pills, style: TextStyles.textSMedium.copyWith(color: appTheme.black)), const SizedBox(height: 4), - CustomTextFormField( - readOnly: true, - controller: TextEditingController(text: profileDetails.aboutTakingPils), - ), + CustomTextFormField(readOnly: true, controller: TextEditingController(text: profileDetails.aboutTakingPils)), const SizedBox(height: 16), - Text( - s.sport_hobbies, - style: TextStyles.textSMedium.copyWith(color: appTheme.black), - ), + Text(s.sport_hobbies, style: TextStyles.textSMedium.copyWith(color: appTheme.black)), const SizedBox(height: 4), - CustomTextFormField( - readOnly: true, - controller: TextEditingController(text: profileDetails.aboutSportHobbies), - ), + CustomTextFormField(readOnly: true, controller: TextEditingController(text: profileDetails.aboutSportHobbies)), const SizedBox(height: 16), - Text( - s.liked_groceries, - style: TextStyles.textSMedium.copyWith(color: appTheme.black), - ), + Text(s.liked_groceries, style: TextStyles.textSMedium.copyWith(color: appTheme.black)), const SizedBox(height: 4), - CustomTextFormField( - readOnly: true, - controller: TextEditingController(text: profileDetails.favoriteFoods), - ), + CustomTextFormField(readOnly: true, controller: TextEditingController(text: profileDetails.favoriteFoods)), const SizedBox(height: 16), - Text( - s.not_liked_groceries, - style: TextStyles.textSMedium.copyWith(color: appTheme.black), - ), + Text(s.not_liked_groceries, style: TextStyles.textSMedium.copyWith(color: appTheme.black)), const SizedBox(height: 4), - CustomTextFormField( - readOnly: true, - controller: TextEditingController(text: profileDetails.unfavoriteFoods), - ), + CustomTextFormField(readOnly: true, controller: TextEditingController(text: profileDetails.unfavoriteFoods)), const SizedBox(height: 16), - Text( - s.additional_information, - style: TextStyles.textSMedium.copyWith(color: appTheme.black), - ), + Text(s.additional_information, style: TextStyles.textSMedium.copyWith(color: appTheme.black)), const SizedBox(height: 4), SizedBox( height: 112, @@ -413,10 +306,7 @@ class _UserDetailsContentState extends State { ), ); } - return Scaffold( - appBar: AppBar(), - body: const Loader(), - ); + return Scaffold(appBar: AppBar(), body: const Loader()); }, ); } @@ -432,31 +322,17 @@ class _UserDetailsContentState extends State { SingleChildScrollView( padding: const EdgeInsets.symmetric(horizontal: 16), scrollDirection: Axis.horizontal, - child: Row( - children: [ - _choiceChipWidget(0), - const SizedBox(width: 8), - _choiceChipWidget(1), - const SizedBox(width: 8), - _choiceChipWidget(2), - ], - ), + child: Row(children: [_choiceChipWidget(0), const SizedBox(width: 8), _choiceChipWidget(1), const SizedBox(width: 8), _choiceChipWidget(2)]), ), Builder( builder: (context) { switch (_selectedChipIndex) { case 0: - return WeightChartWidget( - weightPoints: weightPoints, - ); + return WeightChartWidget(weightPoints: weightPoints); case 1: - return CircumferenceChartWidget( - circumferencePoints: circumferencePoints, - ); + return CircumferenceChartWidget(circumferencePoints: circumferencePoints); case 2: - return ImtChartWidget( - weightPoints: weightPoints, - ); + return ImtChartWidget(weightPoints: weightPoints); default: return const SizedBox.shrink(); } @@ -489,16 +365,10 @@ class _UserDetailsContentState 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: 8, vertical: 4), label: Text(chipsIntervalLabelList[index]), - labelStyle: TextStyle( - color: appTheme.gray600, - fontWeight: FontWeight.w500, - fontSize: 12, - ), + labelStyle: TextStyle(color: appTheme.gray600, fontWeight: FontWeight.w500, fontSize: 12), selected: _selectedIntervalChipIndex == index, onSelected: (bool selected) { context.read().getChartData(period: chipsIntervalList[index], slug: widget.slug); @@ -515,16 +385,10 @@ class _UserDetailsContentState 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: 8, vertical: 4), label: Text(chipsLabelList[index]), - labelStyle: TextStyle( - color: index == _selectedChipIndex ? Colors.white : appTheme.gray600, - fontWeight: FontWeight.w500, - fontSize: 12, - ), + labelStyle: TextStyle(color: index == _selectedChipIndex ? Colors.white : appTheme.gray600, fontWeight: FontWeight.w500, fontSize: 12), selected: _selectedChipIndex == index, backgroundColor: appTheme.gray100, onSelected: (bool selected) { diff --git a/lib/main.dart b/lib/main.dart index 8e2796b0..f96256c5 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,3 +1,4 @@ +import 'dart:io' as io; 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'; @@ -12,6 +13,7 @@ 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'; @@ -32,6 +34,10 @@ Future main() async { await FirebaseMessaging.instance.requestPermission(); await di.init(); + if (kDebugMode) { + io.HttpClient.enableTimelineLogging = true; + } + await SentryFlutter.init((options) { options.dsn = EnvConfig.sentryDsn; options.tracesSampleRate = 1.0; diff --git a/pubspec.yaml b/pubspec.yaml index ef434489..46ff6a69 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -89,6 +89,7 @@ dev_dependencies: flutter: uses-material-design: true assets: + - .env - assets/ - assets/images/ - assets/audio/ From 12af3480000bbd9099b800c3d1a89602827d0ae8 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Mon, 4 Aug 2025 09:48:44 +0000 Subject: [PATCH 058/134] .env --- .env | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 .env diff --git a/.env b/.env new file mode 100644 index 00000000..e69de29b From a9a479b2d3d939de8afc0af60468d984730f0195 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Mon, 4 Aug 2025 12:48:51 +0300 Subject: [PATCH 059/134] fix: print --- .../presentation/widgets/consultation_card_widget.dart | 6 ------ 1 file changed, 6 deletions(-) diff --git a/lib/features/consultations/presentation/widgets/consultation_card_widget.dart b/lib/features/consultations/presentation/widgets/consultation_card_widget.dart index efebd74f..760a8783 100644 --- a/lib/features/consultations/presentation/widgets/consultation_card_widget.dart +++ b/lib/features/consultations/presentation/widgets/consultation_card_widget.dart @@ -246,12 +246,6 @@ class _ConsultationCardWidgetState extends State { InkWell( borderRadius: BorderRadius.circular(20), onTap: () { - print(givenDaysWithDayNumber); - print(DiaryStateEnum.finished); - print(DiaryStateEnum.lastDayFinished); - print(DiaryStateEnum.commented); - print(DiaryStateEnum.lastDayCommented); - print(DiaryStateEnum.reviewedByUser); final lastActiveDay = givenDaysWithDayNumber .where((e) => e.$2.state == DiaryStateEnum.finished || e.$2.state == DiaryStateEnum.lastDayFinished) .lastOrNull; From 9e0c89c04e1105c6306ff64b1e526069b9f528c7 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Mon, 4 Aug 2025 10:08:38 +0000 Subject: [PATCH 060/134] =?UTF-8?q?=D0=A3=D0=B4=D0=B0=D0=BB=D0=B8=D1=82?= =?UTF-8?q?=D1=8C=20.env?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 .env diff --git a/.env b/.env deleted file mode 100644 index e69de29b..00000000 From 7267fc454b58aac758d10799391b61a3424a47cf Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Mon, 4 Aug 2025 10:08:55 +0000 Subject: [PATCH 061/134] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20env/.env?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- env/.env | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 env/.env diff --git a/env/.env b/env/.env new file mode 100644 index 00000000..e69de29b From e75539338b1cb3d8fc5d5cd236a24082f48d8506 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Mon, 4 Aug 2025 13:09:05 +0300 Subject: [PATCH 062/134] fix: env --- lib/main.dart | 2 +- pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index f96256c5..8147c18a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -28,7 +28,7 @@ Future main() async { WidgetsBinding widgetsBinding = SentryWidgetsFlutterBinding.ensureInitialized(); FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); - await dotenv.load(fileName: ".env"); + await dotenv.load(fileName: "env/.env"); await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); await FirebaseMessaging.instance.requestPermission(); diff --git a/pubspec.yaml b/pubspec.yaml index 46ff6a69..0154d606 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -89,7 +89,7 @@ dev_dependencies: flutter: uses-material-design: true assets: - - .env + - env/.env - assets/ - assets/images/ - assets/audio/ From cd9357c050a85a432c449e99796e2b6721a8eec2 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Wed, 6 Aug 2025 11:28:02 +0300 Subject: [PATCH 063/134] feat(notifications): Issue-191 --- .../models/notification_model.dart | 384 ++++++++++++++---- .../notifications_cubit.dart | 190 +++++++-- .../notifications_state.dart | 3 + .../presentation/notifications_screen.dart | 138 ++++--- .../widgets/notification_widget.dart | 327 +++++++++++---- .../repository/notifications_repository.dart | 5 +- .../service/notifications_service.dart | 150 +++++-- lib/main.dart | 2 +- 8 files changed, 918 insertions(+), 281 deletions(-) diff --git a/lib/features/notifications/models/notification_model.dart b/lib/features/notifications/models/notification_model.dart index 827a0904..96626d5d 100644 --- a/lib/features/notifications/models/notification_model.dart +++ b/lib/features/notifications/models/notification_model.dart @@ -1,8 +1,7 @@ -import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart'; - sealed class BaseNotificationModel { abstract final NotificationType type; abstract final bool isRead; + abstract final DateTime createdAt; } class PlanSubscriptionStartedToAuthorNotification implements BaseNotificationModel { @@ -13,6 +12,8 @@ class PlanSubscriptionStartedToAuthorNotification implements BaseNotificationMod final String planName; final String customerName; final String chatId; + @override + final DateTime createdAt; PlanSubscriptionStartedToAuthorNotification({ required this.type, @@ -20,15 +21,17 @@ class PlanSubscriptionStartedToAuthorNotification implements BaseNotificationMod required this.customerName, required this.chatId, required this.isRead, + required this.createdAt, }); - factory PlanSubscriptionStartedToAuthorNotification.fromJson(Map json, NotificationType type, {bool? isRead}) { + factory PlanSubscriptionStartedToAuthorNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { return PlanSubscriptionStartedToAuthorNotification( type: type, planName: json['plan_name'], customerName: json['user_name'], chatId: json['chat_id'], isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); } } @@ -40,20 +43,24 @@ class PlanSubscriptionCanceledToAuthorNotification implements BaseNotificationMo final bool isRead; final String planName; final String customerName; + @override + final DateTime createdAt; PlanSubscriptionCanceledToAuthorNotification({ required this.type, required this.planName, required this.customerName, required this.isRead, + required this.createdAt, }); - factory PlanSubscriptionCanceledToAuthorNotification.fromJson(Map json, NotificationType type, {bool? isRead}) { + factory PlanSubscriptionCanceledToAuthorNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { return PlanSubscriptionCanceledToAuthorNotification( type: type, planName: json['plan_name'], customerName: json['user_name'], isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); } } @@ -65,20 +72,18 @@ class ChosenPlanStartedToClientNotification implements BaseNotificationModel { final bool isRead; final String planName; final int planId; + @override + final DateTime createdAt; - ChosenPlanStartedToClientNotification({ - required this.type, - required this.planName, - required this.planId, - required this.isRead, - }); + ChosenPlanStartedToClientNotification({required this.type, required this.planName, required this.planId, required this.isRead, required this.createdAt}); - factory ChosenPlanStartedToClientNotification.fromJson(Map json, NotificationType type, {bool? isRead}) { + factory ChosenPlanStartedToClientNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { return ChosenPlanStartedToClientNotification( type: type, planName: json['plan_name'], planId: json['plan_id'], isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); } } @@ -89,15 +94,18 @@ class ChosenPlanTomorrowReminderToClientNotification implements BaseNotification @override final bool isRead; final String planName; + @override + final DateTime createdAt; - ChosenPlanTomorrowReminderToClientNotification({ - required this.type, - required this.planName, - required this.isRead, - }); + ChosenPlanTomorrowReminderToClientNotification({required this.type, required this.planName, required this.isRead, required this.createdAt}); - factory ChosenPlanTomorrowReminderToClientNotification.fromJson(Map json, NotificationType type, {bool? isRead}) { - return ChosenPlanTomorrowReminderToClientNotification(type: type, planName: json['plan_name'], isRead: isRead ?? false); + factory ChosenPlanTomorrowReminderToClientNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { + return ChosenPlanTomorrowReminderToClientNotification( + type: type, + planName: json['plan_name'], + isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), + ); } } @@ -106,11 +114,17 @@ class DiaryReminderNotification implements BaseNotificationModel { final NotificationType type; @override final bool isRead; + @override + final DateTime createdAt; - DiaryReminderNotification({required this.type, required this.isRead}); + DiaryReminderNotification({required this.type, required this.isRead, required this.createdAt}); - factory DiaryReminderNotification.fromJson(Map json, NotificationType type, {bool? isRead}) { - return DiaryReminderNotification(type: type, isRead: isRead ?? false); + factory DiaryReminderNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { + return DiaryReminderNotification( + type: type, + isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), + ); } } @@ -120,18 +134,17 @@ class ConsultantLeftCommentNotification implements BaseNotificationModel { @override final bool isRead; final DateTime date; + @override + final DateTime createdAt; - ConsultantLeftCommentNotification({ - required this.type, - required this.date, - required this.isRead, - }); + ConsultantLeftCommentNotification({required this.type, required this.date, required this.isRead, required this.createdAt}); - factory ConsultantLeftCommentNotification.fromJson(Map json, NotificationType type, {bool? isRead}) { + factory ConsultantLeftCommentNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { return ConsultantLeftCommentNotification( type: type, - date: parseDate(json['diary_date']), + date: parseDate(json['diary_date'] ?? DateTime.now().toIso8601String()), isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); } } @@ -141,16 +154,16 @@ class PersonalizedDietIsDefinedNotification implements BaseNotificationModel { final NotificationType type; @override final bool isRead; + @override + final DateTime createdAt; - PersonalizedDietIsDefinedNotification({ - required this.type, - required this.isRead, - }); + PersonalizedDietIsDefinedNotification({required this.type, required this.isRead, required this.createdAt}); - factory PersonalizedDietIsDefinedNotification.fromJson(Map json, NotificationType type, {bool? isRead}) { + factory PersonalizedDietIsDefinedNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { return PersonalizedDietIsDefinedNotification( type: type, isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); } } @@ -161,17 +174,16 @@ class ChosenPlanFinishedToClientNotification implements BaseNotificationModel { @override final bool isRead; final String planName; - ChosenPlanFinishedToClientNotification({ - required this.type, - required this.planName, - required this.isRead, - }); + @override + final DateTime createdAt; + ChosenPlanFinishedToClientNotification({required this.type, required this.planName, required this.isRead, required this.createdAt}); - factory ChosenPlanFinishedToClientNotification.fromJson(Map json, NotificationType type, {bool? isRead}) { + factory ChosenPlanFinishedToClientNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { return ChosenPlanFinishedToClientNotification( type: type, planName: json['plan_name'], isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); } } @@ -182,17 +194,16 @@ class PlanSubscriptionCanceledToCustomerNotification implements BaseNotification @override final bool isRead; final String planName; - PlanSubscriptionCanceledToCustomerNotification({ - required this.type, - required this.planName, - required this.isRead, - }); + @override + final DateTime createdAt; + PlanSubscriptionCanceledToCustomerNotification({required this.type, required this.planName, required this.isRead, required this.createdAt}); - factory PlanSubscriptionCanceledToCustomerNotification.fromJson(Map json, NotificationType type, {bool? isRead}) { + factory PlanSubscriptionCanceledToCustomerNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { return PlanSubscriptionCanceledToCustomerNotification( type: type, planName: json['plan_name'], isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); } } @@ -204,19 +215,23 @@ class ChosenPlanStartedToConsultantNotification implements BaseNotificationModel final bool isRead; final String planName; final String customerName; + @override + final DateTime createdAt; ChosenPlanStartedToConsultantNotification({ required this.type, required this.isRead, required this.planName, required this.customerName, + required this.createdAt, }); - factory ChosenPlanStartedToConsultantNotification.fromJson(Map json, NotificationType type, {bool? isRead}) { + factory ChosenPlanStartedToConsultantNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { return ChosenPlanStartedToConsultantNotification( type: type, isRead: isRead ?? false, planName: json['plan_name'], customerName: json['client_name'], + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); } } @@ -228,19 +243,23 @@ class ChosenPlanTomorrowReminderToConsultantNotification implements BaseNotifica final bool isRead; final String planName; final String customerName; + @override + final DateTime createdAt; ChosenPlanTomorrowReminderToConsultantNotification({ required this.type, required this.planName, required this.customerName, required this.isRead, + required this.createdAt, }); - factory ChosenPlanTomorrowReminderToConsultantNotification.fromJson(Map json, NotificationType type, {bool? isRead}) { + factory ChosenPlanTomorrowReminderToConsultantNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { return ChosenPlanTomorrowReminderToConsultantNotification( type: type, planName: json['plan_name'], customerName: json['client_name'], isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); } } @@ -252,19 +271,23 @@ class PlanSubscriptionPaidToAuthorNotification implements BaseNotificationModel final bool isRead; final String planName; final String customerName; + @override + final DateTime createdAt; PlanSubscriptionPaidToAuthorNotification({ required this.type, required this.isRead, required this.planName, required this.customerName, + required this.createdAt, }); - factory PlanSubscriptionPaidToAuthorNotification.fromJson(Map json, NotificationType type, {bool? isRead}) { + factory PlanSubscriptionPaidToAuthorNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { return PlanSubscriptionPaidToAuthorNotification( type: type, isRead: isRead ?? false, planName: json['plan_name'], customerName: json['user_name'], + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); } } @@ -277,21 +300,25 @@ class ChosenPlanFinishedToConsultantNotification implements BaseNotificationMode final String planName; final String customerName; final int planId; + @override + final DateTime createdAt; ChosenPlanFinishedToConsultantNotification({ required this.type, required this.planName, required this.customerName, required this.planId, required this.isRead, + required this.createdAt, }); - factory ChosenPlanFinishedToConsultantNotification.fromJson(Map json, NotificationType type, {bool? isRead}) { + factory ChosenPlanFinishedToConsultantNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { return ChosenPlanFinishedToConsultantNotification( type: type, planName: json['plan_name'], customerName: json['client_name'], planId: json['plan_id'], isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); } } @@ -301,16 +328,212 @@ class ChosenPlanLastDayFinishedForUser implements BaseNotificationModel { final NotificationType type; @override final bool isRead; + @override + final DateTime createdAt; - ChosenPlanLastDayFinishedForUser({ - required this.type, - required this.isRead, - }); + ChosenPlanLastDayFinishedForUser({required this.type, required this.isRead, required this.createdAt}); - factory ChosenPlanLastDayFinishedForUser.fromJson(Map json, NotificationType type, {bool? isRead}) { + factory ChosenPlanLastDayFinishedForUser.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { return ChosenPlanLastDayFinishedForUser( type: type, isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), + ); + } +} + +class ChosenPlanConsultantLastDayStarted implements BaseNotificationModel { + @override + final NotificationType type; + @override + final bool isRead; + final String planName; + final int planId; + final String chatId; + final String chatUrl; + @override + final DateTime createdAt; + + ChosenPlanConsultantLastDayStarted({ + required this.type, + required this.planName, + required this.planId, + required this.chatId, + required this.chatUrl, + required this.isRead, + required this.createdAt, + }); + + factory ChosenPlanConsultantLastDayStarted.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { + return ChosenPlanConsultantLastDayStarted( + type: type, + planName: json['plan_name'], + planId: json['plan_id'], + chatId: json['chat_id'], + chatUrl: json['chat_url'], + isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), + ); + } +} + +class ComplaintToModerationForUser implements BaseNotificationModel { + @override + final NotificationType type; + @override + final bool isRead; + final String message; + final int entityId; + final String entityType; + @override + final DateTime createdAt; + + ComplaintToModerationForUser({ + required this.type, + required this.message, + required this.entityId, + required this.entityType, + required this.isRead, + required this.createdAt, + }); + + factory ComplaintToModerationForUser.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { + return ComplaintToModerationForUser( + type: type, + message: json['message'], + entityId: json['entity_id'], + entityType: json['entity_type'], + isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), + ); + } +} + +class CommentReplied implements BaseNotificationModel { + @override + final NotificationType type; + @override + final bool isRead; + final String entityName; + final int commentedEntityId; + final String commentedEntityType; + final String author; + final int authorId; + final int parentCommentId; + @override + final DateTime createdAt; + + CommentReplied({ + required this.type, + required this.entityName, + required this.commentedEntityId, + required this.commentedEntityType, + required this.author, + required this.authorId, + required this.parentCommentId, + required this.isRead, + required this.createdAt, + }); + + factory CommentReplied.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { + return CommentReplied( + type: type, + entityName: json['entity_name'], + commentedEntityId: json['commented_entity_id'], + commentedEntityType: json['commented_entity_type'], + author: json['author'], + authorId: json['author_id'], + parentCommentId: json['parrent_comment_id'], + isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), + ); + } +} + +class ScoreAdded implements BaseNotificationModel { + @override + final NotificationType type; + @override + final bool isRead; + final String scoreKey; + final int scorableId; + final String scorableType; + final String scorableName; + final String scorableSlug; + final int scoreValue; + @override + final DateTime createdAt; + + ScoreAdded({ + required this.type, + required this.scoreKey, + required this.scorableId, + required this.scorableType, + required this.scorableName, + required this.scorableSlug, + required this.scoreValue, + required this.isRead, + required this.createdAt, + }); + + factory ScoreAdded.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { + return ScoreAdded( + type: type, + scoreKey: json['score_key'], + scorableId: json['scorable_id'], + scorableType: json['scorable_type'], + scorableName: json['scorable_name'] ?? '', + scorableSlug: json['scorable_slug'] ?? '', + scoreValue: json['score_value'], + isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), + ); + } +} + +class EntityCommented implements BaseNotificationModel { + @override + final NotificationType type; + @override + final bool isRead; + final String entityName; + final int entityId; + final String entityType; + final String entityTranslatedType; + final String entityUrl; + final String authorName; + final int authorId; + final String authorSlug; + @override + final DateTime createdAt; + + EntityCommented({ + required this.type, + required this.entityName, + required this.entityId, + required this.entityType, + required this.entityTranslatedType, + required this.entityUrl, + required this.authorName, + required this.authorId, + required this.authorSlug, + required this.isRead, + required this.createdAt, + }); + + factory EntityCommented.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { + return EntityCommented( + type: type, + entityName: json['entity_name'], + entityId: json['entity_id'], + entityType: json['entity_type'], + entityTranslatedType: json['entity_traslated_type'], + entityUrl: json['entity_url'], + authorName: json['author_name'], + authorId: json['author_id'], + authorSlug: json['author_slug'], + isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); } } @@ -320,11 +543,10 @@ class UnknownNotification implements BaseNotificationModel { final NotificationType type; @override final bool isRead; + @override + final DateTime createdAt; - UnknownNotification({ - required this.type, - this.isRead = true, - }); + UnknownNotification({required this.type, this.isRead = true, DateTime? createdAt}) : createdAt = createdAt ?? DateTime.fromMillisecondsSinceEpoch(0); } enum NotificationType { @@ -342,23 +564,37 @@ enum NotificationType { planSubscriptionPaidToAuthor, chosenPlanFinishedToConsultant, chosenPlanLastDayFinishedForUser, + // Новые типы + chosenPlanConsultantLastDayStarted, + complaintToModerationForUser, + commentReplied, + scoreAdded, + entityCommented, unknown; static NotificationType getTypeFromString(String type) => switch (type) { - 'App\\Notifications\\Plan\\PlanSubscriptionStartedToAuthor' => NotificationType.planSubscriptionStartedToAuthor, - 'App\\Notifications\\Plan\\PlanSubscriptionCanceledToAuthor' => NotificationType.planSubscriptionCanceledToAuthor, - 'App\\Notifications\\Plan\\ChosenPlanStartedToClientNotification' => NotificationType.chosenPlanStartedToClientNotification, - 'App\\Notifications\\Plan\\ChosenPlanTomorrowReminderToClientNotification' => NotificationType.chosenPlanStartedToClientNotification, - 'App\\Notifications\\DiaryReminderNotification' => NotificationType.diaryReminderNotification, - 'App\\Notifications\\Plan\\ConsultantLeftComment' => NotificationType.consultantLeftComment, - 'App\\Notifications\\Plan\\PersonalizedDietIsDefined' => NotificationType.personalizedDietIsDefined, - 'App\\Notifications\\Plan\\ChosenPlanFinishedToClient' => NotificationType.chosenPlanFinishedToClient, - 'App\\Notifications\\Plan\\PlanSubscriptionCanceledToCustomer' => NotificationType.planSubscriptionCanceledToCustomer, - 'App\\Notifications\\Plan\\ChosenPlanStartedToConsultantNotification' => NotificationType.chosenPlanStartedToConsultantNotification, - 'App\\Notifications\\Plan\\ChosenPlanTomorrowReminderToConsultantNotification' => NotificationType.chosenPlanTomorrowReminderToConsultantNotification, - 'App\\Notifications\\Plan\\PlanSubscriptionPaidToAuthor' => NotificationType.planSubscriptionPaidToAuthor, - 'App\\Notifications\\Plan\\ChosenPlanFinishedToConsultant' => NotificationType.chosenPlanFinishedToConsultant, - 'App\\Notifications\\ChosenPlan\\LastDayFinishedForUser' => NotificationType.chosenPlanLastDayFinishedForUser, - _ => NotificationType.unknown, - }; + 'App\\Notifications\\Plan\\PlanSubscriptionStartedToAuthor' => NotificationType.planSubscriptionStartedToAuthor, + 'App\\Notifications\\Plan\\PlanSubscriptionCanceledToAuthor' => NotificationType.planSubscriptionCanceledToAuthor, + 'App\\Notifications\\Plan\\ChosenPlanStartedToClientNotification' => NotificationType.chosenPlanStartedToClientNotification, + 'App\\Notifications\\Plan\\ChosenPlanTomorrowReminderToClientNotification' => NotificationType.chosenPlanStartedToClientNotification, + 'App\\Notifications\\DiaryReminderNotification' => NotificationType.diaryReminderNotification, + 'App\\Notifications\\Plan\\ConsultantLeftComment' => NotificationType.consultantLeftComment, + 'App\\Notifications\\Plan\\PersonalizedDietIsDefined' => NotificationType.personalizedDietIsDefined, + 'App\\Notifications\\Plan\\ChosenPlanFinishedToClient' => NotificationType.chosenPlanFinishedToClient, + 'App\\Notifications\\Plan\\PlanSubscriptionCanceledToCustomer' => NotificationType.planSubscriptionCanceledToCustomer, + 'App\\Notifications\\Plan\\ChosenPlanStartedToConsultantNotification' => NotificationType.chosenPlanStartedToConsultantNotification, + 'App\\Notifications\\Plan\\ChosenPlanTomorrowReminderToConsultantNotification' => NotificationType.chosenPlanTomorrowReminderToConsultantNotification, + 'App\\Notifications\\Plan\\PlanSubscriptionPaidToAuthor' => NotificationType.planSubscriptionPaidToAuthor, + 'App\\Notifications\\Plan\\ChosenPlanFinishedToConsultant' => NotificationType.chosenPlanFinishedToConsultant, + 'App\\Notifications\\ChosenPlan\\LastDayFinishedForUser' => NotificationType.chosenPlanLastDayFinishedForUser, + // Новые маппинги + 'App\\Notifications\\ChosenPlan\\LastDayStartedForConsultant' => NotificationType.chosenPlanConsultantLastDayStarted, + 'App\\Notifications\\ComplaintToModerationNotify' => NotificationType.complaintToModerationForUser, + 'App\\Notifications\\Comment\\Replied' => NotificationType.commentReplied, + 'App\\Notifications\\Score\\ScoreAdded' => NotificationType.scoreAdded, + 'App\\Notifications\\Comment\\EntityCommented' => NotificationType.entityCommented, + _ => NotificationType.unknown, + }; } + +DateTime parseDate(String date) => DateTime.parse(date).toLocal(); diff --git a/lib/features/notifications/presentation/notifications_cubit/notifications_cubit.dart b/lib/features/notifications/presentation/notifications_cubit/notifications_cubit.dart index 61eba9a7..d4e6e17d 100644 --- a/lib/features/notifications/presentation/notifications_cubit/notifications_cubit.dart +++ b/lib/features/notifications/presentation/notifications_cubit/notifications_cubit.dart @@ -13,10 +13,7 @@ class NotificationsCubit extends Cubit { final WsManager wsManager; final NotificationsRepository notificationsRepository; StreamSubscription? _newNotificationsSubscription; - NotificationsCubit({ - required this.wsManager, - required this.notificationsRepository, - }) : super(const NotificationsState.empty()); + NotificationsCubit({required this.wsManager, required this.notificationsRepository}) : super(const NotificationsState.empty()); Future connectToWebSocket() async { await _newNotificationsSubscription?.cancel(); @@ -27,22 +24,95 @@ class NotificationsCubit extends Cubit { BaseNotificationModel _parseNotification(Map notificationJson, NotificationType type) { try { + final createdAt = notificationJson['created_at']; + final isRead = notificationJson['read_at'] != null; + return switch (type) { - NotificationType.planSubscriptionStartedToAuthor => PlanSubscriptionStartedToAuthorNotification.fromJson(notificationJson, type), - NotificationType.planSubscriptionCanceledToAuthor => PlanSubscriptionCanceledToAuthorNotification.fromJson(notificationJson, type), - NotificationType.chosenPlanStartedToClientNotification => ChosenPlanStartedToClientNotification.fromJson(notificationJson, type), - NotificationType.chosenPlanTomorrowReminderToClientNotification => ChosenPlanTomorrowReminderToClientNotification.fromJson(notificationJson, type), - NotificationType.diaryReminderNotification => DiaryReminderNotification.fromJson(notificationJson, type), - NotificationType.consultantLeftComment => ConsultantLeftCommentNotification.fromJson(notificationJson, type), - NotificationType.personalizedDietIsDefined => PersonalizedDietIsDefinedNotification.fromJson(notificationJson, type), - NotificationType.chosenPlanFinishedToClient => ChosenPlanFinishedToClientNotification.fromJson(notificationJson, type), - NotificationType.planSubscriptionCanceledToCustomer => PlanSubscriptionCanceledToAuthorNotification.fromJson(notificationJson, type), - NotificationType.chosenPlanStartedToConsultantNotification => ChosenPlanStartedToConsultantNotification.fromJson(notificationJson, type), - NotificationType.chosenPlanTomorrowReminderToConsultantNotification => - ChosenPlanTomorrowReminderToConsultantNotification.fromJson(notificationJson, type), - NotificationType.planSubscriptionPaidToAuthor => PlanSubscriptionPaidToAuthorNotification.fromJson(notificationJson, type), - NotificationType.chosenPlanFinishedToConsultant => ChosenPlanFinishedToConsultantNotification.fromJson(notificationJson, type), - NotificationType.chosenPlanLastDayFinishedForUser => ChosenPlanLastDayFinishedForUser.fromJson(notificationJson, type), + NotificationType.planSubscriptionStartedToAuthor => PlanSubscriptionStartedToAuthorNotification.fromJson( + notificationJson, + type, + isRead: isRead, + createdAt: createdAt, + ), + NotificationType.planSubscriptionCanceledToAuthor => PlanSubscriptionCanceledToAuthorNotification.fromJson( + notificationJson, + type, + isRead: isRead, + createdAt: createdAt, + ), + NotificationType.chosenPlanStartedToClientNotification => ChosenPlanStartedToClientNotification.fromJson( + notificationJson, + type, + isRead: isRead, + createdAt: createdAt, + ), + NotificationType.chosenPlanTomorrowReminderToClientNotification => ChosenPlanTomorrowReminderToClientNotification.fromJson( + notificationJson, + type, + isRead: isRead, + createdAt: createdAt, + ), + NotificationType.chosenPlanConsultantLastDayStarted => ChosenPlanConsultantLastDayStarted.fromJson( + notificationJson, + type, + isRead: isRead, + createdAt: createdAt, + ), + NotificationType.diaryReminderNotification => DiaryReminderNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt), + NotificationType.consultantLeftComment => ConsultantLeftCommentNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt), + NotificationType.personalizedDietIsDefined => PersonalizedDietIsDefinedNotification.fromJson( + notificationJson, + type, + isRead: isRead, + createdAt: createdAt, + ), + NotificationType.chosenPlanFinishedToClient => ChosenPlanFinishedToClientNotification.fromJson( + notificationJson, + type, + isRead: isRead, + createdAt: createdAt, + ), + NotificationType.planSubscriptionCanceledToCustomer => PlanSubscriptionCanceledToAuthorNotification.fromJson( + notificationJson, + type, + isRead: isRead, + createdAt: createdAt, + ), + NotificationType.chosenPlanStartedToConsultantNotification => ChosenPlanStartedToConsultantNotification.fromJson( + notificationJson, + type, + isRead: isRead, + createdAt: createdAt, + ), + NotificationType.chosenPlanTomorrowReminderToConsultantNotification => ChosenPlanTomorrowReminderToConsultantNotification.fromJson( + notificationJson, + type, + isRead: isRead, + createdAt: createdAt, + ), + NotificationType.planSubscriptionPaidToAuthor => PlanSubscriptionPaidToAuthorNotification.fromJson( + notificationJson, + type, + isRead: isRead, + createdAt: createdAt, + ), + NotificationType.chosenPlanFinishedToConsultant => ChosenPlanFinishedToConsultantNotification.fromJson( + notificationJson, + type, + isRead: isRead, + createdAt: createdAt, + ), + NotificationType.chosenPlanLastDayFinishedForUser => ChosenPlanLastDayFinishedForUser.fromJson( + notificationJson, + type, + isRead: isRead, + createdAt: createdAt, + ), + // New notification types + NotificationType.complaintToModerationForUser => ComplaintToModerationForUser.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt), + NotificationType.commentReplied => CommentReplied.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt), + NotificationType.scoreAdded => ScoreAdded.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt), + NotificationType.entityCommented => EntityCommented.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt), NotificationType.unknown => UnknownNotification(type: type), }; } catch (e) { @@ -56,32 +126,80 @@ class NotificationsCubit extends Cubit { final type = NotificationType.getTypeFromString(stringifiedType); final notification = _parseNotification(json, type); _notifications.insert(0, notification); - emit( - NotificationsState.loaded( - notifications: _notifications, - notificationsCount: _notifications.length, - hasUnreadNotifications: true, - ), - ); + + final currentState = state; + if (currentState is NotificationsStateLoaded) { + emit(currentState.copyWith(notifications: _notifications, notificationsCount: _notifications.length, hasUnreadNotifications: true)); + } } Future getNotifications() async { try { - final notifications = await notificationsRepository.getNotifications(page: 1); - _notifications.clear(); - _notifications.addAll(notifications ?? []); - emit( - NotificationsState.loaded( - notifications: _notifications, - notificationsCount: _notifications.length, - hasUnreadNotifications: (notifications ?? []).where((elem) => elem.isRead == false).isNotEmpty, - ), - ); + final result = await notificationsRepository.getNotifications(page: 1); + if (result != null) { + final notifications = result['notifications'] as List; + final pagination = result['pagination'] as Map; + + _notifications.clear(); + _notifications.addAll(notifications); + + final currentPage = pagination['current_page'] as int; + final lastPage = pagination['last_page'] as int; + final hasNextPage = currentPage < lastPage; + + emit( + NotificationsState.loaded( + notifications: _notifications, + notificationsCount: _notifications.length, + hasUnreadNotifications: notifications.where((elem) => elem.isRead == false).isNotEmpty, + currentPage: currentPage, + hasNextPage: hasNextPage, + ), + ); + } } catch (e, st) { log(e.toString(), stackTrace: st); } } + Future loadMoreNotifications() async { + final currentState = state; + if (currentState is NotificationsStateLoaded && currentState.hasNextPage && !currentState.isLoadingMore) { + try { + emit(currentState.copyWith(isLoadingMore: true)); + + final nextPage = currentState.currentPage + 1; + final result = await notificationsRepository.getNotifications(page: nextPage); + + if (result != null) { + final newNotifications = result['notifications'] as List; + final pagination = result['pagination'] as Map; + + _notifications.addAll(newNotifications); + + final currentPage = pagination['current_page'] as int; + final lastPage = pagination['last_page'] as int; + final hasNextPage = currentPage < lastPage; + + emit( + currentState.copyWith( + notifications: _notifications, + notificationsCount: _notifications.length, + currentPage: currentPage, + hasNextPage: hasNextPage, + isLoadingMore: false, + ), + ); + } else { + emit(currentState.copyWith(isLoadingMore: false)); + } + } catch (e, st) { + log(e.toString(), stackTrace: st); + emit(currentState.copyWith(isLoadingMore: false)); + } + } + } + Future markNotificationsAsRead() async { final currentState = state; if (currentState is NotificationsStateLoaded) { diff --git a/lib/features/notifications/presentation/notifications_cubit/notifications_state.dart b/lib/features/notifications/presentation/notifications_cubit/notifications_state.dart index 9c221b31..391e199e 100644 --- a/lib/features/notifications/presentation/notifications_cubit/notifications_state.dart +++ b/lib/features/notifications/presentation/notifications_cubit/notifications_state.dart @@ -14,6 +14,9 @@ abstract class NotificationsState with _$NotificationsState { required int notificationsCount, @Default(false) bool isLoading, @Default(false) bool hasUnreadNotifications, + @Default(1) int currentPage, + @Default(true) bool hasNextPage, + @Default(false) bool isLoadingMore, }) = NotificationsStateLoaded; const factory NotificationsState.error({ diff --git a/lib/features/notifications/presentation/notifications_screen.dart b/lib/features/notifications/presentation/notifications_screen.dart index ff48617f..58e3c65f 100644 --- a/lib/features/notifications/presentation/notifications_screen.dart +++ b/lib/features/notifications/presentation/notifications_screen.dart @@ -10,9 +10,38 @@ import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -class NotificationsScreen extends StatelessWidget { +class NotificationsScreen extends StatefulWidget { const NotificationsScreen({super.key}); + @override + State createState() => _NotificationsScreenState(); +} + +class _NotificationsScreenState extends State { + final ScrollController _scrollController = ScrollController(); + + @override + void initState() { + super.initState(); + _scrollController.addListener(_onScroll); + } + + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + + void _onScroll() { + if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 200) { + final cubit = context.read(); + final state = cubit.state; + if (state is NotificationsStateLoaded && state.hasNextPage && !state.isLoadingMore) { + cubit.loadMoreNotifications(); + } + } + } + @override Widget build(BuildContext context) { final s = S.of(context); @@ -20,62 +49,65 @@ class NotificationsScreen extends StatelessWidget { builder: (BuildContext context, NotificationsState state) { return Scaffold( backgroundColor: appTheme.gray100, - appBar: AppBar( - title: const Text('Уведомления'), - ), + appBar: AppBar(title: const Text('Уведомления')), body: state is NotificationsStateLoaded - ? Stack(children: [ - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Container( - padding: const EdgeInsets.only(top: 16), - decoration: BoxDecoration( - color: appTheme.whiteA700, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(12), - topRight: Radius.circular(12), + ? Stack( + children: [ + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Container( + padding: const EdgeInsets.only(top: 16), + decoration: BoxDecoration( + color: appTheme.whiteA700, + borderRadius: const BorderRadius.only(topLeft: Radius.circular(12), topRight: Radius.circular(12)), + ), + child: state.isLoading + ? const Center(child: Loader()) + : CustomScrollView( + controller: _scrollController, + slivers: [ + SliverList.separated( + separatorBuilder: (context, index) => + const Padding(padding: EdgeInsets.symmetric(vertical: 12, horizontal: 16), child: CustomDivider()), + itemBuilder: (context, index) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: NotificationWidget(notification: state.notifications[index]), + ); + }, + itemCount: state.notifications.length, + ), + if (state.isLoadingMore) + const SliverToBoxAdapter( + child: Padding( + padding: EdgeInsets.all(16.0), + child: Center(child: Loader()), + ), + ), + ], + ), + ), + ), + Align( + alignment: Alignment.bottomCenter, + child: Padding( + padding: const EdgeInsets.only(left: 16, right: 16, bottom: 16), + child: CustomOutlinedButton( + text: s.markAsRead, + height: 52, + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), + buttonStyle: ButtonStyle( + side: const WidgetStatePropertyAll(BorderSide.none), + backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), + ), + onPressed: () async { + context.read().markNotificationsAsRead(); + }, ), ), - child: state.isLoading - ? const Center(child: Loader()) - : CustomScrollView( - slivers: [ - SliverList.separated( - separatorBuilder: (context, index) => const Padding( - padding: EdgeInsets.symmetric(vertical: 12, horizontal: 16), - child: CustomDivider(), - ), - itemBuilder: (context, index) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: NotificationWidget( - notification: state.notifications[index], - ), - ); - }, - itemCount: state.notifications.length, - ) - ], - ), ), - ), - Align( - alignment: Alignment.bottomCenter, - child: Padding( - padding: const EdgeInsets.only(left: 16, right: 16, bottom: 16), - child: CustomOutlinedButton( - text: s.markAsRead, - height: 52, - buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), - buttonStyle: - ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.blueA400)), - onPressed: () async { - context.read().markNotificationsAsRead(); - }, - ), - ), - ) - ]) + ], + ) : const Placeholder(), ); }, diff --git a/lib/features/notifications/presentation/widgets/notification_widget.dart b/lib/features/notifications/presentation/widgets/notification_widget.dart index 66b6cf18..18c02a13 100644 --- a/lib/features/notifications/presentation/widgets/notification_widget.dart +++ b/lib/features/notifications/presentation/widgets/notification_widget.dart @@ -15,46 +15,39 @@ class NotificationWidget extends StatelessWidget { Widget build(BuildContext context) { return switch (notification) { PlanSubscriptionStartedToAuthorNotification() => _PlanSubscriptionStartedToAuthorWidget( - notification: notification as PlanSubscriptionStartedToAuthorNotification, - ), + notification: notification as PlanSubscriptionStartedToAuthorNotification, + ), PlanSubscriptionCanceledToAuthorNotification() => _PlanSubscriptionCanceledToAuthorWidget( - notification: notification as PlanSubscriptionCanceledToAuthorNotification, - ), - ChosenPlanStartedToClientNotification() => _ChosenPlanStartedToClientWidget( - notification: notification as ChosenPlanStartedToClientNotification, - ), + notification: notification as PlanSubscriptionCanceledToAuthorNotification, + ), + ChosenPlanStartedToClientNotification() => _ChosenPlanStartedToClientWidget(notification: notification as ChosenPlanStartedToClientNotification), ChosenPlanTomorrowReminderToClientNotification() => _ChosenPlanTomorrowReminderToClientWidget( - notification: notification as ChosenPlanTomorrowReminderToClientNotification, - ), - DiaryReminderNotification() => _DiaryReminderWidget( - notification: notification as DiaryReminderNotification, - ), - ConsultantLeftCommentNotification() => _ConsultantLeftCommentWidget( - notification: notification as ConsultantLeftCommentNotification, - ), - PersonalizedDietIsDefinedNotification() => _PersonalizedDietIsDefinedWidget( - notification: notification as PersonalizedDietIsDefinedNotification, - ), - ChosenPlanFinishedToClientNotification() => _ChosenPlanFinishedToClientWidget( - notification: notification as ChosenPlanFinishedToClientNotification, - ), + notification: notification as ChosenPlanTomorrowReminderToClientNotification, + ), + DiaryReminderNotification() => _DiaryReminderWidget(notification: notification as DiaryReminderNotification), + ConsultantLeftCommentNotification() => _ConsultantLeftCommentWidget(notification: notification as ConsultantLeftCommentNotification), + PersonalizedDietIsDefinedNotification() => _PersonalizedDietIsDefinedWidget(notification: notification as PersonalizedDietIsDefinedNotification), + ChosenPlanFinishedToClientNotification() => _ChosenPlanFinishedToClientWidget(notification: notification as ChosenPlanFinishedToClientNotification), PlanSubscriptionCanceledToCustomerNotification() => _PlanSubscriptionCanceledToCustomerWidget( - notification: notification as PlanSubscriptionCanceledToCustomerNotification, - ), + notification: notification as PlanSubscriptionCanceledToCustomerNotification, + ), ChosenPlanStartedToConsultantNotification() => _ChosenPlanStartedToConsultantWidget( - notification: notification as ChosenPlanStartedToConsultantNotification, - ), + notification: notification as ChosenPlanStartedToConsultantNotification, + ), ChosenPlanTomorrowReminderToConsultantNotification() => _ChosenPlanTomorrowReminderToConsultantWidget( - notification: notification as ChosenPlanTomorrowReminderToConsultantNotification, - ), - PlanSubscriptionPaidToAuthorNotification() => _PlanSubscriptionPaidToAuthorWidget( - notification: notification as PlanSubscriptionPaidToAuthorNotification, - ), + notification: notification as ChosenPlanTomorrowReminderToConsultantNotification, + ), + PlanSubscriptionPaidToAuthorNotification() => _PlanSubscriptionPaidToAuthorWidget(notification: notification as PlanSubscriptionPaidToAuthorNotification), ChosenPlanFinishedToConsultantNotification() => _ChosenPlanFinishedToConsultantWidget( - notification: notification as ChosenPlanFinishedToConsultantNotification, - ), - ChosenPlanLastDayFinishedForUser() => Text((notification as ChosenPlanLastDayFinishedForUser).type.toString()), - UnknownNotification() => const SizedBox.shrink(), + notification: notification as ChosenPlanFinishedToConsultantNotification, + ), + ChosenPlanLastDayFinishedForUser() => _ChosenPlanLastDayFinishedForUserWidget(notification: notification as ChosenPlanLastDayFinishedForUser), + ChosenPlanConsultantLastDayStarted() => _ChosenPlanConsultantLastDayStartedWidget(notification: notification as ChosenPlanConsultantLastDayStarted), + ComplaintToModerationForUser() => _ComplaintToModerationForUserWidget(notification: notification as ComplaintToModerationForUser), + CommentReplied() => _CommentRepliedWidget(notification: notification as CommentReplied), + ScoreAdded() => _ScoreAddedWidget(notification: notification as ScoreAdded), + EntityCommented() => _EntityCommentedWidget(notification: notification as EntityCommented), + UnknownNotification() => _UnknownNotificationWidget(notification: notification as UnknownNotification), }; } } @@ -69,6 +62,7 @@ class _PlanSubscriptionStartedToAuthorWidget extends StatelessWidget { return _BaseNotificationWidget( isRead: notification.isRead, notificationsText: s.userSubscribedToPlan(notification.customerName, notification.planName), + createdAt: notification.createdAt, ); } } @@ -83,6 +77,7 @@ class _PlanSubscriptionCanceledToAuthorWidget extends StatelessWidget { return _BaseNotificationWidget( isRead: notification.isRead, notificationsText: s.userCanceldThePlanSubscription(notification.customerName, notification.planName), + createdAt: notification.createdAt, ); } } @@ -94,10 +89,7 @@ class _ChosenPlanStartedToClientWidget extends StatelessWidget { Widget build(BuildContext context) { final s = S.of(context); - return _BaseNotificationWidget( - isRead: notification.isRead, - notificationsText: s.planStarted(notification.planName), - ); + return _BaseNotificationWidget(isRead: notification.isRead, notificationsText: s.planStarted(notification.planName), createdAt: notification.createdAt); } } @@ -111,6 +103,7 @@ class _ChosenPlanTomorrowReminderToClientWidget extends StatelessWidget { return _BaseNotificationWidget( isRead: notification.isRead, notificationsText: s.tomorrowYourPlanWillStart(notification.planName), + createdAt: notification.createdAt, ); } } @@ -121,10 +114,7 @@ class _DiaryReminderWidget extends StatelessWidget { @override Widget build(BuildContext context) { final s = S.of(context); - return _BaseNotificationWidget( - isRead: notification.isRead, - notificationsText: s.fillYasterday, - ); + return _BaseNotificationWidget(isRead: notification.isRead, notificationsText: s.fillYasterday, createdAt: notification.createdAt); } } @@ -138,6 +128,7 @@ class _ConsultantLeftCommentWidget extends StatelessWidget { return _BaseNotificationWidget( isRead: notification.isRead, notificationsText: s.consultantLeftComment(notification.date.format('dd.MM.yyyy')), + createdAt: notification.createdAt, ); } } @@ -147,7 +138,7 @@ class _PersonalizedDietIsDefinedWidget extends StatelessWidget { final PersonalizedDietIsDefinedNotification notification; @override Widget build(BuildContext context) { - return Container(); + return _BaseNotificationWidget(isRead: notification.isRead, notificationsText: 'Персональная диета определена', createdAt: notification.createdAt); } } @@ -161,6 +152,7 @@ class _ChosenPlanFinishedToClientWidget extends StatelessWidget { return _BaseNotificationWidget( isRead: notification.isRead, notificationsText: '${s.congratsYouHaveFinishedPlan} «${notification.planName}»', + createdAt: notification.createdAt, ); } } @@ -175,6 +167,7 @@ class _PlanSubscriptionCanceledToCustomerWidget extends StatelessWidget { return _BaseNotificationWidget( isRead: notification.isRead, notificationsText: s.youCanceledSubscriptionToPlan(notification.planName), + createdAt: notification.createdAt, ); } } @@ -189,6 +182,7 @@ class _ChosenPlanStartedToConsultantWidget extends StatelessWidget { return _BaseNotificationWidget( isRead: notification.isRead, notificationsText: s.planWithUserStarted(notification.planName, notification.customerName), + createdAt: notification.createdAt, ); } } @@ -203,6 +197,7 @@ class _ChosenPlanTomorrowReminderToConsultantWidget extends StatelessWidget { return _BaseNotificationWidget( isRead: notification.isRead, notificationsText: s.tommorowPlanWithUserWillStart(notification.planName, notification.customerName), + createdAt: notification.createdAt, ); } } @@ -217,6 +212,7 @@ class _PlanSubscriptionPaidToAuthorWidget extends StatelessWidget { return _BaseNotificationWidget( isRead: notification.isRead, notificationsText: s.userPaidThePlan(notification.customerName, notification.planName), + createdAt: notification.createdAt, ); } } @@ -227,58 +223,223 @@ class _ChosenPlanFinishedToConsultantWidget extends StatelessWidget { @override Widget build(BuildContext context) { final s = S.of(context); - return Column(children: [ - Text( - '${s.congratsYouHaveFinishedPlan} «${notification.planName}»', - style: TextStyles.textMSemibold.copyWith(color: appTheme.black), - ), - const SizedBox(height: 12), - Text( - s.userConfirmedPlanCompletionWithoutSum, - style: TextStyles.textSMedium.copyWith(color: appTheme.black), - ), - const SizedBox(height: 12), - CustomOutlinedButton( - text: s.collectCharge, - 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); - }, - ), - const SizedBox(height: 12), - ]); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('${s.congratsYouHaveFinishedPlan} «${notification.planName}»', style: TextStyles.textMSemibold.copyWith(color: appTheme.black)), + const SizedBox(height: 12), + Text(s.userConfirmedPlanCompletionWithoutSum, style: TextStyles.textSMedium.copyWith(color: appTheme.black)), + const SizedBox(height: 12), + CustomOutlinedButton( + text: s.collectCharge, + 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); + }, + ), + ], + ), + ), + if (!notification.isRead) + Container( + decoration: BoxDecoration(color: appTheme.blueA400, shape: BoxShape.circle), + width: 8, + height: 8, + ), + ], + ), + const SizedBox(height: 4), + Text( + '${notification.createdAt.day} ${_getMonthName(notification.createdAt.month)} ${notification.createdAt.hour.toString().padLeft(2, '0')}:${notification.createdAt.minute.toString().padLeft(2, '0')}', + style: TextStyles.textSMedium.copyWith(color: appTheme.gray600), + ), + ], + ); + } + + String _getMonthName(int month) { + switch (month) { + case 1: + return 'янв'; + case 2: + return 'фев'; + case 3: + return 'мар'; + case 4: + return 'апр'; + case 5: + return 'май'; + case 6: + return 'июн'; + case 7: + return 'июл'; + case 8: + return 'авг'; + case 9: + return 'сен'; + case 10: + return 'окт'; + case 11: + return 'ноя'; + case 12: + return 'дек'; + default: + return ''; + } } } // CardSettingsScreen class _BaseNotificationWidget extends StatelessWidget { - const _BaseNotificationWidget({required this.notificationsText, required this.isRead}); + const _BaseNotificationWidget({required this.notificationsText, required this.isRead, required this.createdAt}); final String notificationsText; final bool isRead; + final DateTime createdAt; @override Widget build(BuildContext context) { - return Row( + return Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox( - width: MediaQuery.sizeOf(context).width * 0.6, - child: Text( - notificationsText, - style: TextStyles.textMMedium.copyWith(color: appTheme.black), - ), - ), - if (!isRead) ...[ - const Spacer(), - Container( - decoration: BoxDecoration( - color: appTheme.blueA400, - shape: BoxShape.circle, + Row( + children: [ + Expanded( + child: Text(notificationsText, style: TextStyles.textMMedium.copyWith(color: appTheme.black)), ), - width: 8, - height: 8, - ) - ] + if (!isRead) + Container( + decoration: BoxDecoration(color: appTheme.blueA400, shape: BoxShape.circle), + width: 8, + height: 8, + ), + ], + ), + const SizedBox(height: 4), + Text( + '${createdAt.day} ${_getMonthName(createdAt.month)} ${createdAt.hour.toString().padLeft(2, '0')}:${createdAt.minute.toString().padLeft(2, '0')}', + style: TextStyles.textSMedium.copyWith(color: appTheme.gray600), + ), ], ); } + + String _getMonthName(int month) { + switch (month) { + case 1: + return 'янв'; + case 2: + return 'фев'; + case 3: + return 'мар'; + case 4: + return 'апр'; + case 5: + return 'май'; + case 6: + return 'июн'; + case 7: + return 'июл'; + case 8: + return 'авг'; + case 9: + return 'сен'; + case 10: + return 'окт'; + case 11: + return 'ноя'; + case 12: + return 'дек'; + default: + return ''; + } + } +} + +class _ChosenPlanLastDayFinishedForUserWidget extends StatelessWidget { + const _ChosenPlanLastDayFinishedForUserWidget({required this.notification}); + final ChosenPlanLastDayFinishedForUser notification; + @override + Widget build(BuildContext context) { + return _BaseNotificationWidget(isRead: notification.isRead, notificationsText: 'Последний день плана завершен', createdAt: notification.createdAt); + } +} + +class _ChosenPlanConsultantLastDayStartedWidget extends StatelessWidget { + const _ChosenPlanConsultantLastDayStartedWidget({required this.notification}); + final ChosenPlanConsultantLastDayStarted notification; + @override + Widget build(BuildContext context) { + return _BaseNotificationWidget( + isRead: notification.isRead, + notificationsText: 'Последний день плана начался: ${notification.planName}', + createdAt: notification.createdAt, + ); + } +} + +class _ComplaintToModerationForUserWidget extends StatelessWidget { + const _ComplaintToModerationForUserWidget({required this.notification}); + final ComplaintToModerationForUser notification; + @override + Widget build(BuildContext context) { + return _BaseNotificationWidget(isRead: notification.isRead, notificationsText: 'Жалоба отправлена на модерацию', createdAt: notification.createdAt); + } +} + +class _CommentRepliedWidget extends StatelessWidget { + const _CommentRepliedWidget({required this.notification}); + final CommentReplied notification; + @override + Widget build(BuildContext context) { + return _BaseNotificationWidget( + isRead: notification.isRead, + notificationsText: 'Ответ на комментарий: ${notification.entityName}', + createdAt: notification.createdAt, + ); + } +} + +class _ScoreAddedWidget extends StatelessWidget { + const _ScoreAddedWidget({required this.notification}); + final ScoreAdded notification; + @override + Widget build(BuildContext context) { + return _BaseNotificationWidget( + isRead: notification.isRead, + notificationsText: 'Добавлены баллы: ${notification.scoreValue}', + createdAt: notification.createdAt, + ); + } +} + +class _EntityCommentedWidget extends StatelessWidget { + const _EntityCommentedWidget({required this.notification}); + final EntityCommented notification; + @override + Widget build(BuildContext context) { + return _BaseNotificationWidget( + isRead: notification.isRead, + notificationsText: 'Комментарий к ${notification.entityTranslatedType}: ${notification.entityName}', + createdAt: notification.createdAt, + ); + } +} + +class _UnknownNotificationWidget extends StatelessWidget { + const _UnknownNotificationWidget({required this.notification}); + final UnknownNotification notification; + @override + Widget build(BuildContext context) { + return _BaseNotificationWidget( + isRead: notification.isRead, + notificationsText: 'Неизвестное уведомление: ${notification.type.toString()}', + createdAt: notification.createdAt, + ); + } } diff --git a/lib/features/notifications/repository/notifications_repository.dart b/lib/features/notifications/repository/notifications_repository.dart index beef645f..267faa89 100644 --- a/lib/features/notifications/repository/notifications_repository.dart +++ b/lib/features/notifications/repository/notifications_repository.dart @@ -1,14 +1,15 @@ -import 'package:eatvio_app/features/notifications/models/notification_model.dart'; +import 'dart:developer'; import 'package:eatvio_app/features/notifications/service/notifications_service.dart'; class NotificationsRepository { final NotificationsService notificationsService; NotificationsRepository({required this.notificationsService}); - Future?> getNotifications({required int page}) { + Future?> getNotifications({required int page}) { try { return notificationsService.getNotifications(page: page); } catch (e) { + log(e.toString()); rethrow; } } diff --git a/lib/features/notifications/service/notifications_service.dart b/lib/features/notifications/service/notifications_service.dart index ec2aad74..61f545ab 100644 --- a/lib/features/notifications/service/notifications_service.dart +++ b/lib/features/notifications/service/notifications_service.dart @@ -5,53 +5,137 @@ class NotificationsService { final Dio dio; NotificationsService(this.dio); - Future?> getNotifications({required int page}) async { + Future?> getNotifications({required int page}) async { try { final response = await dio.get('/api/profile/notifications?page=$page'); if (response.statusCode == 200) { final data = response.data['data'] as List; + final notifications = data.map((notificationJson) { try { final stringifiedType = notificationJson['data']['notification_class_name']; final readDate = notificationJson['read_at']; + final createdAt = notificationJson['created_at']; final type = NotificationType.getTypeFromString(stringifiedType); + return switch (type) { - NotificationType.planSubscriptionStartedToAuthor => - PlanSubscriptionStartedToAuthorNotification.fromJson(notificationJson['data'], type, isRead: readDate != null), - NotificationType.planSubscriptionCanceledToAuthor => - PlanSubscriptionCanceledToAuthorNotification.fromJson(notificationJson['data'], type, isRead: readDate != null), - NotificationType.chosenPlanStartedToClientNotification => - ChosenPlanStartedToClientNotification.fromJson(notificationJson['data'], type, isRead: readDate != null), - NotificationType.chosenPlanTomorrowReminderToClientNotification => - ChosenPlanTomorrowReminderToClientNotification.fromJson(notificationJson['data'], type, isRead: readDate != null), - NotificationType.diaryReminderNotification => DiaryReminderNotification.fromJson(notificationJson['data'], type, isRead: readDate != null), - NotificationType.consultantLeftComment => ConsultantLeftCommentNotification.fromJson(notificationJson['data'], type, isRead: readDate != null), - NotificationType.personalizedDietIsDefined => - PersonalizedDietIsDefinedNotification.fromJson(notificationJson['data'], type, isRead: readDate != null), - NotificationType.chosenPlanFinishedToClient => - ChosenPlanFinishedToClientNotification.fromJson(notificationJson['data'], type, isRead: readDate != null), - NotificationType.planSubscriptionCanceledToCustomer => - PlanSubscriptionCanceledToAuthorNotification.fromJson(notificationJson['data'], type, isRead: readDate != null), - NotificationType.chosenPlanStartedToConsultantNotification => - ChosenPlanStartedToConsultantNotification.fromJson(notificationJson['data'], type, isRead: readDate != null), - NotificationType.chosenPlanTomorrowReminderToConsultantNotification => - ChosenPlanTomorrowReminderToConsultantNotification.fromJson(notificationJson['data'], type, isRead: readDate != null), - NotificationType.planSubscriptionPaidToAuthor => - PlanSubscriptionPaidToAuthorNotification.fromJson(notificationJson['data'], type, isRead: readDate != null), - NotificationType.chosenPlanFinishedToConsultant => - ChosenPlanFinishedToConsultantNotification.fromJson(notificationJson['data'], type, isRead: readDate != null), + NotificationType.planSubscriptionStartedToAuthor => PlanSubscriptionStartedToAuthorNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.planSubscriptionCanceledToAuthor => PlanSubscriptionCanceledToAuthorNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.chosenPlanStartedToClientNotification => ChosenPlanStartedToClientNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.chosenPlanTomorrowReminderToClientNotification => ChosenPlanTomorrowReminderToClientNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.diaryReminderNotification => DiaryReminderNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.consultantLeftComment => ConsultantLeftCommentNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.personalizedDietIsDefined => PersonalizedDietIsDefinedNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.chosenPlanFinishedToClient => ChosenPlanFinishedToClientNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.planSubscriptionCanceledToCustomer => PlanSubscriptionCanceledToAuthorNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.chosenPlanStartedToConsultantNotification => ChosenPlanStartedToConsultantNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.chosenPlanTomorrowReminderToConsultantNotification => ChosenPlanTomorrowReminderToConsultantNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.planSubscriptionPaidToAuthor => PlanSubscriptionPaidToAuthorNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.chosenPlanFinishedToConsultant => ChosenPlanFinishedToConsultantNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), NotificationType.chosenPlanLastDayFinishedForUser => ChosenPlanLastDayFinishedForUser.fromJson( - notificationJson['data'], - type, - isRead: readDate != null, - ), - NotificationType.unknown => UnknownNotification(type: type), + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + // Новые типы уведомлений + NotificationType.chosenPlanConsultantLastDayStarted => ChosenPlanConsultantLastDayStarted.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.complaintToModerationForUser => ComplaintToModerationForUser.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.commentReplied => CommentReplied.fromJson(notificationJson['data'], type, isRead: readDate != null, createdAt: createdAt), + NotificationType.scoreAdded => ScoreAdded.fromJson(notificationJson['data'], type, isRead: readDate != null, createdAt: createdAt), + NotificationType.entityCommented => EntityCommented.fromJson(notificationJson['data'], type, isRead: readDate != null, createdAt: createdAt), + NotificationType.unknown => UnknownNotification(type: type, createdAt: parseDate(createdAt)), }; } catch (e) { - return UnknownNotification(type: NotificationType.unknown); + return UnknownNotification(type: NotificationType.unknown, createdAt: DateTime.now()); } }).toList(); - return notifications.where((notification) => notification.type != NotificationType.unknown).toList(); + + // Извлекаем информацию о пагинации из корня ответа + final currentPage = response.data['current_page'] as int; + final lastPage = response.data['last_page'] as int; + final perPage = response.data['per_page'] as int; + final total = response.data['total'] as int; + + return { + 'notifications': notifications, + 'pagination': {'current_page': currentPage, 'last_page': lastPage, 'per_page': perPage, 'total': total}, + }; } return null; } catch (e) { @@ -67,3 +151,5 @@ class NotificationsService { } } } + +DateTime parseDate(String date) => DateTime.parse(date).toLocal(); diff --git a/lib/main.dart b/lib/main.dart index 8147c18a..6b44348c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -28,7 +28,7 @@ Future main() async { WidgetsBinding widgetsBinding = SentryWidgetsFlutterBinding.ensureInitialized(); FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); - await dotenv.load(fileName: "env/.env"); + await dotenv.load(fileName: "env/.env"); await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); await FirebaseMessaging.instance.requestPermission(); From 76582fd487ab11893a937d0adf5d0a7d6b3ae90e Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Wed, 6 Aug 2025 11:28:31 +0300 Subject: [PATCH 064/134] feat(notifications): Issue-191 --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index b448cede..6960fad1 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ android/build/ ios/XCBuildData/ .env env.example +env/.env From 79861190547149f817622c23b42b13a44176c3be Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Wed, 6 Aug 2025 12:42:37 +0300 Subject: [PATCH 065/134] feat(env): add .env from repo --- .gitea/workflows/test-flight-cd.yaml | 22 ++++++++++++++++++++++ .gitea/workflows/test.yml | 23 +++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/.gitea/workflows/test-flight-cd.yaml b/.gitea/workflows/test-flight-cd.yaml index 4320ecc5..f32dd5e3 100644 --- a/.gitea/workflows/test-flight-cd.yaml +++ b/.gitea/workflows/test-flight-cd.yaml @@ -11,6 +11,28 @@ jobs: - name: Checkout uses: actions/checkout@v2 + - name: Checkout env repository + uses: actions/checkout@v2 + with: + repository: mso7nueh/eatvio_app_env + path: env_repo + + - name: Copy .env file + run: | + mkdir -p env + cp env_repo/.env env/.env + + - name: Load environment variables from .env + run: | + set -a + source env/.env + set +a + # Export all variables to GITHUB_ENV + while IFS= read -r line; do + if [[ $line =~ ^[A-Za-z_][A-Za-z0-9_]*= ]]; then + echo "$line" >> $GITHUB_ENV + fi + done < env/.env - name: Unlock login keychain run: | diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml index 2aeef03a..a9d29311 100644 --- a/.gitea/workflows/test.yml +++ b/.gitea/workflows/test.yml @@ -16,6 +16,29 @@ jobs: steps: - uses: actions/checkout@v3 + - name: Checkout env repository + uses: actions/checkout@v3 + with: + repository: mso7nueh/eatvio_app_env + path: env_repo + + - name: Copy .env file + run: | + mkdir -p env + cp env_repo/.env env/.env + + - name: Load environment variables from .env + run: | + set -a + source env/.env + set +a + # Export all variables to GITHUB_ENV + while IFS= read -r line; do + if [[ $line =~ ^[A-Za-z_][A-Za-z0-9_]*= ]]; then + echo "$line" >> $GITHUB_ENV + fi + done < env/.env + - name: Create key.properties file run: | cat > android/key.properties < Date: Wed, 6 Aug 2025 12:44:19 +0300 Subject: [PATCH 066/134] feat(env): from repo --- .gitea/workflows/test-flight-cd.yaml | 2 +- .gitea/workflows/test.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitea/workflows/test-flight-cd.yaml b/.gitea/workflows/test-flight-cd.yaml index f32dd5e3..f5344c1b 100644 --- a/.gitea/workflows/test-flight-cd.yaml +++ b/.gitea/workflows/test-flight-cd.yaml @@ -14,7 +14,7 @@ jobs: - name: Checkout env repository uses: actions/checkout@v2 with: - repository: mso7nueh/eatvio_app_env + repository: http://gitea.office.local:3000/mso7nueh/eatvio_app_env path: env_repo - name: Copy .env file diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml index a9d29311..4904bfed 100644 --- a/.gitea/workflows/test.yml +++ b/.gitea/workflows/test.yml @@ -19,7 +19,7 @@ jobs: - name: Checkout env repository uses: actions/checkout@v3 with: - repository: mso7nueh/eatvio_app_env + repository: http://gitea.office.local:3000/mso7nueh/eatvio_app_env path: env_repo - name: Copy .env file From b302be66d7be2d23c695ca8ab6672abffeca404f Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Wed, 6 Aug 2025 12:55:21 +0300 Subject: [PATCH 067/134] fix(env): add token --- .gitea/workflows/test-flight-cd.yaml | 5 +++-- .gitea/workflows/test.yml | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.gitea/workflows/test-flight-cd.yaml b/.gitea/workflows/test-flight-cd.yaml index f5344c1b..99b661a0 100644 --- a/.gitea/workflows/test-flight-cd.yaml +++ b/.gitea/workflows/test-flight-cd.yaml @@ -14,8 +14,9 @@ jobs: - name: Checkout env repository uses: actions/checkout@v2 with: - repository: http://gitea.office.local:3000/mso7nueh/eatvio_app_env + repository: mso7nueh/eatvio_app_env path: env_repo + token: ${{ vars.MSO7NUEH_TOKEN }} - name: Copy .env file run: | @@ -58,4 +59,4 @@ jobs: 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 }} - API_APPSTORE_KEY: ${{ vars.API_APPSTORE_KEY }} \ No newline at end of file + API_APPSTORE_KEY: ${{ vars.API_APPSTORE_KEY }} diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml index 4904bfed..1f4c4f03 100644 --- a/.gitea/workflows/test.yml +++ b/.gitea/workflows/test.yml @@ -19,8 +19,9 @@ jobs: - name: Checkout env repository uses: actions/checkout@v3 with: - repository: http://gitea.office.local:3000/mso7nueh/eatvio_app_env + repository: mso7nueh/eatvio_app_env path: env_repo + token: ${{ vars.MSO7NUEH_TOKEN }} - name: Copy .env file run: | From 33151b7daeae98b6fb434901935edab36f9acdcc Mon Sep 17 00:00:00 2001 From: Username1548 Date: Wed, 6 Aug 2025 16:55:42 +0700 Subject: [PATCH 068/134] changed lable of the app and hid user purchases and finaces --- android/app/src/main/AndroidManifest.xml | 2 +- ios/Runner/Info.plist | 2 +- .../menu/presentation/pages/menu_screen.dart | 26 ++++++++++--------- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 3cc6894e..0d239126 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -7,7 +7,7 @@ diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 2341e63f..1277096e 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -13,7 +13,7 @@ CFBundleInfoDictionaryVersion 6.0 CFBundleName - eatvio_app + Итвио CFBundlePackageType APPL CFBundleShortVersionString diff --git a/lib/features/menu/presentation/pages/menu_screen.dart b/lib/features/menu/presentation/pages/menu_screen.dart index f5b5f3d0..6b8b47fe 100644 --- a/lib/features/menu/presentation/pages/menu_screen.dart +++ b/lib/features/menu/presentation/pages/menu_screen.dart @@ -37,18 +37,20 @@ class MenuScreen extends StatelessWidget { if (profileModel.roles.contains('consultant')) _divider(context), if (profileModel.roles.contains('consultant')) _tile(leading: ImageConstant.icnPlans, title: 'Мои планы', onTap: () => GoRouter.of(context).push(MyPlansScreen.path)), - _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 (!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); + }, + ), + ], _divider(context), _tile(leading: ImageConstant.icnGlossary, title: 'Глоссарий', onTap: () => GoRouter.of(context).push('/glossary-screen')), _divider(context), From c4ff14c7b2a3c58b3eb7f87c121f8d4b72d7eaa9 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Wed, 6 Aug 2025 13:04:30 +0300 Subject: [PATCH 069/134] fix(env): repo branch --- .gitea/workflows/test-flight-cd.yaml | 1 + .gitea/workflows/test.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.gitea/workflows/test-flight-cd.yaml b/.gitea/workflows/test-flight-cd.yaml index 99b661a0..c1244bd9 100644 --- a/.gitea/workflows/test-flight-cd.yaml +++ b/.gitea/workflows/test-flight-cd.yaml @@ -17,6 +17,7 @@ jobs: repository: mso7nueh/eatvio_app_env path: env_repo token: ${{ vars.MSO7NUEH_TOKEN }} + ref: main - name: Copy .env file run: | diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml index 1f4c4f03..c7aefd5c 100644 --- a/.gitea/workflows/test.yml +++ b/.gitea/workflows/test.yml @@ -22,6 +22,7 @@ jobs: repository: mso7nueh/eatvio_app_env path: env_repo token: ${{ vars.MSO7NUEH_TOKEN }} + ref: main - name: Copy .env file run: | From 138a81d9d1f842aa87a55725b196f8c0b0893ec1 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Wed, 6 Aug 2025 14:46:06 +0300 Subject: [PATCH 070/134] feat(version): update --- 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 9a27847e..6388d648 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -396,7 +396,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 64; + CURRENT_PROJECT_VERSION = 65; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -546,7 +546,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 64; + CURRENT_PROJECT_VERSION = 65; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -584,7 +584,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 64; + CURRENT_PROJECT_VERSION = 65; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 2341e63f..e6c78de1 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -21,7 +21,7 @@ CFBundleSignature ???? CFBundleVersion - 64 + 65 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/pubspec.yaml b/pubspec.yaml index 0154d606..fefe422b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: eatvio_app description: A new Flutter project. -version: 1.2.6+64 +version: 1.2.7+66 publish_to: 'none' environment: From b051a588e08b0eb73de8e25dd33e3f6badaff570 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Wed, 6 Aug 2025 15:29:10 +0300 Subject: [PATCH 071/134] fix: fastlane --- ios/fastlane/Fastfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios/fastlane/Fastfile b/ios/fastlane/Fastfile index 7dff33a3..d9653264 100644 --- a/ios/fastlane/Fastfile +++ b/ios/fastlane/Fastfile @@ -25,7 +25,7 @@ platform :ios do scheme: "Runner", export_method: "app-store", codesigning_identity: "Apple Distribution", - build_args: "--dart-define=HIDE_PLANS=true" + xcargs: "--dart-define=HIDE_PLANS=true" ) app_store_connect_api_key(key_id: "325JS9JCZH", issuer_id: "dfa27d1d-2bb8-4d4d-847e-0b32f9b880b3", key_filepath: "./AuthKey.p8") upload_to_testflight From 108e067b119edb63d4001ef87147b6e3c68a5a34 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Wed, 6 Aug 2025 15:38:21 +0300 Subject: [PATCH 072/134] fix: fastlane --- .gitea/workflows/test-flight-cd.yaml | 19 ++++++++++++++++++- ios/Gemfile | 2 +- ios/fastlane/Fastfile | 24 +++++++++++++++++++++++- 3 files changed, 42 insertions(+), 3 deletions(-) diff --git a/.gitea/workflows/test-flight-cd.yaml b/.gitea/workflows/test-flight-cd.yaml index c1244bd9..1f9668f6 100644 --- a/.gitea/workflows/test-flight-cd.yaml +++ b/.gitea/workflows/test-flight-cd.yaml @@ -51,7 +51,24 @@ jobs: flutter pub run build_runner build --delete-conflicting-outputs cd ios pod update - fastlane beta + # Install/update fastlane + bundle install + # Retry logic for fastlane upload + for i in {1..3}; do + echo "Attempt $i of 3 for fastlane upload" + if bundle exec fastlane beta; then + echo "Fastlane upload successful on attempt $i" + break + else + echo "Fastlane upload failed on attempt $i" + if [ $i -eq 3 ]; then + echo "All attempts failed" + exit 1 + fi + echo "Waiting 30 seconds before retry..." + sleep 30 + fi + done env: LANG: en_US.UTF-8 LC_ALL: en_US.UTF-8 diff --git a/ios/Gemfile b/ios/Gemfile index 7a118b49..791ac593 100644 --- a/ios/Gemfile +++ b/ios/Gemfile @@ -1,3 +1,3 @@ source "https://rubygems.org" -gem "fastlane" +gem "fastlane", "~> 2.217" diff --git a/ios/fastlane/Fastfile b/ios/fastlane/Fastfile index d9653264..72d89b2c 100644 --- a/ios/fastlane/Fastfile +++ b/ios/fastlane/Fastfile @@ -28,6 +28,28 @@ platform :ios do xcargs: "--dart-define=HIDE_PLANS=true" ) app_store_connect_api_key(key_id: "325JS9JCZH", issuer_id: "dfa27d1d-2bb8-4d4d-847e-0b32f9b880b3", key_filepath: "./AuthKey.p8") - upload_to_testflight + + # Try the newer upload method first, fallback to the older one + begin + upload_to_app_store_connect( + api_key_path: "./AuthKey.p8", + api_key_id: "325JS9JCZH", + api_issuer_id: "dfa27d1d-2bb8-4d4d-847e-0b32f9b880b3", + skip_metadata: true, + skip_screenshots: true, + precheck_include_in_app_purchases: false + ) + rescue => e + UI.message("upload_to_app_store_connect failed, trying upload_to_testflight: #{e.message}") + upload_to_testflight( + skip_waiting_for_build_processing: true, + skip_submission: true, + reject_if_possible: false, + precheck_include_in_app_purchases: false, + api_key_path: "./AuthKey.p8", + api_key_id: "325JS9JCZH", + api_issuer_id: "dfa27d1d-2bb8-4d4d-847e-0b32f9b880b3" + ) + end end end From 1387e20553f9031f03336c30b0da30af63536586 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Wed, 6 Aug 2025 15:42:02 +0300 Subject: [PATCH 073/134] fix: fastlane --- .gitea/workflows/test-flight-cd.yaml | 45 ++-------------------------- ios/fastlane/Fastfile | 32 ++------------------ 2 files changed, 4 insertions(+), 73 deletions(-) diff --git a/.gitea/workflows/test-flight-cd.yaml b/.gitea/workflows/test-flight-cd.yaml index 1f9668f6..4320ecc5 100644 --- a/.gitea/workflows/test-flight-cd.yaml +++ b/.gitea/workflows/test-flight-cd.yaml @@ -11,30 +11,6 @@ jobs: - name: Checkout uses: actions/checkout@v2 - - name: Checkout env repository - uses: actions/checkout@v2 - with: - repository: mso7nueh/eatvio_app_env - path: env_repo - token: ${{ vars.MSO7NUEH_TOKEN }} - ref: main - - - name: Copy .env file - run: | - mkdir -p env - cp env_repo/.env env/.env - - - name: Load environment variables from .env - run: | - set -a - source env/.env - set +a - # Export all variables to GITHUB_ENV - while IFS= read -r line; do - if [[ $line =~ ^[A-Za-z_][A-Za-z0-9_]*= ]]; then - echo "$line" >> $GITHUB_ENV - fi - done < env/.env - name: Unlock login keychain run: | @@ -51,24 +27,7 @@ jobs: flutter pub run build_runner build --delete-conflicting-outputs cd ios pod update - # Install/update fastlane - bundle install - # Retry logic for fastlane upload - for i in {1..3}; do - echo "Attempt $i of 3 for fastlane upload" - if bundle exec fastlane beta; then - echo "Fastlane upload successful on attempt $i" - break - else - echo "Fastlane upload failed on attempt $i" - if [ $i -eq 3 ]; then - echo "All attempts failed" - exit 1 - fi - echo "Waiting 30 seconds before retry..." - sleep 30 - fi - done + fastlane beta env: LANG: en_US.UTF-8 LC_ALL: en_US.UTF-8 @@ -77,4 +36,4 @@ jobs: 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 }} - API_APPSTORE_KEY: ${{ vars.API_APPSTORE_KEY }} + API_APPSTORE_KEY: ${{ vars.API_APPSTORE_KEY }} \ No newline at end of file diff --git a/ios/fastlane/Fastfile b/ios/fastlane/Fastfile index 72d89b2c..9340117d 100644 --- a/ios/fastlane/Fastfile +++ b/ios/fastlane/Fastfile @@ -20,36 +20,8 @@ platform :ios do lane :beta do match(type: "appstore", readonly: true) increment_build_number(xcodeproj: "Runner.xcodeproj") - build_app( - workspace: "Runner.xcworkspace", - scheme: "Runner", - export_method: "app-store", - codesigning_identity: "Apple Distribution", - xcargs: "--dart-define=HIDE_PLANS=true" - ) + build_app(workspace: "Runner.xcworkspace", scheme: "Runner", export_method: "app-store", codesigning_identity: "Apple Distribution") app_store_connect_api_key(key_id: "325JS9JCZH", issuer_id: "dfa27d1d-2bb8-4d4d-847e-0b32f9b880b3", key_filepath: "./AuthKey.p8") - - # Try the newer upload method first, fallback to the older one - begin - upload_to_app_store_connect( - api_key_path: "./AuthKey.p8", - api_key_id: "325JS9JCZH", - api_issuer_id: "dfa27d1d-2bb8-4d4d-847e-0b32f9b880b3", - skip_metadata: true, - skip_screenshots: true, - precheck_include_in_app_purchases: false - ) - rescue => e - UI.message("upload_to_app_store_connect failed, trying upload_to_testflight: #{e.message}") - upload_to_testflight( - skip_waiting_for_build_processing: true, - skip_submission: true, - reject_if_possible: false, - precheck_include_in_app_purchases: false, - api_key_path: "./AuthKey.p8", - api_key_id: "325JS9JCZH", - api_issuer_id: "dfa27d1d-2bb8-4d4d-847e-0b32f9b880b3" - ) - end + upload_to_testflight end end From 64b158d1ff2c38252d45fdc17588d1107e3f86fe Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Wed, 6 Aug 2025 15:53:32 +0300 Subject: [PATCH 074/134] fix: fastlane --- ios/fastlane/Fastfile | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/ios/fastlane/Fastfile b/ios/fastlane/Fastfile index 9340117d..d9653264 100644 --- a/ios/fastlane/Fastfile +++ b/ios/fastlane/Fastfile @@ -20,7 +20,13 @@ platform :ios do lane :beta do match(type: "appstore", readonly: true) increment_build_number(xcodeproj: "Runner.xcodeproj") - build_app(workspace: "Runner.xcworkspace", scheme: "Runner", export_method: "app-store", codesigning_identity: "Apple Distribution") + build_app( + workspace: "Runner.xcworkspace", + scheme: "Runner", + export_method: "app-store", + codesigning_identity: "Apple Distribution", + xcargs: "--dart-define=HIDE_PLANS=true" + ) app_store_connect_api_key(key_id: "325JS9JCZH", issuer_id: "dfa27d1d-2bb8-4d4d-847e-0b32f9b880b3", key_filepath: "./AuthKey.p8") upload_to_testflight end From 1f714b3bc2a812e6141987084a7446546fb77a61 Mon Sep 17 00:00:00 2001 From: Username1548 Date: Wed, 6 Aug 2025 20:29:56 +0700 Subject: [PATCH 075/134] feat(diary): fixed commenting of diary --- .../diary/presentation/bloc/diary_cubit.dart | 58 +++++++------------ 1 file changed, 22 insertions(+), 36 deletions(-) diff --git a/lib/features/diary/presentation/bloc/diary_cubit.dart b/lib/features/diary/presentation/bloc/diary_cubit.dart index 1bbfbbc7..8f8c8a0a 100644 --- a/lib/features/diary/presentation/bloc/diary_cubit.dart +++ b/lib/features/diary/presentation/bloc/diary_cubit.dart @@ -136,15 +136,7 @@ class DiaryCubit extends Cubit { final fifthMeal = MealModel(id: 0, time: '0', typeNumber: 5, satiety: 0, mood: 0, portions: fifthMealPortions ?? [], photos: []); emit( DiaryState.loadedTomorrowWithDiet( - diary: diaryDay.copyWith( - meals: [ - firstMeal, - secondMeal, - thirdMeal, - fourthMeal, - fifthMeal, - ], - ), + diary: diaryDay.copyWith(meals: [firstMeal, secondMeal, thirdMeal, fourthMeal, fifthMeal]), selectedDay: selectedDay, selectedMeal: selectedMeal, ), @@ -156,7 +148,7 @@ class DiaryCubit extends Cubit { Future fetchDiary([bool needLoading = false, int? diaryId, int? currentDay]) async { this.diaryId ??= diaryId; - + final isConsultantView = this.diaryId != null; if (needLoading) { emit(const DiaryState.loading()); selectedMeal = 1; @@ -180,7 +172,8 @@ class DiaryCubit extends Cubit { String? planName; if (diaryDay.chosenPlanId != null) { final chosenPlanModel = await chatRepository.getChosenPlan(id: diaryDay.chosenPlanId!); - if (diaryDay.typeOfDiary.diaryState == DiaryStateEnum.lastDayCommented || diaryDay.typeOfDiary.diaryState == DiaryStateEnum.commented) { + if (isConsultantView && + (diaryDay.typeOfDiary.diaryState == DiaryStateEnum.lastDayCommented || diaryDay.typeOfDiary.diaryState == DiaryStateEnum.commented)) { await diaryRepository.viewCommentOfDiaryDayByConsultant(diaryId: diaryDay.id); } consultantReview = await diaryRepository.getDiaryComments(planId: diaryDay.chosenPlanId!, diaryId: diaryDay.id); @@ -192,24 +185,26 @@ class DiaryCubit extends Cubit { final isDiaryFinished = diaryDay.typeOfDiary.diaryState == DiaryStateEnum.finished || diaryDay.typeOfDiary.diaryState == DiaryStateEnum.lastDayFinished; - final isDiaryCommented = diaryDay.typeOfDiary.diaryState == DiaryStateEnum.commented || + final isDiaryCommented = + diaryDay.typeOfDiary.diaryState == DiaryStateEnum.commented || diaryDay.typeOfDiary.diaryState == DiaryStateEnum.lastDayCommented || diaryDay.typeOfDiary.diaryState == DiaryStateEnum.reviewedByUser; emit( DiaryState.loaded( - diary: diaryDay, - nutrition: nutrition, - selectedDay: selectedDay, - selectedMeal: selectedMeal, - isConsultantView: this.diaryId != null, - consultantReview: consultantReview, - isDiaryFinished: isDiaryFinished, - isDiaryCommented: isDiaryCommented, - chosenPlanCurrentDay: currentPlanDay, - chosenPlanDuration: planDuration, - chosenPlanName: planName, - isMoodEnabled: isMoodEnabled, - isWakeUpAndRetireEnabled: isWakeUpAndRetireTimeEnabled), + diary: diaryDay, + nutrition: nutrition, + selectedDay: selectedDay, + selectedMeal: selectedMeal, + isConsultantView: isConsultantView, + consultantReview: consultantReview, + isDiaryFinished: isDiaryFinished, + isDiaryCommented: isDiaryCommented, + chosenPlanCurrentDay: currentPlanDay, + chosenPlanDuration: planDuration, + chosenPlanName: planName, + isMoodEnabled: isMoodEnabled, + isWakeUpAndRetireEnabled: isWakeUpAndRetireTimeEnabled, + ), ); } catch (e, st) { log(e.toString(), stackTrace: st); @@ -217,17 +212,8 @@ 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, - ); + 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(); } From 7e7f8bfb034f49819637bbb0637add2df3b6bfee Mon Sep 17 00:00:00 2001 From: Username1548 Date: Wed, 6 Aug 2025 20:36:50 +0700 Subject: [PATCH 076/134] fix --- lib/features/diary/presentation/bloc/diary_cubit.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/features/diary/presentation/bloc/diary_cubit.dart b/lib/features/diary/presentation/bloc/diary_cubit.dart index 8f8c8a0a..6e6f8891 100644 --- a/lib/features/diary/presentation/bloc/diary_cubit.dart +++ b/lib/features/diary/presentation/bloc/diary_cubit.dart @@ -172,7 +172,7 @@ class DiaryCubit extends Cubit { String? planName; if (diaryDay.chosenPlanId != null) { final chosenPlanModel = await chatRepository.getChosenPlan(id: diaryDay.chosenPlanId!); - if (isConsultantView && + if (!isConsultantView && (diaryDay.typeOfDiary.diaryState == DiaryStateEnum.lastDayCommented || diaryDay.typeOfDiary.diaryState == DiaryStateEnum.commented)) { await diaryRepository.viewCommentOfDiaryDayByConsultant(diaryId: diaryDay.id); } From 832ba421e9bb8d3dbca5688ea2c777aa2b515c7e Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Wed, 6 Aug 2025 17:41:35 +0300 Subject: [PATCH 077/134] fix: secure storage --- pubspec.yaml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index fefe422b..a56c030b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,7 +13,11 @@ dependencies: flutter_localizations: sdk: flutter - flutter_secure_storage: ^9.2.4 + flutter_secure_storage: + git: + url: https://github.com/sherlockvn/flutter_secure_storage.git + ref: remove-old-encrypt-algorithm + path: flutter_secure_storage flutter_bloc: ^9.1.1 cached_network_image: ^3.4.1 flutter_svg: ^2.2.0 From 1305e678179a9daec70e1f85038145be78f061c3 Mon Sep 17 00:00:00 2001 From: Username1548 Date: Thu, 7 Aug 2025 18:10:58 +0700 Subject: [PATCH 078/134] feat(env): added env loading to ci/cd --- .gitea/workflows/test-flight-cd.yaml | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/.gitea/workflows/test-flight-cd.yaml b/.gitea/workflows/test-flight-cd.yaml index 4320ecc5..c1244bd9 100644 --- a/.gitea/workflows/test-flight-cd.yaml +++ b/.gitea/workflows/test-flight-cd.yaml @@ -11,6 +11,30 @@ jobs: - name: Checkout uses: actions/checkout@v2 + - name: Checkout env repository + uses: actions/checkout@v2 + with: + repository: mso7nueh/eatvio_app_env + path: env_repo + token: ${{ vars.MSO7NUEH_TOKEN }} + ref: main + + - name: Copy .env file + run: | + mkdir -p env + cp env_repo/.env env/.env + + - name: Load environment variables from .env + run: | + set -a + source env/.env + set +a + # Export all variables to GITHUB_ENV + while IFS= read -r line; do + if [[ $line =~ ^[A-Za-z_][A-Za-z0-9_]*= ]]; then + echo "$line" >> $GITHUB_ENV + fi + done < env/.env - name: Unlock login keychain run: | @@ -36,4 +60,4 @@ jobs: 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 }} - API_APPSTORE_KEY: ${{ vars.API_APPSTORE_KEY }} \ No newline at end of file + API_APPSTORE_KEY: ${{ vars.API_APPSTORE_KEY }} From f9b63f3401e9565e061d5fc2b05b34c7a11b3fb4 Mon Sep 17 00:00:00 2001 From: Username1548 Date: Thu, 7 Aug 2025 18:20:26 +0700 Subject: [PATCH 079/134] version update --- 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 9a27847e..14890f55 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -396,7 +396,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 64; + CURRENT_PROJECT_VERSION = 66; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -546,7 +546,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 64; + CURRENT_PROJECT_VERSION = 66; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -584,7 +584,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 64; + CURRENT_PROJECT_VERSION = 66; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 2341e63f..65ea6c40 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -21,7 +21,7 @@ CFBundleSignature ???? CFBundleVersion - 64 + 66 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/pubspec.yaml b/pubspec.yaml index 0154d606..a3560aec 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: eatvio_app description: A new Flutter project. -version: 1.2.6+64 +version: 1.2.6+66 publish_to: 'none' environment: From 905559b1f006de37c6328aa3df1e85b4edd7a70a Mon Sep 17 00:00:00 2001 From: Username1548 Date: Thu, 7 Aug 2025 19:06:27 +0700 Subject: [PATCH 080/134] feat(fastlane): changed fastlane build uploading --- ios/fastlane/Fastfile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ios/fastlane/Fastfile b/ios/fastlane/Fastfile index d9653264..de31023b 100644 --- a/ios/fastlane/Fastfile +++ b/ios/fastlane/Fastfile @@ -28,6 +28,8 @@ platform :ios do xcargs: "--dart-define=HIDE_PLANS=true" ) app_store_connect_api_key(key_id: "325JS9JCZH", issuer_id: "dfa27d1d-2bb8-4d4d-847e-0b32f9b880b3", key_filepath: "./AuthKey.p8") - upload_to_testflight + upload_to_testflight( + use_legacy_upload: false + ) end end From 6fa7beb070ef040ca4bf9858b92941742bb18264 Mon Sep 17 00:00:00 2001 From: Username1548 Date: Thu, 7 Aug 2025 19:23:12 +0700 Subject: [PATCH 081/134] reverted changes --- ios/fastlane/Fastfile | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/ios/fastlane/Fastfile b/ios/fastlane/Fastfile index de31023b..d9653264 100644 --- a/ios/fastlane/Fastfile +++ b/ios/fastlane/Fastfile @@ -28,8 +28,6 @@ platform :ios do xcargs: "--dart-define=HIDE_PLANS=true" ) app_store_connect_api_key(key_id: "325JS9JCZH", issuer_id: "dfa27d1d-2bb8-4d4d-847e-0b32f9b880b3", key_filepath: "./AuthKey.p8") - upload_to_testflight( - use_legacy_upload: false - ) + upload_to_testflight end end From 15248a65fe36e2dca9085fe6a617fe325f7b0811 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Thu, 7 Aug 2025 18:05:11 +0300 Subject: [PATCH 082/134] feat(profile): Issue-248 --- .../presentation/pages/profile_screen.dart | 271 +++++++++++------- 1 file changed, 171 insertions(+), 100 deletions(-) diff --git a/lib/features/profile/presentation/pages/profile_screen.dart b/lib/features/profile/presentation/pages/profile_screen.dart index 211543a9..4e0a6524 100644 --- a/lib/features/profile/presentation/pages/profile_screen.dart +++ b/lib/features/profile/presentation/pages/profile_screen.dart @@ -41,6 +41,7 @@ import 'package:intl/intl.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:share_plus/share_plus.dart'; import 'package:url_launcher/url_launcher.dart'; +import 'package:flutter/services.dart'; class ProfileScreen extends StatefulWidget { final Author? author; @@ -760,76 +761,92 @@ class _ProfileScreenState extends State { child: Row( children: [ PopupMenuButton( - onSelected: (value) async { - if (value == 'view') { - context.pushTransparentRoute(rootNavigator: true, PhotoViewScreen(imageUrls: [baseUrl + state.profile.profilePhotoUrl], index: 0)); - } - if (value == 'replace') { - showCupertinoModalPopup( - context: context, - builder: (context) { - final s = S.of(context); - return CupertinoActionSheet( - actions: [ - CupertinoActionSheetAction( - onPressed: () async { - PermissionHandler.checkPermission( - permission: Permission.camera, - title: s.allowAccessToCamera, - description: s.allowAccessToCameraDescription, - onGranted: () async { - AppRouter.navigatorKey.currentContext?.pop(); - final picker = ImagePicker(); - XFile? image = await picker.pickImage(source: ImageSource.camera); - if (image != null) { - await profileCubit.uploadPhoto(File(image.path)); - await sl().getMyProfile(); - } + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + color: Colors.white, + constraints: const BoxConstraints(minWidth: 260), + surfaceTintColor: Colors.transparent, + elevation: 2, + itemBuilder: (context) => [ + _buildCompactItem( + icon: 'assets/icons/eye.svg', + text: 'Посмотреть', + onTap: () async { + context.pushTransparentRoute(rootNavigator: true, PhotoViewScreen(imageUrls: [baseUrl + state.profile.profilePhotoUrl], index: 0)); + }, + ), + if (widget.author == null || widget.author!.authorIsMe) ...[ + _buildDivider(), + _buildCompactItem( + icon: 'assets/icons/edit_paint.svg', + text: 'Заменить', + onTap: () async { + showCupertinoModalPopup( + context: context, + builder: (context) { + final s = S.of(context); + return CupertinoActionSheet( + actions: [ + CupertinoActionSheetAction( + onPressed: () async { + PermissionHandler.checkPermission( + permission: Permission.camera, + title: s.allowAccessToCamera, + description: s.allowAccessToCameraDescription, + onGranted: () async { + AppRouter.navigatorKey.currentContext?.pop(); + final picker = ImagePicker(); + XFile? image = await picker.pickImage(source: ImageSource.camera); + if (image != null) { + await profileCubit.uploadPhoto(File(image.path)); + await sl().getMyProfile(); + } + }, + context: context, + ); }, - context: context, - ); - }, - child: const Row(children: [Icon(Icons.photo_camera), SizedBox(width: 8), Text('Фото')]), - ), - CupertinoActionSheetAction( - onPressed: () async { - PermissionHandler.checkPermission( - permission: Permission.photos, - title: s.allowAcessToGalery, - description: s.allowAcessToGaleryDescription, - onGranted: () async { - AppRouter.navigatorKey.currentContext?.pop(); - final picker = ImagePicker(); - XFile? image = await picker.pickImage(source: ImageSource.gallery); - if (image != null) { - await profileCubit.uploadPhoto(File(image.path)); - await sl().getMyProfile(); - } + child: const Row(children: [Icon(Icons.photo_camera), SizedBox(width: 8), Text('Фото')]), + ), + CupertinoActionSheetAction( + onPressed: () async { + PermissionHandler.checkPermission( + permission: Permission.photos, + title: s.allowAcessToGalery, + description: s.allowAcessToGaleryDescription, + onGranted: () async { + AppRouter.navigatorKey.currentContext?.pop(); + final picker = ImagePicker(); + XFile? image = await picker.pickImage(source: ImageSource.gallery); + if (image != null) { + await profileCubit.uploadPhoto(File(image.path)); + await sl().getMyProfile(); + } + }, + context: context, + ); }, - context: context, - ); - }, - child: const Row(children: [Icon(Icons.photo), SizedBox(width: 8), Text('Галерея')]), - ), - ], - cancelButton: CupertinoActionSheetAction( - onPressed: () => Navigator.of(context).pop(), - isDestructiveAction: true, - child: const Text('Отмена'), - ), + child: const Row(children: [Icon(Icons.photo), SizedBox(width: 8), Text('Галерея')]), + ), + ], + cancelButton: CupertinoActionSheetAction( + onPressed: () => Navigator.of(context).pop(), + isDestructiveAction: true, + child: const Text('Отмена'), + ), + ); + }, ); }, - ); - } - if (value == 'delete') { - await profileCubit.deletePhoto(); - await sl().getMyProfile(); - } - }, - itemBuilder: (context) => [ - const PopupMenuItem(value: 'view', child: Text('Посмотреть')), - if (widget.author == null || widget.author!.authorIsMe) const PopupMenuItem(value: 'replace', child: Text('Заменить')), - if (widget.author == null || widget.author!.authorIsMe) const PopupMenuItem(value: 'delete', child: Text('Удалить')), + ), + _buildDivider(), + _buildCompactItem( + icon: 'assets/icons/red_trash.svg', + text: 'Удалить', + onTap: () async { + await profileCubit.deletePhoto(); + await sl().getMyProfile(); + }, + ), + ], ], child: Hero( tag: state.profile.profilePhotoUrl, @@ -899,41 +916,68 @@ class _ProfileScreenState extends State { ), const Spacer(), PopupMenuButton( - onSelected: (value) { - if (value == 'share') { - 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), - ); - } else if (value == 'block') { - isFirstFetch = true; - sl().blockUser(state.profile.slug); - widget.author == null || widget.author!.authorIsMe ? profileCubit.getMyProfile() : profileCubit.getProfileBySlug(widget.author!.authorSlug); - if (state.profile.roles.contains('consultant')) { - plansCubit.getPlans(state.profile.slug); - } - isFirstFetch = false; - } else if (value == 'unblock') { - isFirstFetch = true; - sl().unblockUser(state.profile.slug); - widget.author == null || widget.author!.authorIsMe ? profileCubit.getMyProfile() : profileCubit.getProfileBySlug(widget.author!.authorSlug); - if (state.profile.roles.contains('consultant')) { - plansCubit.getPlans(state.profile.slug); - } - isFirstFetch = false; - } else { - showModalBottomSheet( - context: context, - isScrollControlled: true, - builder: (context) => ComplainingBottomSheet(complainedContentType: ComplainedContent.profile, slug: widget.author?.authorSlug), - ); - } - }, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + color: Colors.white, + constraints: const BoxConstraints(minWidth: 260), + surfaceTintColor: Colors.transparent, + elevation: 2, itemBuilder: (context) => [ - const PopupMenuItem(value: 'share', child: Text('Поделиться')), - if (widget.author != null) const PopupMenuItem(value: 'complain', child: Text('Пожаловаться')), - if (widget.author != null && !state.profile.isBlockedByMe) const PopupMenuItem(value: 'block', child: Text('Заблокировать')), - if (widget.author != null && state.profile.isBlockedByMe) const PopupMenuItem(value: 'unblock', child: Text('Разблокировать')), + _buildCompactItem( + icon: 'assets/icons/share2.svg', + text: 'Поделиться', + 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), + ); + }, + ), + if (widget.author != null) ...[ + _buildDivider(), + _buildCompactItem( + icon: 'assets/icons/complain.svg', + text: 'Пожаловаться', + onTap: () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (context) => ComplainingBottomSheet(complainedContentType: ComplainedContent.profile, slug: widget.author?.authorSlug), + ); + }, + ), + ], + if (widget.author != null && !state.profile.isBlockedByMe) ...[ + _buildDivider(), + _buildCompactItem( + icon: 'assets/icons/complain.svg', + text: 'Заблокировать', + onTap: () { + isFirstFetch = true; + sl().blockUser(state.profile.slug); + widget.author == null || widget.author!.authorIsMe ? profileCubit.getMyProfile() : profileCubit.getProfileBySlug(widget.author!.authorSlug); + if (state.profile.roles.contains('consultant')) { + plansCubit.getPlans(state.profile.slug); + } + isFirstFetch = false; + }, + ), + ], + if (widget.author != null && state.profile.isBlockedByMe) ...[ + _buildDivider(), + _buildCompactItem( + icon: 'assets/icons/complain.svg', + text: 'Разблокировать', + onTap: () { + isFirstFetch = true; + sl().unblockUser(state.profile.slug); + widget.author == null || widget.author!.authorIsMe ? profileCubit.getMyProfile() : profileCubit.getProfileBySlug(widget.author!.authorSlug); + if (state.profile.roles.contains('consultant')) { + plansCubit.getPlans(state.profile.slug); + } + isFirstFetch = false; + }, + ), + ], ], child: const Icon(Icons.more_horiz_rounded), ), @@ -942,6 +986,33 @@ class _ProfileScreenState extends State { ); } + PopupMenuItem _buildCompactItem({required String icon, required String text, required Function() onTap}) { + return PopupMenuItem( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + height: 36, + onTap: onTap, + child: SizedBox( + width: double.infinity, + child: Row( + children: [ + Text(text, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)), + const Spacer(), + SvgPicture.asset(icon), + ], + ), + ), + ); + } + + PopupMenuItem _buildDivider() { + return PopupMenuItem( + enabled: false, + height: 1, + padding: EdgeInsets.zero, + child: Container(margin: const EdgeInsets.symmetric(horizontal: 12), height: 1, color: appTheme.dividerColor), + ); + } + Row _additional(String title, String value) { return Row( children: [ From fe99890c0e8a2c239c37e8531c95a04722f1041f Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Thu, 7 Aug 2025 21:29:30 +0300 Subject: [PATCH 083/134] fix: analyzer --- lib/features/profile/presentation/pages/profile_screen.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/features/profile/presentation/pages/profile_screen.dart b/lib/features/profile/presentation/pages/profile_screen.dart index 4e0a6524..f3329074 100644 --- a/lib/features/profile/presentation/pages/profile_screen.dart +++ b/lib/features/profile/presentation/pages/profile_screen.dart @@ -41,7 +41,6 @@ import 'package:intl/intl.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:share_plus/share_plus.dart'; import 'package:url_launcher/url_launcher.dart'; -import 'package:flutter/services.dart'; class ProfileScreen extends StatefulWidget { final Author? author; From 2fac007675e6c78cb90ba7270fe77c87700639a5 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Fri, 8 Aug 2025 11:11:00 +0300 Subject: [PATCH 084/134] feat(fastlane): Issue-622 --- .gitea/workflows/test-flight-cd.yaml | 9 +++++++- ios/fastlane/Fastfile | 34 ++++++++++++++++++++++++++-- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/.gitea/workflows/test-flight-cd.yaml b/.gitea/workflows/test-flight-cd.yaml index c1244bd9..0bb7c723 100644 --- a/.gitea/workflows/test-flight-cd.yaml +++ b/.gitea/workflows/test-flight-cd.yaml @@ -42,9 +42,16 @@ jobs: env: KEYCHAIN_PASSWORD: ${{ vars.IOS_RUNNER_CI_PASSWORD }} - - name: Flutter build + - name: Setup API key run: | echo "${API_APPSTORE_KEY}" | base64 --decode > ios/AuthKey.p8 + chmod 600 ios/AuthKey.p8 + ls -la ios/AuthKey.p8 + env: + API_APPSTORE_KEY: ${{ vars.API_APPSTORE_KEY }} + + - name: Flutter build + run: | flutter clean flutter pub get flutter pub upgrade diff --git a/ios/fastlane/Fastfile b/ios/fastlane/Fastfile index d9653264..a43260fe 100644 --- a/ios/fastlane/Fastfile +++ b/ios/fastlane/Fastfile @@ -18,8 +18,16 @@ default_platform(:ios) platform :ios do desc "Push a new beta build to TestFlight" lane :beta do + UI.message "Checking API key file..." + if File.exist?("./AuthKey.p8") + UI.message "API key file exists" + else + UI.user_error!("API key file not found at ./AuthKey.p8") + end + match(type: "appstore", readonly: true) increment_build_number(xcodeproj: "Runner.xcodeproj") + build_app( workspace: "Runner.xcworkspace", scheme: "Runner", @@ -27,7 +35,29 @@ platform :ios do codesigning_identity: "Apple Distribution", xcargs: "--dart-define=HIDE_PLANS=true" ) - app_store_connect_api_key(key_id: "325JS9JCZH", issuer_id: "dfa27d1d-2bb8-4d4d-847e-0b32f9b880b3", key_filepath: "./AuthKey.p8") - upload_to_testflight + + app_store_connect_api_key( + key_id: "325JS9JCZH", + issuer_id: "dfa27d1d-2bb8-4d4d-847e-0b32f9b880b3", + key_filepath: "./AuthKey.p8", + duration: 1200, + in_house: false + ) + + begin + upload_to_testflight( + api_key_path: "./AuthKey.p8", + skip_waiting_for_build_processing: true, + skip_submission: true, + distribute_external: false, + notify_external_testers: false, + changelog: "TestFlight build #{get_build_number(xcodeproj: 'Runner.xcodeproj')}", + verbose: true + ) + rescue => ex + UI.error "Upload to TestFlight failed: #{ex.message}" + UI.error "Full error: #{ex.backtrace.join("\n")}" + raise ex + end end end From 73f27efadd87cea03d45dae677ac8fccc302f8a1 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Fri, 8 Aug 2025 11:14:41 +0300 Subject: [PATCH 085/134] fix(fastlane): testflight --- .gitea/workflows/test-flight-cd.yaml | 22 +++++++++++++++++++++- ios/fastlane/Fastfile | 3 +++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/.gitea/workflows/test-flight-cd.yaml b/.gitea/workflows/test-flight-cd.yaml index 0bb7c723..faff6332 100644 --- a/.gitea/workflows/test-flight-cd.yaml +++ b/.gitea/workflows/test-flight-cd.yaml @@ -44,9 +44,20 @@ jobs: - name: Setup API key run: | - echo "${API_APPSTORE_KEY}" | base64 --decode > ios/AuthKey.p8 + echo "API_APPSTORE_KEY length: ${#API_APPSTORE_KEY}" + if [ -z "$API_APPSTORE_KEY" ]; then + echo "ERROR: API_APPSTORE_KEY is empty or not set" + exit 1 + fi + echo "${API_APPSTORE_KEY}" | tr -d '\n' | base64 --decode > ios/AuthKey.p8 chmod 600 ios/AuthKey.p8 ls -la ios/AuthKey.p8 + echo "File size: $(wc -c < ios/AuthKey.p8) bytes" + echo "First few characters of decoded file:" + head -c 50 ios/AuthKey.p8 | xxd + echo "Current directory: $(pwd)" + echo "Contents of ios directory:" + ls -la ios/ env: API_APPSTORE_KEY: ${{ vars.API_APPSTORE_KEY }} @@ -57,6 +68,15 @@ jobs: flutter pub upgrade flutter pub run build_runner build --delete-conflicting-outputs cd ios + echo "Current directory after cd ios: $(pwd)" + echo "Contents of current directory:" + ls -la + echo "Checking if AuthKey.p8 exists:" + if [ -f "AuthKey.p8" ]; then + echo "AuthKey.p8 exists, size: $(wc -c < AuthKey.p8) bytes" + else + echo "AuthKey.p8 does not exist" + fi pod update fastlane beta env: diff --git a/ios/fastlane/Fastfile b/ios/fastlane/Fastfile index a43260fe..6ecef599 100644 --- a/ios/fastlane/Fastfile +++ b/ios/fastlane/Fastfile @@ -18,9 +18,12 @@ default_platform(:ios) platform :ios do desc "Push a new beta build to TestFlight" lane :beta do + UI.message "Current working directory: #{Dir.pwd}" UI.message "Checking API key file..." + UI.message "Files in current directory: #{Dir.entries('.').join(', ')}" if File.exist?("./AuthKey.p8") UI.message "API key file exists" + UI.message "File size: #{File.size('./AuthKey.p8')} bytes" else UI.user_error!("API key file not found at ./AuthKey.p8") end From d8378e3686ccdc528c53736d6a57149b15696668 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Fri, 8 Aug 2025 11:17:48 +0300 Subject: [PATCH 086/134] fix: fastlane --- .gitea/workflows/test-flight-cd.yaml | 20 -------------------- ios/fastlane/Fastfile | 13 ++++++------- 2 files changed, 6 insertions(+), 27 deletions(-) diff --git a/.gitea/workflows/test-flight-cd.yaml b/.gitea/workflows/test-flight-cd.yaml index faff6332..8b3a8cf5 100644 --- a/.gitea/workflows/test-flight-cd.yaml +++ b/.gitea/workflows/test-flight-cd.yaml @@ -44,20 +44,9 @@ jobs: - name: Setup API key run: | - echo "API_APPSTORE_KEY length: ${#API_APPSTORE_KEY}" - if [ -z "$API_APPSTORE_KEY" ]; then - echo "ERROR: API_APPSTORE_KEY is empty or not set" - exit 1 - fi echo "${API_APPSTORE_KEY}" | tr -d '\n' | base64 --decode > ios/AuthKey.p8 chmod 600 ios/AuthKey.p8 ls -la ios/AuthKey.p8 - echo "File size: $(wc -c < ios/AuthKey.p8) bytes" - echo "First few characters of decoded file:" - head -c 50 ios/AuthKey.p8 | xxd - echo "Current directory: $(pwd)" - echo "Contents of ios directory:" - ls -la ios/ env: API_APPSTORE_KEY: ${{ vars.API_APPSTORE_KEY }} @@ -68,15 +57,6 @@ jobs: flutter pub upgrade flutter pub run build_runner build --delete-conflicting-outputs cd ios - echo "Current directory after cd ios: $(pwd)" - echo "Contents of current directory:" - ls -la - echo "Checking if AuthKey.p8 exists:" - if [ -f "AuthKey.p8" ]; then - echo "AuthKey.p8 exists, size: $(wc -c < AuthKey.p8) bytes" - else - echo "AuthKey.p8 does not exist" - fi pod update fastlane beta env: diff --git a/ios/fastlane/Fastfile b/ios/fastlane/Fastfile index 6ecef599..059e30e1 100644 --- a/ios/fastlane/Fastfile +++ b/ios/fastlane/Fastfile @@ -18,14 +18,13 @@ default_platform(:ios) platform :ios do desc "Push a new beta build to TestFlight" lane :beta do - UI.message "Current working directory: #{Dir.pwd}" UI.message "Checking API key file..." - UI.message "Files in current directory: #{Dir.entries('.').join(', ')}" - if File.exist?("./AuthKey.p8") + + auth_key_path = "../AuthKey.p8" + if File.exist?(auth_key_path) UI.message "API key file exists" - UI.message "File size: #{File.size('./AuthKey.p8')} bytes" else - UI.user_error!("API key file not found at ./AuthKey.p8") + UI.user_error!("API key file not found at #{auth_key_path}") end match(type: "appstore", readonly: true) @@ -42,14 +41,14 @@ platform :ios do app_store_connect_api_key( key_id: "325JS9JCZH", issuer_id: "dfa27d1d-2bb8-4d4d-847e-0b32f9b880b3", - key_filepath: "./AuthKey.p8", + key_filepath: "../AuthKey.p8", duration: 1200, in_house: false ) begin upload_to_testflight( - api_key_path: "./AuthKey.p8", + api_key_path: "../AuthKey.p8", skip_waiting_for_build_processing: true, skip_submission: true, distribute_external: false, From cdb6fcb462b3738ebef0843aaf314e2cf29cf919 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Fri, 8 Aug 2025 11:23:55 +0300 Subject: [PATCH 087/134] fix: fastlane --- .gitea/workflows/test-flight-cd.yaml | 3 +++ ios/fastlane/Fastfile | 29 +++++++++++++++++++++------- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/.gitea/workflows/test-flight-cd.yaml b/.gitea/workflows/test-flight-cd.yaml index 8b3a8cf5..ac54d483 100644 --- a/.gitea/workflows/test-flight-cd.yaml +++ b/.gitea/workflows/test-flight-cd.yaml @@ -47,6 +47,9 @@ jobs: echo "${API_APPSTORE_KEY}" | tr -d '\n' | base64 --decode > ios/AuthKey.p8 chmod 600 ios/AuthKey.p8 ls -la ios/AuthKey.p8 + echo "File size: $(wc -c < ios/AuthKey.p8) bytes" + echo "Contents of ios directory:" + ls -la ios/ env: API_APPSTORE_KEY: ${{ vars.API_APPSTORE_KEY }} diff --git a/ios/fastlane/Fastfile b/ios/fastlane/Fastfile index 059e30e1..8304f8bc 100644 --- a/ios/fastlane/Fastfile +++ b/ios/fastlane/Fastfile @@ -18,13 +18,28 @@ default_platform(:ios) platform :ios do desc "Push a new beta build to TestFlight" lane :beta do + UI.message "Current working directory: #{Dir.pwd}" UI.message "Checking API key file..." - auth_key_path = "../AuthKey.p8" - if File.exist?(auth_key_path) - UI.message "API key file exists" - else - UI.user_error!("API key file not found at #{auth_key_path}") + possible_paths = [ + "../AuthKey.p8", + "./AuthKey.p8", + "../../AuthKey.p8", + File.expand_path("../AuthKey.p8"), + File.expand_path("./AuthKey.p8") + ] + + auth_key_path = nil + possible_paths.each do |path| + if File.exist?(path) + UI.message "Found AuthKey.p8 at: #{path}" + auth_key_path = path + break + end + end + + if auth_key_path.nil? + UI.user_error!("API key file not found. Tried paths: #{possible_paths.join(', ')}") end match(type: "appstore", readonly: true) @@ -41,14 +56,14 @@ platform :ios do app_store_connect_api_key( key_id: "325JS9JCZH", issuer_id: "dfa27d1d-2bb8-4d4d-847e-0b32f9b880b3", - key_filepath: "../AuthKey.p8", + key_filepath: auth_key_path, duration: 1200, in_house: false ) begin upload_to_testflight( - api_key_path: "../AuthKey.p8", + api_key_path: auth_key_path, skip_waiting_for_build_processing: true, skip_submission: true, distribute_external: false, From 3c56c59c9f6289e32b73a432b1f7730ad04c2a54 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Fri, 8 Aug 2025 11:29:47 +0300 Subject: [PATCH 088/134] fix: fastlane --- ios/fastlane/Fastfile | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/ios/fastlane/Fastfile b/ios/fastlane/Fastfile index 8304f8bc..4d2e0516 100644 --- a/ios/fastlane/Fastfile +++ b/ios/fastlane/Fastfile @@ -33,7 +33,7 @@ platform :ios do possible_paths.each do |path| if File.exist?(path) UI.message "Found AuthKey.p8 at: #{path}" - auth_key_path = path + auth_key_path = File.expand_path(path) break end end @@ -42,6 +42,10 @@ platform :ios do UI.user_error!("API key file not found. Tried paths: #{possible_paths.join(', ')}") end + UI.message "Using auth_key_path: #{auth_key_path}" + UI.message "File exists check: #{File.exist?(auth_key_path)}" + UI.message "File size: #{File.size(auth_key_path)} bytes" + match(type: "appstore", readonly: true) increment_build_number(xcodeproj: "Runner.xcodeproj") @@ -53,6 +57,9 @@ platform :ios do xcargs: "--dart-define=HIDE_PLANS=true" ) + UI.message "About to call app_store_connect_api_key with key_filepath: #{auth_key_path}" + UI.message "File still exists: #{File.exist?(auth_key_path)}" + app_store_connect_api_key( key_id: "325JS9JCZH", issuer_id: "dfa27d1d-2bb8-4d4d-847e-0b32f9b880b3", From 06117ddb6a2a699bf31eeb367e5cec87f028088b Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Fri, 8 Aug 2025 11:34:41 +0300 Subject: [PATCH 089/134] fix: fastlane --- ios/fastlane/Fastfile | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/ios/fastlane/Fastfile b/ios/fastlane/Fastfile index 4d2e0516..f7261196 100644 --- a/ios/fastlane/Fastfile +++ b/ios/fastlane/Fastfile @@ -43,8 +43,6 @@ platform :ios do end UI.message "Using auth_key_path: #{auth_key_path}" - UI.message "File exists check: #{File.exist?(auth_key_path)}" - UI.message "File size: #{File.size(auth_key_path)} bytes" match(type: "appstore", readonly: true) increment_build_number(xcodeproj: "Runner.xcodeproj") @@ -57,8 +55,7 @@ platform :ios do xcargs: "--dart-define=HIDE_PLANS=true" ) - UI.message "About to call app_store_connect_api_key with key_filepath: #{auth_key_path}" - UI.message "File still exists: #{File.exist?(auth_key_path)}" + app_store_connect_api_key( key_id: "325JS9JCZH", @@ -70,7 +67,6 @@ platform :ios do begin upload_to_testflight( - api_key_path: auth_key_path, skip_waiting_for_build_processing: true, skip_submission: true, distribute_external: false, From 60d30163e7b79e8b4f3e7a09adeb2d801eb51205 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Fri, 8 Aug 2025 11:40:32 +0300 Subject: [PATCH 090/134] fix(fastlane): update version --- 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 14890f55..2e0d56e4 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -396,7 +396,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 66; + CURRENT_PROJECT_VERSION = 67; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -546,7 +546,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 66; + CURRENT_PROJECT_VERSION = 67; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -584,7 +584,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 66; + CURRENT_PROJECT_VERSION = 67; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 24c51e81..a1ded5ee 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -21,7 +21,7 @@ CFBundleSignature ???? CFBundleVersion - 66 + 67 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/pubspec.yaml b/pubspec.yaml index a56c030b..3895a06b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: eatvio_app description: A new Flutter project. -version: 1.2.7+66 +version: 1.2.8+68 publish_to: 'none' environment: From 3e8c8c7e9f5acced311512e4ca4acf32589041e0 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Fri, 8 Aug 2025 12:16:20 +0300 Subject: [PATCH 091/134] fix(env): remove default value from config --- lib/core/env_config.dart | 42 ++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/lib/core/env_config.dart b/lib/core/env_config.dart index 23c18f62..b363ad70 100644 --- a/lib/core/env_config.dart +++ b/lib/core/env_config.dart @@ -1,43 +1,43 @@ import 'package:flutter_dotenv/flutter_dotenv.dart'; class EnvConfig { - static String get baseUrl => dotenv.env['BASE_URL'] ?? 'https://eatvio.ru'; - static String get devBaseUrl => dotenv.env['DEV_BASE_URL'] ?? 'https://dev2.eatvio.com'; + static String get baseUrl => dotenv.env['BASE_URL'] ?? ''; + static String get devBaseUrl => dotenv.env['DEV_BASE_URL'] ?? ''; // Sentry Configuration - static String get sentryDsn => dotenv.env['SENTRY_DSN'] ?? 'http://ba7f2653067776f3034e6e89850e85a1@sentry.md-desk.ru:9000/2'; + static String get sentryDsn => dotenv.env['SENTRY_DSN'] ?? ''; // Firebase Configuration - static String get firebaseProjectId => dotenv.env['FIREBASE_PROJECT_ID'] ?? 'eatvio-3467b'; - static String get firebaseDatabaseUrl => dotenv.env['FIREBASE_DATABASE_URL'] ?? 'https://eatvio-3467b-default-rtdb.europe-west1.firebasedatabase.app'; - static String get firebaseStorageBucket => dotenv.env['FIREBASE_STORAGE_BUCKET'] ?? 'eatvio-3467b.firebasestorage.app'; - static String get firebaseMessagingSenderId => dotenv.env['FIREBASE_MESSAGING_SENDER_ID'] ?? '613980912922'; + static String get firebaseProjectId => dotenv.env['FIREBASE_PROJECT_ID'] ?? ''; + static String get firebaseDatabaseUrl => dotenv.env['FIREBASE_DATABASE_URL'] ?? ''; + static String get firebaseStorageBucket => dotenv.env['FIREBASE_STORAGE_BUCKET'] ?? ''; + static String get firebaseMessagingSenderId => dotenv.env['FIREBASE_MESSAGING_SENDER_ID'] ?? ''; // Firebase Android Configuration - static String get firebaseAndroidApiKey => dotenv.env['FIREBASE_ANDROID_API_KEY'] ?? 'AIzaSyBUe1UqAAMfgMcd4hWdRoLF1Os-yn-HE9k'; - static String get firebaseAndroidAppId => dotenv.env['FIREBASE_ANDROID_APP_ID'] ?? '1:613980912922:android:4de95e5f2d50ba69ecc0a3'; + static String get firebaseAndroidApiKey => dotenv.env['FIREBASE_ANDROID_API_KEY'] ?? ''; + static String get firebaseAndroidAppId => dotenv.env['FIREBASE_ANDROID_APP_ID'] ?? ''; // Firebase iOS Configuration - static String get firebaseIosApiKey => dotenv.env['FIREBASE_IOS_API_KEY'] ?? 'AIzaSyBnE8tQjL9_1HuXrTnqNy6qbXpoenO36yQ'; - static String get firebaseIosAppId => dotenv.env['FIREBASE_IOS_APP_ID'] ?? '1:613980912922:ios:c557d140a1212e63ecc0a3'; - static String get firebaseIosBundleId => dotenv.env['FIREBASE_IOS_BUNDLE_ID'] ?? 'com.eatvioapp.app'; + static String get firebaseIosApiKey => dotenv.env['FIREBASE_IOS_API_KEY'] ?? ''; + static String get firebaseIosAppId => dotenv.env['FIREBASE_IOS_APP_ID'] ?? ''; + static String get firebaseIosBundleId => dotenv.env['FIREBASE_IOS_BUNDLE_ID'] ?? ''; // App Store URLs - static String get appStoreUrl => dotenv.env['APP_STORE_URL'] ?? 'https://apps.apple.com/app/idYOUR_APP_ID'; - static String get rustoreUrl => dotenv.env['RUSTORE_URL'] ?? 'https://www.rustore.ru/catalog/app/com.eatvioapp.app'; + static String get appStoreUrl => dotenv.env['APP_STORE_URL'] ?? ''; + static String get rustoreUrl => dotenv.env['RUSTORE_URL'] ?? ''; // Telegram Support - static String get telegramSupportUrl => dotenv.env['TELEGRAM_SUPPORT_URL'] ?? 'https://t.me/eatvio'; - static String get supportEmail => dotenv.env['SUPPORT_EMAIL'] ?? 'help@eatv.io'; + static String get telegramSupportUrl => dotenv.env['TELEGRAM_SUPPORT_URL'] ?? ''; + static String get supportEmail => dotenv.env['SUPPORT_EMAIL'] ?? ''; // Test Credentials (for development only) - static String get testEmail => dotenv.env['TEST_EMAIL'] ?? 'reshetnick.mike+user1@gmail.com'; - static String get testPassword => dotenv.env['TEST_PASSWORD'] ?? '1234567890'; + static String get testEmail => dotenv.env['TEST_EMAIL'] ?? ''; + static String get testPassword => dotenv.env['TEST_PASSWORD'] ?? ''; // Email Validation API - static String get emailValidationApiUrl => dotenv.env['EMAIL_VALIDATION_API_URL'] ?? 'https://api.validate.email/validate'; + static String get emailValidationApiUrl => dotenv.env['EMAIL_VALIDATION_API_URL'] ?? ''; // Connection Check URLs - static String get googleCheckUrl => dotenv.env['GOOGLE_CHECK_URL'] ?? 'https://google.com'; - static String get yandexCheckUrl => dotenv.env['YANDEX_CHECK_URL'] ?? 'https://yandex.ru'; + static String get googleCheckUrl => dotenv.env['GOOGLE_CHECK_URL'] ?? ''; + static String get yandexCheckUrl => dotenv.env['YANDEX_CHECK_URL'] ?? ''; } From 4b1d21b4af454224200bdad7ab4947aad0ed317c Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Fri, 8 Aug 2025 13:29:32 +0300 Subject: [PATCH 092/134] fix(finances): Issue-186 --- .../consultations/services/consultation_service.dart | 6 ++++-- .../cubit/archive/consultations_archive_state.dart | 2 +- .../consultant_finances/consultant_finances_state.dart | 2 +- .../cubit/user_purchases/user_purchases_state.dart | 2 +- .../presentation/consultant_finances_screen.dart | 4 ++-- 5 files changed, 9 insertions(+), 7 deletions(-) diff --git a/lib/features/consultations/services/consultation_service.dart b/lib/features/consultations/services/consultation_service.dart index f0a89220..a3935d5a 100644 --- a/lib/features/consultations/services/consultation_service.dart +++ b/lib/features/consultations/services/consultation_service.dart @@ -77,7 +77,8 @@ class ConsultationService { final response = await dio.get( '/api/finances/my_finances', queryParameters: { - if (dateComparison != null && date != null) 'filters[end_date]': date, + if (date != null && dateComparison != null) 'filters[start_date][${dateComparison.value}]': date, + 'sort[]': 'start_date:desc', }, ); final data = response.data as Map; @@ -96,7 +97,8 @@ class ConsultationService { final response = await dio.get( '/api/finances/my_purchases', queryParameters: { - if (dateComparison != null && date != null) 'filters[start_date]': date, + if (date != null && dateComparison != null) 'filters[start_date][${dateComparison.value}]': date, + 'sort[]': 'start_date:desc', }, ); final data = response.data as Map; diff --git a/lib/features/finances_and_consultations_archive/cubit/archive/consultations_archive_state.dart b/lib/features/finances_and_consultations_archive/cubit/archive/consultations_archive_state.dart index c54f8073..dfe84a55 100644 --- a/lib/features/finances_and_consultations_archive/cubit/archive/consultations_archive_state.dart +++ b/lib/features/finances_and_consultations_archive/cubit/archive/consultations_archive_state.dart @@ -14,7 +14,7 @@ abstract class ConsultationsArchiveState with _$ConsultationsArchiveState { @Default([]) List consultations, @Default(false) isLoading, @Default(null) String? error, - @Default(null) PeriodFilterStatus? periodFilterStatus, + @Default(PeriodFilterStatus.all) PeriodFilterStatus? periodFilterStatus, @Default(null) ConsultationFilterStatus? consultationFilterStatus, required StatisticModel statisticModel, }) = _ConsultationsArchiveState; diff --git a/lib/features/finances_and_consultations_archive/cubit/consultant_finances/consultant_finances_state.dart b/lib/features/finances_and_consultations_archive/cubit/consultant_finances/consultant_finances_state.dart index fff61e45..4e1123e1 100644 --- a/lib/features/finances_and_consultations_archive/cubit/consultant_finances/consultant_finances_state.dart +++ b/lib/features/finances_and_consultations_archive/cubit/consultant_finances/consultant_finances_state.dart @@ -10,7 +10,7 @@ abstract class ConsultantFinancesState with _$ConsultantFinancesState { @Default([]) List financesModels, @Default(false) isLoading, @Default(null) String? error, - @Default(null) PeriodFilterStatus? periodFilterStatus, + @Default(PeriodFilterStatus.all) PeriodFilterStatus? periodFilterStatus, }) = _ConsultantFinancesState; } diff --git a/lib/features/finances_and_consultations_archive/cubit/user_purchases/user_purchases_state.dart b/lib/features/finances_and_consultations_archive/cubit/user_purchases/user_purchases_state.dart index 7ae22c03..65a53842 100644 --- a/lib/features/finances_and_consultations_archive/cubit/user_purchases/user_purchases_state.dart +++ b/lib/features/finances_and_consultations_archive/cubit/user_purchases/user_purchases_state.dart @@ -10,6 +10,6 @@ abstract class UserPurchasesState with _$UserPurchasesState { @Default([]) List purchasesModels, @Default(false) isLoading, @Default(null) String? error, - @Default(null) PeriodFilterStatus? periodFilterStatus, + @Default(PeriodFilterStatus.all) PeriodFilterStatus? periodFilterStatus, }) = _UserPurchasesState; } diff --git a/lib/features/finances_and_consultations_archive/presentation/consultant_finances_screen.dart b/lib/features/finances_and_consultations_archive/presentation/consultant_finances_screen.dart index 7e731474..085e9fa6 100644 --- a/lib/features/finances_and_consultations_archive/presentation/consultant_finances_screen.dart +++ b/lib/features/finances_and_consultations_archive/presentation/consultant_finances_screen.dart @@ -96,7 +96,7 @@ class _ConsultantFinancesScreenState extends State { if (value == null) return; context.read().setFinancesPeriod(value); }, - value: state.periodFilterStatus ?? PeriodFilterStatus.values.first, + value: state.periodFilterStatus ?? PeriodFilterStatus.all, ), const SizedBox(height: 12), if (state.financesModels.isNotEmpty) @@ -129,7 +129,7 @@ class _ConsultantFinancesScreenState extends State { ); }), Text( - state.financesModels[index].startDate.format('yyyy.MM.dd'), + state.financesModels[index].startDate.format('dd.MM.yy'), style: const TextStyle( color: Color(0xFF6D6F71), height: 1, From 412ac0aeeb8e754906d8a83d3387b7141d542418 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Fri, 8 Aug 2025 13:43:04 +0300 Subject: [PATCH 093/134] fix(archive_of_consultatons): Issue-331 --- .../consultations/services/consultation_service.dart | 1 + .../cubit/archive/consultations_archive_cubit.dart | 9 ++------- .../presentation/consultations_archive_screen.dart | 2 +- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/lib/features/consultations/services/consultation_service.dart b/lib/features/consultations/services/consultation_service.dart index a3935d5a..8f09e4a1 100644 --- a/lib/features/consultations/services/consultation_service.dart +++ b/lib/features/consultations/services/consultation_service.dart @@ -50,6 +50,7 @@ class ConsultationService { else if (state != null && stateComparison != null) 'filters[last_state][${stateComparison.value}]': state, if (dateComparison != null && date != null) 'filters[start_date][${dateComparison.value}]': date, + 'sort[]': 'created_at:desc', }, ); return ConsultationPaginationModel.fromJson(response.data); diff --git a/lib/features/finances_and_consultations_archive/cubit/archive/consultations_archive_cubit.dart b/lib/features/finances_and_consultations_archive/cubit/archive/consultations_archive_cubit.dart index dd6c713e..a2f6daf0 100644 --- a/lib/features/finances_and_consultations_archive/cubit/archive/consultations_archive_cubit.dart +++ b/lib/features/finances_and_consultations_archive/cubit/archive/consultations_archive_cubit.dart @@ -31,9 +31,7 @@ class ConsultationsArchiveCubit extends Cubit { dateComparison: state.periodFilterStatus == PeriodFilterStatus.all ? null : FiltersComparisonEnum.greaterThanOrEqual, stateComparison: state.consultationFilterStatus == ConsultationFilterStatus.all ? null - : state.consultationFilterStatus == ConsultationFilterStatus.inProgress - ? FiltersComparisonEnum.notInArray - : FiltersComparisonEnum.equal, + : FiltersComparisonEnum.equal, ); emit(state.copyWith(consultations: consultations, isLoading: false)); } catch (e) { @@ -67,20 +65,17 @@ class ConsultationsArchiveCubit extends Cubit { enum ConsultationFilterStatus { all, canceled, - finished, - inProgress; + finished; String get name => switch (this) { ConsultationFilterStatus.all => 'Все консультации', ConsultationFilterStatus.canceled => 'Отменён', ConsultationFilterStatus.finished => 'Завершён', - ConsultationFilterStatus.inProgress => 'В процессе', }; String? get value => switch (this) { ConsultationFilterStatus.all => null, ConsultationFilterStatus.canceled => 'canceled_by_the_initiator', ConsultationFilterStatus.finished => 'charge_was_collected', - ConsultationFilterStatus.inProgress => 'active', }; } diff --git a/lib/features/finances_and_consultations_archive/presentation/consultations_archive_screen.dart b/lib/features/finances_and_consultations_archive/presentation/consultations_archive_screen.dart index ec2d4900..c356e9fb 100644 --- a/lib/features/finances_and_consultations_archive/presentation/consultations_archive_screen.dart +++ b/lib/features/finances_and_consultations_archive/presentation/consultations_archive_screen.dart @@ -195,7 +195,7 @@ class _ConsultationsArchiveScreenState extends State ), ), Text( - state.consultations[index].startDate.format('yyyy.MM.dd'), + state.consultations[index].startDate.format('dd.MM.yy'), style: const TextStyle( color: Color(0xFF6D6F71), height: 1, From 48f433288f66fb8616cd908ce01a86dae3c71b62 Mon Sep 17 00:00:00 2001 From: Username1548 Date: Mon, 11 Aug 2025 02:52:41 +0700 Subject: [PATCH 094/134] feat(chats): added message resend and deleting, feat(menu): added build number --- assets/icons/resend.svg | 3 + .../comments_actions_bottom_sheet.dart | 45 +----- .../widgets/bottom_sheet_action_widget.dart | 33 +++++ .../chat/models/chat_message_model.dart | 4 + .../bloc/chat_room_cubit/chat_room_cubit.dart | 137 ++++++++++++++---- .../chat/presentation/pages/chat_screen.dart | 118 +++++---------- .../widgets/chat_bubble_widget.dart | 89 ++++++------ .../widgets/message_action_bottom_sheet.dart | 48 ++++++ .../menu/presentation/pages/menu_screen.dart | 37 +++++ lib/generated/intl/messages_en.dart | 1 + lib/generated/l10n.dart | 10 ++ lib/l10n/intl_en.arb | 4 +- pubspec.yaml | 1 + 13 files changed, 333 insertions(+), 197 deletions(-) create mode 100644 assets/icons/resend.svg create mode 100644 lib/core/widgets/bottom_sheet_action_widget.dart create mode 100644 lib/features/chat/presentation/widgets/message_action_bottom_sheet.dart diff --git a/assets/icons/resend.svg b/assets/icons/resend.svg new file mode 100644 index 00000000..245a03be --- /dev/null +++ b/assets/icons/resend.svg @@ -0,0 +1,3 @@ + + + diff --git a/lib/core/comments/presentation/comments_actions_bottom_sheet.dart b/lib/core/comments/presentation/comments_actions_bottom_sheet.dart index 5506215d..3c1c3859 100644 --- a/lib/core/comments/presentation/comments_actions_bottom_sheet.dart +++ b/lib/core/comments/presentation/comments_actions_bottom_sheet.dart @@ -1,11 +1,10 @@ -import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/core/widgets/bottom_sheet_action_widget.dart'; import 'package:eatvio_app/core/widgets/bottom_sheet_handler_bar.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/generated/l10n.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_svg/svg.dart'; import 'package:go_router/go_router.dart'; void showCommentsActionsBottomSheet( @@ -123,7 +122,7 @@ class _CommentsActionsBottomSheetState extends State ), const BottomSheetHandlerBar(), if (widget.isMyComment) - _ActionWidget( + BottomSheetActionWidget( text: s.edit, iconPath: 'assets/icons/edit_paint.svg', needToPop: false, @@ -132,13 +131,13 @@ class _CommentsActionsBottomSheetState extends State }), ) else if (widget.reply != null) - _ActionWidget( + BottomSheetActionWidget( text: s.reply, iconPath: 'assets/icons/reply.svg', onTap: widget.reply!, ), if (widget.reply != null) const CustomDivider(), - _ActionWidget( + BottomSheetActionWidget( text: s.copy, iconPath: 'assets/icons/copy2.svg', onTap: widget.copy, @@ -146,21 +145,21 @@ class _CommentsActionsBottomSheetState extends State if (widget.isMyComment) ...[ if (widget.reply != null) ...[ const CustomDivider(), - _ActionWidget( + BottomSheetActionWidget( text: s.reply, iconPath: 'assets/icons/reply.svg', onTap: widget.reply!, ), ], const CustomDivider(), - _ActionWidget( + BottomSheetActionWidget( text: s.deleteComment, iconPath: 'assets/icons/red_trash.svg', onTap: widget.delete, ), ] else ...[ const CustomDivider(), - _ActionWidget( + BottomSheetActionWidget( text: s.complain, iconPath: 'assets/icons/complain.svg', onTap: widget.complain, @@ -172,33 +171,3 @@ class _CommentsActionsBottomSheetState extends State } } -class _ActionWidget extends StatelessWidget { - const _ActionWidget({required this.text, required this.iconPath, required this.onTap, this.needToPop = true}); - final String text; - final String iconPath; - final void Function() onTap; - final bool needToPop; - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: () { - if (needToPop) Navigator.pop(context); - onTap(); - }, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 12), - child: Row( - children: [ - Expanded( - child: Text( - text, - style: TextStyles.textLMedium.copyWith(color: appTheme.black), - ), - ), - SvgPicture.asset(iconPath) - ], - ), - ), - ); - } -} diff --git a/lib/core/widgets/bottom_sheet_action_widget.dart b/lib/core/widgets/bottom_sheet_action_widget.dart new file mode 100644 index 00000000..004b5567 --- /dev/null +++ b/lib/core/widgets/bottom_sheet_action_widget.dart @@ -0,0 +1,33 @@ +import 'package:eatvio_app/core/theme/custom_text_style.dart'; +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; + +class BottomSheetActionWidget extends StatelessWidget { + const BottomSheetActionWidget({super.key, required this.text, required this.iconPath, required this.onTap, this.needToPop = true, this.color}); + final String text; + final String iconPath; + final void Function() onTap; + final bool needToPop; + final Color? color; + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + if (needToPop) Navigator.pop(context); + onTap(); + }, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: Row( + children: [ + Expanded( + child: Text(text, style: TextStyles.textLMedium.copyWith(color: appTheme.black)), + ), + SvgPicture.asset(iconPath, colorFilter: color != null ? ColorFilter.mode(color!, BlendMode.srcIn) : null), + ], + ), + ), + ); + } +} diff --git a/lib/features/chat/models/chat_message_model.dart b/lib/features/chat/models/chat_message_model.dart index 7224912a..8dd8545a 100644 --- a/lib/features/chat/models/chat_message_model.dart +++ b/lib/features/chat/models/chat_message_model.dart @@ -15,7 +15,11 @@ abstract class ChatMessageModel with _$ChatMessageModel { @JsonKey(name: 'read_by_to_user', defaultValue: false) required bool isRead, @JsonKey(name: 'is_author_message_auth_user') required bool? isCurrentUserAuthor, @JsonKey(name: 'room_id') required String? roomId, + @Default(MessageState.sent) @JsonKey(includeToJson: false, includeFromJson: false) MessageState messageState, + @Default(null) @JsonKey(includeToJson: false, includeFromJson: false) String? uuid, }) = _ChatMessageModel; factory ChatMessageModel.fromJson(Map json) => _$ChatMessageModelFromJson(json); } + +enum MessageState { loading, sent, error } diff --git a/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart b/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart index c4d8ed45..5206889b 100644 --- a/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart +++ b/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart @@ -4,6 +4,7 @@ import 'dart:developer'; import 'dart:io'; import 'package:audioplayers/audioplayers.dart'; +import 'package:collection/collection.dart'; import 'package:dart_pusher_channels/dart_pusher_channels.dart'; import 'package:easy_debounce/easy_debounce.dart'; import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart'; @@ -19,6 +20,7 @@ import 'package:eatvio_app/features/my_finance/models/card_model.dart'; import 'package:eatvio_app/features/my_finance/repositories/cards_repository.dart'; import 'package:eatvio_app/features/plans/models/chosen_plan_model.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:uuid/uuid.dart'; class ChatRoomCubit extends Cubit { final ChatRepository chatRepository; @@ -31,12 +33,9 @@ class ChatRoomCubit extends Cubit { late int lastPage; bool isLoading = false; Timer? _timer; + var uuid = const Uuid(); - ChatRoomCubit({ - required this.chatRepository, - required this.wsManager, - required this.cardsRepository, - }) : super(const ChatRoomState.loading()); + ChatRoomCubit({required this.chatRepository, required this.wsManager, required this.cardsRepository}) : super(const ChatRoomState.loading()); void setTimer() { _timer?.cancel(); @@ -89,10 +88,7 @@ class ChatRoomCubit extends Cubit { } } - Future initChat( - String uuid, { - bool sendInitialMessage = false, - }) async { + Future initChat(String uuid, {bool sendInitialMessage = false}) async { try { final chatRoom = await chatRepository.getChatRoom(uuid); ChosenPlanModel? chosenPlan; @@ -171,11 +167,7 @@ class ChatRoomCubit extends Cubit { List messagesList = [message, ...(state as ChatRoomStateLoaded).chatMessages]; chatRepository.emitNewMessageToChats(message); emit((state as ChatRoomStateLoaded).copyWith(chatMessages: messagesList, isLoading: isLoading)); - EasyDebounce.debounce( - 'searchChatRoom', - const Duration(milliseconds: 300), - () => sl().getChatRooms(), - ); + EasyDebounce.debounce('searchChatRoom', const Duration(milliseconds: 300), () => sl().getChatRooms()); }); typingSubscription = wsManager.clientTypingStream?.listen((event) { @@ -183,22 +175,14 @@ class ChatRoomCubit extends Cubit { EasyDebounce.debounce( 'client-typing', const Duration(seconds: 3), - () => emit( - (state as ChatRoomStateLoaded).copyWith( - isTyping: false, - isLoading: isLoading, - ), - ), + () => emit((state as ChatRoomStateLoaded).copyWith(isTyping: false, isLoading: isLoading)), ); }); } - Future sendMessage({ - String? data, - List? files, - required String userSlug, - }) async { + Future sendMessage({String? data, List? files, required String userSlug}) async { try { + final messageUuid = uuid.v4(); final compressedFiles = files != null ? await ImageCompressUtility.compressFilesList(List.from(files)) : null; AudioPlayer().play((AssetSource('audio/send_message.wav'))); emit( @@ -213,16 +197,38 @@ class ChatRoomCubit extends Cubit { isRead: true, isCurrentUserAuthor: true, roomId: '', + messageState: MessageState.loading, + uuid: messageUuid, ), ...(state as ChatRoomStateLoaded).chatMessages, ], ), ); - await chatRepository.sendMessage( - data: data, - files: compressedFiles, - slug: userSlug, - ); + + try { + await chatRepository.sendMessage(data: data, files: compressedFiles, slug: userSlug); + emit( + (state as ChatRoomStateLoaded).copyWith( + chatMessages: (state as ChatRoomStateLoaded).chatMessages.map((elem) { + if (elem.uuid == messageUuid) { + return elem.copyWith(messageState: MessageState.sent); + } + return elem; + }).toList(), + ), + ); + } catch (e) { + emit( + (state as ChatRoomStateLoaded).copyWith( + chatMessages: (state as ChatRoomStateLoaded).chatMessages.map((elem) { + if (elem.uuid == messageUuid) { + return elem.copyWith(messageState: MessageState.error); + } + return elem; + }).toList(), + ), + ); + } sl().getChatRooms(); return true; } catch (e, st) { @@ -268,7 +274,10 @@ class ChatRoomCubit extends Cubit { try { final planId = (state as ChatRoomStateLoaded).chatRoom.chosenPlan?.id; if (planId == null) return false; - await chatRepository.complainAboutConsultation(id: planId, body: PlanComplainModel(description: description)); + await chatRepository.complainAboutConsultation( + id: planId, + body: PlanComplainModel(description: description), + ); return true; } catch (e) { log(e.toString()); @@ -296,4 +305,70 @@ class ChatRoomCubit extends Cubit { typingSubscription?.cancel(); super.close(); } + + void deleteMessage(String uuid) { + final currentState = state; + if (currentState is ChatRoomStateLoaded) { + emit(currentState.copyWith(chatMessages: currentState.chatMessages.where((message) => message.uuid != uuid).toList())); + } + } + + Future resendMessage({required String uuid, required String userSlug}) async { + final currentState = state; + if (currentState is ChatRoomStateLoaded) { + final message = currentState.chatMessages.firstWhereOrNull((message) => message.uuid == uuid); + if (message != null) { + try { + AudioPlayer().play((AssetSource('audio/send_message.wav'))); + emit( + (state as ChatRoomStateLoaded).copyWith( + chatMessages: [ + ChatMessageModel( + id: 1, + data: message.data, + mediaData: message.mediaData, + authorId: 1, + updatedAt: DateTime.now(), + isRead: true, + isCurrentUserAuthor: true, + roomId: '', + messageState: MessageState.loading, + uuid: uuid, + ), + ...(state as ChatRoomStateLoaded).chatMessages.where((message) => message.uuid != uuid), + ], + ), + ); + + try { + await chatRepository.sendMessage(data: message.data, files: message.mediaData as List?, slug: userSlug); + emit( + (state as ChatRoomStateLoaded).copyWith( + chatMessages: (state as ChatRoomStateLoaded).chatMessages.map((elem) { + if (elem.uuid == uuid) { + return elem.copyWith(messageState: MessageState.sent); + } + return elem; + }).toList(), + ), + ); + } catch (e) { + emit( + (state as ChatRoomStateLoaded).copyWith( + chatMessages: (state as ChatRoomStateLoaded).chatMessages.map((elem) { + if (elem.uuid == uuid) { + return elem.copyWith(messageState: MessageState.error); + } + return elem; + }).toList(), + ), + ); + } + sl().getChatRooms(); + } catch (e, st) { + log(e.toString(), stackTrace: st); + } + } + } + } } diff --git a/lib/features/chat/presentation/pages/chat_screen.dart b/lib/features/chat/presentation/pages/chat_screen.dart index c017f047..1d7235c6 100644 --- a/lib/features/chat/presentation/pages/chat_screen.dart +++ b/lib/features/chat/presentation/pages/chat_screen.dart @@ -93,7 +93,10 @@ class _ChatScreenState extends State { if (state is ChatRoomStateLoaded) { return _buildChat(context, state); } else if (state is ChatRoomStateError) { - return Scaffold(appBar: AppBar(), body: DataErrorWidget(onReload: () => chatRoomCubit.initChat(widget.uuid))); + return Scaffold( + appBar: AppBar(), + body: DataErrorWidget(onReload: () => chatRoomCubit.initChat(widget.uuid)), + ); } else { return const Scaffold(body: Loader()); } @@ -168,34 +171,19 @@ class _ChatScreenState extends State { width: 40, height: 40, alignment: Alignment.center, - decoration: const BoxDecoration( - color: Color(0xFFEBF4FF), - shape: BoxShape.circle, - ), + decoration: const BoxDecoration(color: Color(0xFFEBF4FF), shape: BoxShape.circle), child: Text( state.chatRoom.users.receiver.name.split(' ').first[0] + state.chatRoom.users.receiver.name.split(' ')[1][0], - style: const TextStyle( - color: Color(0xFF7F9CF5), - fontWeight: FontWeight.w400, - fontSize: 20, - ), + style: const TextStyle(color: Color(0xFF7F9CF5), fontWeight: FontWeight.w400, fontSize: 20), ), ), const SizedBox(width: 16), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - state.chatRoom.users.receiver.name, - style: CustomTextStyles.titleSmallBold.copyWith( - color: PrimaryColors().black, - ), - ), + Text(state.chatRoom.users.receiver.name, style: CustomTextStyles.titleSmallBold.copyWith(color: PrimaryColors().black)), AnimatedCrossFade( - firstChild: Text( - 'Печатает...', - style: CustomTextStyles.bodySmallMulishPrimary, - ), + firstChild: Text('Печатает...', style: CustomTextStyles.bodySmallMulishPrimary), firstCurve: Curves.easeIn, secondCurve: Curves.easeOut, secondChild: const SizedBox.shrink(), @@ -251,9 +239,9 @@ class _ChatScreenState extends State { if (!result) return; if (!context.mounted) return; context.read().reduceUnreadMessagesCount( - chatId: state.chatRoom.id, - count: unreadMessagesIdsCopy.length, - ); + chatId: state.chatRoom.id, + count: unreadMessagesIdsCopy.length, + ); unreadMessagesIdsCopy.clear(); }); } @@ -264,6 +252,9 @@ class _ChatScreenState extends State { text: state.chatMessages[index].data.trim(), isCurrentUser: isCurrentUserAuthor, mediaData: state.chatMessages[index].mediaData ?? [], + messageState: state.chatMessages[index].messageState, + messageUuid: state.chatMessages[index].uuid ?? '', + userSlug: state.chatRoom.users.receiver.slug, ), ), ], @@ -290,6 +281,9 @@ class _ChatScreenState extends State { text: state.chatMessages[index].data.trim(), isCurrentUser: isCurrentUserAuthor, mediaData: state.chatMessages[index].mediaData ?? [], + messageState: state.chatMessages[index].messageState, + messageUuid: state.chatMessages[index].uuid ?? '', + userSlug: state.chatRoom.users.receiver.slug, ), ); }, @@ -361,11 +355,7 @@ class _ChatScreenState extends State { final imageIndex = _imageIndexAt(pickedFiles, i); context.pushTransparentRoute( - PhotoViewScreen( - imageUrls: imageFiles.map((e) => e.path).toList(), - isLocal: true, - index: imageIndex, - ), + PhotoViewScreen(imageUrls: imageFiles.map((e) => e.path).toList(), isLocal: true, index: imageIndex), ); }, child: Hero( @@ -373,14 +363,7 @@ class _ChatScreenState extends State { transitionOnUserGestures: true, child: ClipRRect( borderRadius: BorderRadius.circular(8), - child: SizedBox( - height: 104, - width: 104, - child: Image.file( - pickedFiles[i], - fit: BoxFit.cover, - ), - ), + child: SizedBox(height: 104, width: 104, child: Image.file(pickedFiles[i], fit: BoxFit.cover)), ), ), ) @@ -397,15 +380,8 @@ class _ChatScreenState extends State { }, child: Container( padding: const EdgeInsets.all(4), - decoration: BoxDecoration( - color: appTheme.gray50, - shape: BoxShape.circle, - ), - child: const Icon( - size: 20, - Icons.close, - color: Colors.black, - ), + decoration: BoxDecoration(color: appTheme.gray50, shape: BoxShape.circle), + child: const Icon(size: 20, Icons.close, color: Colors.black), ), ), ), @@ -427,7 +403,10 @@ class _ChatScreenState extends State { child: Ink( height: 40, width: 40, - child: Transform.rotate(angle: pi / 4, child: const Icon(Icons.attach_file_rounded, size: 30, color: Color(0xFF858E99))), + child: Transform.rotate( + angle: pi / 4, + child: const Icon(Icons.attach_file_rounded, size: 30, color: Color(0xFF858E99)), + ), ), ), const SizedBox(width: 8), @@ -449,7 +428,9 @@ class _ChatScreenState extends State { hintStyle: const TextStyle(fontSize: 14, fontWeight: FontWeight.w400, height: 1, color: Color(0xFFAEAEB2)), filled: true, border: OutlineInputBorder( - borderRadius: BorderRadius.circular(100), borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1)), + borderRadius: BorderRadius.circular(100), + borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1), + ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(100), borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1), @@ -525,13 +506,7 @@ class _ChatScreenState extends State { const Icon(Icons.attach_file, size: 32, color: Colors.black54), Padding( padding: const EdgeInsets.symmetric(horizontal: 4), - child: Text( - fileName, - textAlign: TextAlign.center, - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: const TextStyle(fontSize: 12), - ), + child: Text(fileName, textAlign: TextAlign.center, maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 12)), ), ], ), @@ -579,13 +554,7 @@ class _ChatScreenState extends State { // await chatRoomCubit.sendMessage(files: [File(image.path)], userSlug: slug); // } }, - child: const Row( - children: [ - Icon(Icons.photo_camera), - SizedBox(width: 8), - Text('Фото'), - ], - ), + child: const Row(children: [Icon(Icons.photo_camera), SizedBox(width: 8), Text('Фото')]), ), CupertinoActionSheetAction( onPressed: () async { @@ -600,13 +569,7 @@ class _ChatScreenState extends State { // await chatRoomCubit.sendMessage(files: files, userSlug: slug); } }, - child: const Row( - children: [ - Icon(Icons.photo_library_rounded), - SizedBox(width: 8), - Text('Галерея'), - ], - ), + child: const Row(children: [Icon(Icons.photo_library_rounded), SizedBox(width: 8), Text('Галерея')]), ), CupertinoActionSheetAction( onPressed: () async { @@ -643,20 +606,10 @@ class _ChatScreenState extends State { // await chatRoomCubit.sendMessage(files: files, userSlug: slug); } }, - child: const Row( - children: [ - Icon(Icons.file_present_rounded), - SizedBox(width: 8), - Text('Файлы'), - ], - ), + child: const Row(children: [Icon(Icons.file_present_rounded), SizedBox(width: 8), Text('Файлы')]), ), ], - cancelButton: CupertinoActionSheetAction( - onPressed: () => Navigator.of(context).pop(), - isDestructiveAction: true, - child: const Text('Отмена'), - ), + cancelButton: CupertinoActionSheetAction(onPressed: () => Navigator.of(context).pop(), isDestructiveAction: true, child: const Text('Отмена')), ), ); } @@ -665,7 +618,10 @@ class _ChatScreenState extends State { return Center( child: Padding( padding: const EdgeInsets.only(top: 32), - child: Text(DateFormat.yMMMMd('RU').format(date), style: const TextStyle(height: 1, color: Color(0xFF8393A1), fontWeight: FontWeight.w500)), + child: Text( + DateFormat.yMMMMd('RU').format(date), + style: const TextStyle(height: 1, color: Color(0xFF8393A1), fontWeight: FontWeight.w500), + ), ), ); } diff --git a/lib/features/chat/presentation/widgets/chat_bubble_widget.dart b/lib/features/chat/presentation/widgets/chat_bubble_widget.dart index 60a31c44..eb387691 100644 --- a/lib/features/chat/presentation/widgets/chat_bubble_widget.dart +++ b/lib/features/chat/presentation/widgets/chat_bubble_widget.dart @@ -5,8 +5,13 @@ import 'package:dismissible_page/dismissible_page.dart'; import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/utils/date_time_utils.dart'; +import 'package:eatvio_app/features/chat/models/chat_message_model.dart'; +import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart'; import 'package:eatvio_app/features/chat/presentation/pages/photo_view_screen.dart'; +import 'package:eatvio_app/features/chat/presentation/widgets/message_action_bottom_sheet.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/svg.dart'; import 'package:url_launcher/url_launcher.dart'; class ChatBubbleWidget extends StatelessWidget { @@ -17,6 +22,9 @@ class ChatBubbleWidget extends StatelessWidget { required this.isCurrentUser, required this.mediaData, required this.isRead, + required this.messageState, + required this.messageUuid, + required this.userSlug }); final DateTime sendTime; @@ -24,7 +32,9 @@ class ChatBubbleWidget extends StatelessWidget { final bool isCurrentUser; final bool isRead; final List mediaData; - + final MessageState messageState; + final String messageUuid; + final String userSlug; @override Widget build(BuildContext context) { final size = MediaQuery.sizeOf(context); @@ -38,17 +48,33 @@ class ChatBubbleWidget extends StatelessWidget { mainAxisAlignment: isCurrentUser ? MainAxisAlignment.end : MainAxisAlignment.start, children: [ if (isCurrentUser) - Padding( - padding: const EdgeInsets.only(right: 4, bottom: 4), - child: Text( - sendTime.format('HH:mm', 'ru'), - style: const TextStyle( - color: Color(0xFF7B8794), - height: 1, - fontSize: 10, - ), + switch (messageState) { + MessageState.loading => Padding( + padding: const EdgeInsets.only(right: 4, bottom: 4), + child: SizedBox(width: 10, height: 10, child: CircularProgressIndicator(strokeWidth: 2, color: appTheme.blueA400)), ), - ), + + MessageState.sent => Padding( + padding: const EdgeInsets.only(right: 4, bottom: 4), + child: Text(sendTime.format('HH:mm', 'ru'), style: const TextStyle(color: Color(0xFF7B8794), height: 1, fontSize: 10)), + ), + + MessageState.error => GestureDetector( + onTap: () { + showMessageActionsBottomSheet( + context, + resend: () { + context.read().resendMessage(uuid: messageUuid, userSlug: userSlug); + }, + delete: () { + context.read().deleteMessage(messageUuid); + }, + ); + }, + child: Padding(padding: const EdgeInsets.only(right: 4, bottom: 4), child: SvgPicture.asset('assets/icons/error_icon.svg')), + ), + }, + Container( constraints: BoxConstraints(maxWidth: size.width * 0.7), decoration: BoxDecoration( @@ -87,33 +113,19 @@ class ChatBubbleWidget extends StatelessWidget { throw Exception('Could not launch ${mediaData[index].toString()}'); } }, - leading: const Icon( - Icons.file_copy_outlined, - color: Colors.black, - ), - trailing: const Icon( - Icons.download_outlined, - color: Colors.black, - ), + leading: const Icon(Icons.file_copy_outlined, color: Colors.black), + trailing: const Icon(Icons.download_outlined, color: Colors.black), title: Text( mediaData[index].toString().replaceAll('chat_files/', ''), maxLines: 2, - style: const TextStyle( - color: Colors.black, - fontSize: 12, - overflow: TextOverflow.ellipsis, - ), + style: const TextStyle(color: Colors.black, fontSize: 12, overflow: TextOverflow.ellipsis), ), ); } return GestureDetector( onTap: () { context.pushTransparentRoute( - PhotoViewScreen( - imageUrls: mediaData, - index: index, - isLocal: mediaData[index] is File ? true : false, - ), + PhotoViewScreen(imageUrls: mediaData, index: index, isLocal: mediaData[index] is File ? true : false), ); }, child: Hero( @@ -121,11 +133,7 @@ class ChatBubbleWidget extends StatelessWidget { transitionOnUserGestures: true, child: ClipRRect( borderRadius: BorderRadius.circular(16), - child: mediaData[index] is File - ? Image.file(mediaData[index]) - : CachedNetworkImage( - imageUrl: '${mediaData[index]}', - ), + child: mediaData[index] is File ? Image.file(mediaData[index]) : CachedNetworkImage(imageUrl: '${mediaData[index]}'), ), ), ); @@ -137,11 +145,7 @@ class ChatBubbleWidget extends StatelessWidget { ), ), if (mediaData.isNotEmpty && text.isNotEmpty) const SizedBox(height: 8), - if (text.isNotEmpty) - Text( - text, - style: TextStyles.textFS13FW400.copyWith(color: appTheme.black, fontSize: 16), - ), + if (text.isNotEmpty) Text(text, style: TextStyles.textFS13FW400.copyWith(color: appTheme.black, fontSize: 16)), ], ), ), @@ -151,12 +155,7 @@ class ChatBubbleWidget extends StatelessWidget { padding: const EdgeInsets.only(left: 4, bottom: 4), child: Text( sendTime.format('HH:mm', 'ru'), - style: const TextStyle( - color: Color(0xFF7B8794), - height: 1, - fontSize: 10, - fontWeight: FontWeight.w500, - ), + style: const TextStyle(color: Color(0xFF7B8794), height: 1, fontSize: 10, fontWeight: FontWeight.w500), ), ), ], diff --git a/lib/features/chat/presentation/widgets/message_action_bottom_sheet.dart b/lib/features/chat/presentation/widgets/message_action_bottom_sheet.dart new file mode 100644 index 00000000..1f455764 --- /dev/null +++ b/lib/features/chat/presentation/widgets/message_action_bottom_sheet.dart @@ -0,0 +1,48 @@ +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/core/widgets/bottom_sheet_action_widget.dart'; +import 'package:eatvio_app/core/widgets/bottom_sheet_handler_bar.dart'; +import 'package:eatvio_app/core/widgets/custom_divider.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; + +void showMessageActionsBottomSheet(BuildContext context, {required void Function() resend, required void Function() delete}) { + 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: MessageActionsBottomSheet(resend: resend, delete: delete), + ), + ); +} + +class MessageActionsBottomSheet extends StatefulWidget { + const MessageActionsBottomSheet({super.key, required this.resend, required this.delete}); + final void Function() resend; + final void Function() delete; + + @override + State createState() => _MessageActionsBottomSheet(); +} + +class _MessageActionsBottomSheet extends State { + @override + Widget build(BuildContext context) { + final s = S.of(context); + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 7), + const BottomSheetHandlerBar(), + BottomSheetActionWidget(text: s.resend, iconPath: 'assets/icons/resend.svg', onTap: widget.resend), + const CustomDivider(), + BottomSheetActionWidget(text: s.delete, iconPath: 'assets/icons/trash_can.svg', onTap: widget.delete, color: appTheme.red), + ], + ), + ); + } +} diff --git a/lib/features/menu/presentation/pages/menu_screen.dart b/lib/features/menu/presentation/pages/menu_screen.dart index 6b8b47fe..90c866a8 100644 --- a/lib/features/menu/presentation/pages/menu_screen.dart +++ b/lib/features/menu/presentation/pages/menu_screen.dart @@ -1,5 +1,6 @@ import 'package:dio/dio.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'; import 'package:eatvio_app/core/utils/image_constant.dart'; import 'package:eatvio_app/core/locator_service.dart'; @@ -14,6 +15,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:go_router/go_router.dart'; +import 'package:package_info_plus/package_info_plus.dart'; class MenuScreen extends StatelessWidget { final ProfileModel profileModel; @@ -67,6 +69,8 @@ class MenuScreen extends StatelessWidget { // ), _divider(context), _tile(leading: ImageConstant.icnExit, title: 'Выход', onTap: _onTapLogout(context)), + const SizedBox(height: 10), + const AppInfo(), ], ), ), @@ -124,3 +128,36 @@ class MenuScreen extends StatelessWidget { }; } } + +class AppInfo extends StatefulWidget { + const AppInfo({super.key}); + + @override + State createState() => _AppInfoState(); +} + +class _AppInfoState extends State { + String version = ''; + String buildNumber = ''; + @override + void didChangeDependencies() { + PackageInfo.fromPlatform().then((PackageInfo packageInfo) { + setState(() { + version = packageInfo.version; + buildNumber = packageInfo.buildNumber; + }); + }); + super.didChangeDependencies(); + } + + @override + Widget build(BuildContext context) { + return Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsetsGeometry.only(left: 16), + child: Text('Версия приложения: $version ($buildNumber)', style: TextStyles.textXSSemibold.copyWith(color: appTheme.gray)), + ), + ); + } +} diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart index 360b4288..373b48eb 100644 --- a/lib/generated/intl/messages_en.dart +++ b/lib/generated/intl/messages_en.dart @@ -634,6 +634,7 @@ class MessageLookup extends MessageLookupByLibrary { "Снять с публикации", ), "reply": MessageLookupByLibrary.simpleMessage("Ответить"), + "resend": MessageLookupByLibrary.simpleMessage("Отправить повторно"), "save": MessageLookupByLibrary.simpleMessage("Сохранить"), "send": MessageLookupByLibrary.simpleMessage("Отправить"), "send_complain": MessageLookupByLibrary.simpleMessage("Отправить жалобу"), diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index 7df3cbcc..25d71d68 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -3459,6 +3459,16 @@ class S { args: [], ); } + + /// `Отправить повторно` + String get resend { + return Intl.message( + 'Отправить повторно', + name: 'resend', + desc: '', + args: [], + ); + } } class AppLocalizationDelegate extends LocalizationsDelegate { diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 452e2d5a..7424fe87 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -410,7 +410,7 @@ "youShureYouWantToDeleteAccount": "Вы уверены, что хотите удалить аккаунт?", "itCantBeRecovered": "Его нельзя будет восстановить", "delete": "Удалить", - "cannotDeleteAccount": "Не получилось удалить ваш аккаунт" - + "cannotDeleteAccount": "Не получилось удалить ваш аккаунт", + "resend": "Отправить повторно" } diff --git a/pubspec.yaml b/pubspec.yaml index 3895a06b..f61ae9c8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -77,6 +77,7 @@ dependencies: permission_handler: ^12.0.1 qr_code_scanner_plus: ^2.0.10+1 flutter_dotenv: ^5.1.0 + uuid: ^4.5.1 dev_dependencies: build_runner: ^2.6.0 From dca4b4c4715031d90818091efee17a6b87a756be Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Mon, 11 Aug 2025 14:53:43 +0300 Subject: [PATCH 095/134] fix(blackList+notifications): Issue-523, 191 --- .../chat/presentation/pages/chat_screen.dart | 24 +- .../presentation/pages/empty_chat_screen.dart | 114 +++++-- .../models/notification_model.dart | 311 ++++++++++++++++++ .../notifications_cubit.dart | 10 + .../widgets/notification_widget.dart | 128 +++++++ .../service/notifications_service.dart | 55 ++++ .../profile/models/profile_model.dart | 1 + .../presentation/pages/profile_screen.dart | 41 ++- .../services/profile_service_test.dart | 1 + 9 files changed, 650 insertions(+), 35 deletions(-) diff --git a/lib/features/chat/presentation/pages/chat_screen.dart b/lib/features/chat/presentation/pages/chat_screen.dart index c017f047..706fca26 100644 --- a/lib/features/chat/presentation/pages/chat_screen.dart +++ b/lib/features/chat/presentation/pages/chat_screen.dart @@ -13,6 +13,8 @@ import 'package:eatvio_app/features/chat/presentation/pages/photo_view_screen.da import 'package:eatvio_app/features/chat/presentation/widgets/about_order_button.dart'; import 'package:eatvio_app/features/chat/presentation/widgets/chat_bubble_widget.dart'; import 'package:eatvio_app/core/locator_service.dart'; +import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_cubit.dart'; +import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_state.dart'; import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/features/chat/presentation/widgets/notification_banner.dart'; @@ -465,7 +467,27 @@ class _ChatScreenState extends State { const SizedBox(width: 8), InkWell( customBorder: const CircleBorder(), - onTap: () { + onTap: () async { + // Проверяем, заблокирован ли пользователь + final blockedUsersCubit = sl(); + await blockedUsersCubit.getBlockedUsers(); + final blockedState = blockedUsersCubit.state; + + if (blockedState is BlockedUsersStateLoaded) { + final isBlocked = blockedState.profilesList.any((profile) => profile.slug == state.chatRoom.users.receiver.slug); + if (isBlocked) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Вы не можете отправить сообщение заблокированному пользователю'), + backgroundColor: Colors.red, + ), + ); + } + return; + } + } + if (textEditingController.text.isNotEmpty || pickedFiles.isNotEmpty) { chatRoomCubit.sendMessage( data: textEditingController.text.isEmpty ? null : textEditingController.text.trim(), diff --git a/lib/features/chat/presentation/pages/empty_chat_screen.dart b/lib/features/chat/presentation/pages/empty_chat_screen.dart index 7b543f86..d6a87c46 100644 --- a/lib/features/chat/presentation/pages/empty_chat_screen.dart +++ b/lib/features/chat/presentation/pages/empty_chat_screen.dart @@ -12,6 +12,8 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:image_picker/image_picker.dart'; +import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_cubit.dart'; +import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_state.dart'; class EmptyChatScreen extends StatefulWidget { final Author author; @@ -133,21 +135,45 @@ class _EmptyChatScreenState extends State { InkWell( customBorder: const CircleBorder(), onTap: () async { + // Проверяем, заблокирован ли пользователь + final blockedUsersCubit = sl(); + await blockedUsersCubit.getBlockedUsers(); + final blockedState = blockedUsersCubit.state; + + if (blockedState is BlockedUsersStateLoaded) { + final isBlocked = blockedState.profilesList.any((profile) => profile.slug == widget.author.authorSlug); + if (isBlocked) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Вы не можете отправить сообщение заблокированному пользователю'), + backgroundColor: Colors.red, + ), + ); + } + return; + } + } + if (textEditingController.text.isNotEmpty) { await sl().sendMessage( data: textEditingController.text.trim(), slug: widget.author.authorSlug, ); - setState(() { - textEditingController.clear(); - }); + if (mounted) { + setState(() { + textEditingController.clear(); + }); + } final chatIdOrError = await sl().getChatByUserSlug(widget.author.authorSlug); - AppRouter.navigatorKey.currentContext?.pushReplacementNamed( - 'chatScreen', - extra: { - 'uuid': chatIdOrError, - }, - ); + if (context.mounted) { + AppRouter.navigatorKey.currentContext?.pushReplacementNamed( + 'chatScreen', + extra: { + 'uuid': chatIdOrError, + }, + ); + } } }, child: Ink( @@ -175,20 +201,44 @@ class _EmptyChatScreenState extends State { CupertinoActionSheetAction( onPressed: () async { Navigator.of(context).pop(); + + // Проверяем, заблокирован ли пользователь + final blockedUsersCubit = sl(); + await blockedUsersCubit.getBlockedUsers(); + final blockedState = blockedUsersCubit.state; + + if (blockedState is BlockedUsersStateLoaded) { + final isBlocked = blockedState.profilesList.any((profile) => profile.slug == widget.author.authorSlug); + if (isBlocked) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Вы не можете отправить сообщение заблокированному пользователю'), + backgroundColor: Colors.red, + ), + ); + } + return; + } + } + final picker = ImagePicker(); - XFile? image = await picker.pickImage(source: ImageSource.camera); - if (image != null) { + List? images = await picker.pickMultiImage(); + if (images.isNotEmpty) { + List files = images.map((image) => File(image.path)).toList(); await sl().sendMessage( - files: [File(image.path)], + files: files, slug: widget.author.authorSlug, ); final chatIdOrError = await sl().getChatByUserSlug(widget.author.authorSlug); - AppRouter.navigatorKey.currentContext?.pushReplacementNamed( - 'chatScreen', - extra: { - 'uuid': chatIdOrError, - }, - ); + if (mounted) { + AppRouter.navigatorKey.currentContext?.pushReplacementNamed( + 'chatScreen', + extra: { + 'uuid': chatIdOrError, + }, + ); + } } }, child: const Row( @@ -211,12 +261,14 @@ class _EmptyChatScreenState extends State { slug: widget.author.authorSlug, ); final chatIdOrError = await sl().getChatByUserSlug(widget.author.authorSlug); - AppRouter.navigatorKey.currentContext?.pushReplacementNamed( - 'chatScreen', - extra: { - 'uuid': chatIdOrError, - }, - ); + if (mounted) { + AppRouter.navigatorKey.currentContext?.pushReplacementNamed( + 'chatScreen', + extra: { + 'uuid': chatIdOrError, + }, + ); + } } }, child: const Row( @@ -261,12 +313,14 @@ class _EmptyChatScreenState extends State { slug: widget.author.authorSlug, ); final chatIdOrError = await sl().getChatByUserSlug(widget.author.authorSlug); - AppRouter.navigatorKey.currentContext?.pushReplacementNamed( - 'chatScreen', - extra: { - 'uuid': chatIdOrError, - }, - ); + if (mounted) { + AppRouter.navigatorKey.currentContext?.pushReplacementNamed( + 'chatScreen', + extra: { + 'uuid': chatIdOrError, + }, + ); + } } }, child: const Row( diff --git a/lib/features/notifications/models/notification_model.dart b/lib/features/notifications/models/notification_model.dart index 96626d5d..31d74e10 100644 --- a/lib/features/notifications/models/notification_model.dart +++ b/lib/features/notifications/models/notification_model.dart @@ -538,6 +538,297 @@ class EntityCommented implements BaseNotificationModel { } } +class PlanSubscriptionExpiredNotification implements BaseNotificationModel { + @override + final NotificationType type; + @override + final bool isRead; + final String planName; + final String customerName; + @override + final DateTime createdAt; + + PlanSubscriptionExpiredNotification({ + required this.type, + required this.planName, + required this.customerName, + required this.isRead, + required this.createdAt, + }); + + factory PlanSubscriptionExpiredNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { + return PlanSubscriptionExpiredNotification( + type: type, + planName: json['plan_name'], + customerName: json['user_name'], + isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), + ); + } +} + +class PlanSubscriptionRenewedNotification implements BaseNotificationModel { + @override + final NotificationType type; + @override + final bool isRead; + final String planName; + final String customerName; + final String renewalDate; + @override + final DateTime createdAt; + + PlanSubscriptionRenewedNotification({ + required this.type, + required this.planName, + required this.customerName, + required this.renewalDate, + required this.isRead, + required this.createdAt, + }); + + factory PlanSubscriptionRenewedNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { + return PlanSubscriptionRenewedNotification( + type: type, + planName: json['plan_name'], + customerName: json['user_name'], + renewalDate: json['renewal_date'], + isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), + ); + } +} + +class ConsultantAssignedNotification implements BaseNotificationModel { + @override + final NotificationType type; + @override + final bool isRead; + final String consultantName; + final String planName; + final int planId; + @override + final DateTime createdAt; + + ConsultantAssignedNotification({ + required this.type, + required this.consultantName, + required this.planName, + required this.planId, + required this.isRead, + required this.createdAt, + }); + + factory ConsultantAssignedNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { + return ConsultantAssignedNotification( + type: type, + consultantName: json['consultant_name'], + planName: json['plan_name'], + planId: json['plan_id'], + isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), + ); + } +} + +class ConsultantUnassignedNotification implements BaseNotificationModel { + @override + final NotificationType type; + @override + final bool isRead; + final String consultantName; + final String planName; + final int planId; + @override + final DateTime createdAt; + + ConsultantUnassignedNotification({ + required this.type, + required this.consultantName, + required this.planName, + required this.planId, + required this.isRead, + required this.createdAt, + }); + + factory ConsultantUnassignedNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { + return ConsultantUnassignedNotification( + type: type, + consultantName: json['consultant_name'], + planName: json['plan_name'], + planId: json['plan_id'], + isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), + ); + } +} + +class PaymentFailedNotification implements BaseNotificationModel { + @override + final NotificationType type; + @override + final bool isRead; + final String planName; + final String errorMessage; + final String paymentMethod; + @override + final DateTime createdAt; + + PaymentFailedNotification({ + required this.type, + required this.planName, + required this.errorMessage, + required this.paymentMethod, + required this.isRead, + required this.createdAt, + }); + + factory PaymentFailedNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { + return PaymentFailedNotification( + type: type, + planName: json['plan_name'], + errorMessage: json['error_message'], + paymentMethod: json['payment_method'], + isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), + ); + } +} + +class PaymentSuccessfulNotification implements BaseNotificationModel { + @override + final NotificationType type; + @override + final bool isRead; + final String planName; + final double amount; + final String currency; + final String transactionId; + @override + final DateTime createdAt; + + PaymentSuccessfulNotification({ + required this.type, + required this.planName, + required this.amount, + required this.currency, + required this.transactionId, + required this.isRead, + required this.createdAt, + }); + + factory PaymentSuccessfulNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { + return PaymentSuccessfulNotification( + type: type, + planName: json['plan_name'], + amount: (json['amount'] as num).toDouble(), + currency: json['currency'], + transactionId: json['transaction_id'], + isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), + ); + } +} + +class GoalAchievedNotification implements BaseNotificationModel { + @override + final NotificationType type; + @override + final bool isRead; + final String goalName; + final String goalType; + final double achievedValue; + final double targetValue; + @override + final DateTime createdAt; + + GoalAchievedNotification({ + required this.type, + required this.goalName, + required this.goalType, + required this.achievedValue, + required this.targetValue, + required this.isRead, + required this.createdAt, + }); + + factory GoalAchievedNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { + return GoalAchievedNotification( + type: type, + goalName: json['goal_name'], + goalType: json['goal_type'], + achievedValue: (json['achieved_value'] as num).toDouble(), + targetValue: (json['target_value'] as num).toDouble(), + isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), + ); + } +} + +class WorkoutReminderNotification implements BaseNotificationModel { + @override + final NotificationType type; + @override + final bool isRead; + final String workoutName; + final String workoutType; + final int duration; + @override + final DateTime createdAt; + + WorkoutReminderNotification({ + required this.type, + required this.workoutName, + required this.workoutType, + required this.duration, + required this.isRead, + required this.createdAt, + }); + + factory WorkoutReminderNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { + return WorkoutReminderNotification( + type: type, + workoutName: json['workout_name'], + workoutType: json['workout_type'], + duration: json['duration'], + isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), + ); + } +} + +class MealReminderNotification implements BaseNotificationModel { + @override + final NotificationType type; + @override + final bool isRead; + final String mealName; + final String mealType; + final int calories; + @override + final DateTime createdAt; + + MealReminderNotification({ + required this.type, + required this.mealName, + required this.mealType, + required this.calories, + required this.isRead, + required this.createdAt, + }); + + factory MealReminderNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { + return MealReminderNotification( + type: type, + mealName: json['meal_name'], + mealType: json['meal_type'], + calories: json['calories'], + isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), + ); + } +} + class UnknownNotification implements BaseNotificationModel { @override final NotificationType type; @@ -570,6 +861,16 @@ enum NotificationType { commentReplied, scoreAdded, entityCommented, + // Дополнительные типы уведомлений + planSubscriptionExpired, + planSubscriptionRenewed, + consultantAssigned, + consultantUnassigned, + paymentFailed, + paymentSuccessful, + goalAchieved, + workoutReminder, + mealReminder, unknown; static NotificationType getTypeFromString(String type) => switch (type) { @@ -593,6 +894,16 @@ enum NotificationType { 'App\\Notifications\\Comment\\Replied' => NotificationType.commentReplied, 'App\\Notifications\\Score\\ScoreAdded' => NotificationType.scoreAdded, 'App\\Notifications\\Comment\\EntityCommented' => NotificationType.entityCommented, + // Дополнительные маппинги + 'App\\Notifications\\Plan\\PlanSubscriptionExpired' => NotificationType.planSubscriptionExpired, + 'App\\Notifications\\Plan\\PlanSubscriptionRenewed' => NotificationType.planSubscriptionRenewed, + 'App\\Notifications\\Plan\\ConsultantAssigned' => NotificationType.consultantAssigned, + 'App\\Notifications\\Plan\\ConsultantUnassigned' => NotificationType.consultantUnassigned, + 'App\\Notifications\\Payment\\PaymentFailed' => NotificationType.paymentFailed, + 'App\\Notifications\\Payment\\PaymentSuccessful' => NotificationType.paymentSuccessful, + 'App\\Notifications\\Goal\\GoalAchieved' => NotificationType.goalAchieved, + 'App\\Notifications\\Workout\\WorkoutReminder' => NotificationType.workoutReminder, + 'App\\Notifications\\Meal\\MealReminder' => NotificationType.mealReminder, _ => NotificationType.unknown, }; } diff --git a/lib/features/notifications/presentation/notifications_cubit/notifications_cubit.dart b/lib/features/notifications/presentation/notifications_cubit/notifications_cubit.dart index d4e6e17d..df921b4a 100644 --- a/lib/features/notifications/presentation/notifications_cubit/notifications_cubit.dart +++ b/lib/features/notifications/presentation/notifications_cubit/notifications_cubit.dart @@ -113,6 +113,16 @@ class NotificationsCubit extends Cubit { NotificationType.commentReplied => CommentReplied.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt), NotificationType.scoreAdded => ScoreAdded.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt), NotificationType.entityCommented => EntityCommented.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt), + // Новые типы уведомлений + NotificationType.planSubscriptionExpired => PlanSubscriptionExpiredNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt), + NotificationType.planSubscriptionRenewed => PlanSubscriptionRenewedNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt), + NotificationType.consultantAssigned => ConsultantAssignedNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt), + NotificationType.consultantUnassigned => ConsultantUnassignedNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt), + NotificationType.paymentFailed => PaymentFailedNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt), + NotificationType.paymentSuccessful => PaymentSuccessfulNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt), + NotificationType.goalAchieved => GoalAchievedNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt), + NotificationType.workoutReminder => WorkoutReminderNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt), + NotificationType.mealReminder => MealReminderNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt), NotificationType.unknown => UnknownNotification(type: type), }; } catch (e) { diff --git a/lib/features/notifications/presentation/widgets/notification_widget.dart b/lib/features/notifications/presentation/widgets/notification_widget.dart index 18c02a13..eff047da 100644 --- a/lib/features/notifications/presentation/widgets/notification_widget.dart +++ b/lib/features/notifications/presentation/widgets/notification_widget.dart @@ -47,6 +47,16 @@ class NotificationWidget extends StatelessWidget { CommentReplied() => _CommentRepliedWidget(notification: notification as CommentReplied), ScoreAdded() => _ScoreAddedWidget(notification: notification as ScoreAdded), EntityCommented() => _EntityCommentedWidget(notification: notification as EntityCommented), + // Новые типы уведомлений + PlanSubscriptionExpiredNotification() => _PlanSubscriptionExpiredWidget(notification: notification as PlanSubscriptionExpiredNotification), + PlanSubscriptionRenewedNotification() => _PlanSubscriptionRenewedWidget(notification: notification as PlanSubscriptionRenewedNotification), + ConsultantAssignedNotification() => _ConsultantAssignedWidget(notification: notification as ConsultantAssignedNotification), + ConsultantUnassignedNotification() => _ConsultantUnassignedWidget(notification: notification as ConsultantUnassignedNotification), + PaymentFailedNotification() => _PaymentFailedWidget(notification: notification as PaymentFailedNotification), + PaymentSuccessfulNotification() => _PaymentSuccessfulWidget(notification: notification as PaymentSuccessfulNotification), + GoalAchievedNotification() => _GoalAchievedWidget(notification: notification as GoalAchievedNotification), + WorkoutReminderNotification() => _WorkoutReminderWidget(notification: notification as WorkoutReminderNotification), + MealReminderNotification() => _MealReminderWidget(notification: notification as MealReminderNotification), UnknownNotification() => _UnknownNotificationWidget(notification: notification as UnknownNotification), }; } @@ -443,3 +453,121 @@ class _UnknownNotificationWidget extends StatelessWidget { ); } } + +// Новые виджеты для дополнительных типов уведомлений +class _PlanSubscriptionExpiredWidget extends StatelessWidget { + const _PlanSubscriptionExpiredWidget({required this.notification}); + final PlanSubscriptionExpiredNotification notification; + @override + Widget build(BuildContext context) { + return _BaseNotificationWidget( + isRead: notification.isRead, + notificationsText: 'Подписка на план "${notification.planName}" истекла', + createdAt: notification.createdAt, + ); + } +} + +class _PlanSubscriptionRenewedWidget extends StatelessWidget { + const _PlanSubscriptionRenewedWidget({required this.notification}); + final PlanSubscriptionRenewedNotification notification; + @override + Widget build(BuildContext context) { + return _BaseNotificationWidget( + isRead: notification.isRead, + notificationsText: 'Подписка на план "${notification.planName}" продлена до ${notification.renewalDate}', + createdAt: notification.createdAt, + ); + } +} + +class _ConsultantAssignedWidget extends StatelessWidget { + const _ConsultantAssignedWidget({required this.notification}); + final ConsultantAssignedNotification notification; + @override + Widget build(BuildContext context) { + return _BaseNotificationWidget( + isRead: notification.isRead, + notificationsText: 'К плану "${notification.planName}" назначен консультант ${notification.consultantName}', + createdAt: notification.createdAt, + ); + } +} + +class _ConsultantUnassignedWidget extends StatelessWidget { + const _ConsultantUnassignedWidget({required this.notification}); + final ConsultantUnassignedNotification notification; + @override + Widget build(BuildContext context) { + return _BaseNotificationWidget( + isRead: notification.isRead, + notificationsText: 'Консультант ${notification.consultantName} отстранен от плана "${notification.planName}"', + createdAt: notification.createdAt, + ); + } +} + +class _PaymentFailedWidget extends StatelessWidget { + const _PaymentFailedWidget({required this.notification}); + final PaymentFailedNotification notification; + @override + Widget build(BuildContext context) { + return _BaseNotificationWidget( + isRead: notification.isRead, + notificationsText: 'Ошибка оплаты плана "${notification.planName}": ${notification.errorMessage}', + createdAt: notification.createdAt, + ); + } +} + +class _PaymentSuccessfulWidget extends StatelessWidget { + const _PaymentSuccessfulWidget({required this.notification}); + final PaymentSuccessfulNotification notification; + @override + Widget build(BuildContext context) { + return _BaseNotificationWidget( + isRead: notification.isRead, + notificationsText: 'Успешная оплата плана "${notification.planName}" на сумму ${notification.amount} ${notification.currency}', + createdAt: notification.createdAt, + ); + } +} + +class _GoalAchievedWidget extends StatelessWidget { + const _GoalAchievedWidget({required this.notification}); + final GoalAchievedNotification notification; + @override + Widget build(BuildContext context) { + return _BaseNotificationWidget( + isRead: notification.isRead, + notificationsText: 'Достигнута цель "${notification.goalName}": ${notification.achievedValue}/${notification.targetValue}', + createdAt: notification.createdAt, + ); + } +} + +class _WorkoutReminderWidget extends StatelessWidget { + const _WorkoutReminderWidget({required this.notification}); + final WorkoutReminderNotification notification; + @override + Widget build(BuildContext context) { + return _BaseNotificationWidget( + isRead: notification.isRead, + notificationsText: 'Напоминание о тренировке "${notification.workoutName}" (${notification.duration} мин)', + createdAt: notification.createdAt, + ); + } +} + +class _MealReminderWidget extends StatelessWidget { + const _MealReminderWidget({required this.notification}); + final MealReminderNotification notification; + @override + Widget build(BuildContext context) { + return _BaseNotificationWidget( + isRead: notification.isRead, + notificationsText: 'Напоминание о приеме пищи "${notification.mealName}" (${notification.calories} ккал)', + createdAt: notification.createdAt, + ); + } +} diff --git a/lib/features/notifications/service/notifications_service.dart b/lib/features/notifications/service/notifications_service.dart index 61f545ab..7fdcb4fe 100644 --- a/lib/features/notifications/service/notifications_service.dart +++ b/lib/features/notifications/service/notifications_service.dart @@ -119,6 +119,61 @@ class NotificationsService { NotificationType.commentReplied => CommentReplied.fromJson(notificationJson['data'], type, isRead: readDate != null, createdAt: createdAt), NotificationType.scoreAdded => ScoreAdded.fromJson(notificationJson['data'], type, isRead: readDate != null, createdAt: createdAt), NotificationType.entityCommented => EntityCommented.fromJson(notificationJson['data'], type, isRead: readDate != null, createdAt: createdAt), + // Новые типы уведомлений + NotificationType.planSubscriptionExpired => PlanSubscriptionExpiredNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.planSubscriptionRenewed => PlanSubscriptionRenewedNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.consultantAssigned => ConsultantAssignedNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.consultantUnassigned => ConsultantUnassignedNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.paymentFailed => PaymentFailedNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.paymentSuccessful => PaymentSuccessfulNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.goalAchieved => GoalAchievedNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.workoutReminder => WorkoutReminderNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.mealReminder => MealReminderNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), NotificationType.unknown => UnknownNotification(type: type, createdAt: parseDate(createdAt)), }; } catch (e) { diff --git a/lib/features/profile/models/profile_model.dart b/lib/features/profile/models/profile_model.dart index f798021f..03fd57f3 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_user", fromJson: parseBool, defaultValue: false) required bool isBlockedByUser, @JsonKey(name: 'is_entity_subscribed_by_me') required bool isEntitySubscribedByMe, @JsonKey(name: "slug") required String slug, @JsonKey(name: "profile_photo_url") required String profilePhotoUrl, diff --git a/lib/features/profile/presentation/pages/profile_screen.dart b/lib/features/profile/presentation/pages/profile_screen.dart index f3329074..990beed1 100644 --- a/lib/features/profile/presentation/pages/profile_screen.dart +++ b/lib/features/profile/presentation/pages/profile_screen.dart @@ -93,6 +93,39 @@ 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( @@ -174,10 +207,10 @@ 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) _socialLinks(state), - if (state.profile.roles.contains('consultant') && state.consultantInfo != null) _education(state), - if (state.profile.roles.contains('consultant') && state.consultantInfo != null) _diplomasAndCertificates(state), - if (state.profile.roles.contains('consultant') && state.consultantInfo != null && !hidePlans) _activePlans(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), _blogPosts(state), ], ), diff --git a/test/features/profile/services/profile_service_test.dart b/test/features/profile/services/profile_service_test.dart index 106da096..8c666344 100644 --- a/test/features/profile/services/profile_service_test.dart +++ b/test/features/profile/services/profile_service_test.dart @@ -17,6 +17,7 @@ void main() { (_) async => await Future.value( const ProfileModel( id: 0, + isBlockedByUser: false, name: 'Alexander Avanesyan', email: 'mso7nueh@mail.ru', phoneConfirmationState: '', From ef4d91a5d81e9fef4217641578017e95c264a1bd Mon Sep 17 00:00:00 2001 From: Username1548 Date: Mon, 11 Aug 2025 20:46:09 +0700 Subject: [PATCH 096/134] removed public offert from ui on ios --- .../auth/presentation/pages/auth_screen.dart | 106 +++----- .../presentation/pages/last_step_screen.dart | 46 +--- .../presentation/pages/settings_screen.dart | 248 +++--------------- .../presentation/pages/support_screen.dart | 27 +- 4 files changed, 95 insertions(+), 332 deletions(-) diff --git a/lib/features/auth/presentation/pages/auth_screen.dart b/lib/features/auth/presentation/pages/auth_screen.dart index f5fd7a34..20b0e26c 100644 --- a/lib/features/auth/presentation/pages/auth_screen.dart +++ b/lib/features/auth/presentation/pages/auth_screen.dart @@ -73,10 +73,7 @@ class _AuthScreenState extends State { rethrow; } } - AppRouter.navigatorKey.currentContext?.go( - '/feed-screen', - extra: {'needToInitializeWebSocket': false}, - ); + AppRouter.navigatorKey.currentContext?.go('/feed-screen', extra: {'needToInitializeWebSocket': false}); } @override @@ -102,11 +99,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: isFocused ? 64 : MediaQuery.of(context).size.width - 64, fit: BoxFit.contain), ), ), body: SafeArea( @@ -137,10 +130,7 @@ class _AuthScreenState extends State { hidePassword = !hidePassword; }); }, - icon: Icon( - hidePassword ? Icons.visibility_outlined : Icons.visibility_off_outlined, - color: Colors.grey, - ), + icon: Icon(hidePassword ? Icons.visibility_outlined : Icons.visibility_off_outlined, color: Colors.grey), ), ), const SizedBox(height: 16), @@ -149,11 +139,7 @@ class _AuthScreenState extends State { onTap: () => GoRouter.of(context).goNamed('forgetPasswordScreen', extra: false), child: Text( 'Забыли пароль?', - style: TextStyle( - color: appTheme.blueA400, - decorationColor: appTheme.blueA400, - decoration: TextDecoration.underline, - ), + style: TextStyle(color: appTheme.blueA400, decorationColor: appTheme.blueA400, decoration: TextDecoration.underline), ), ), ), @@ -166,11 +152,7 @@ class _AuthScreenState extends State { onTap: () => GoRouter.of(context).goNamed('registrationScreen'), child: Text( 'Зарегистрироваться', - style: TextStyle( - color: appTheme.blueA400, - decorationColor: appTheme.blueA400, - decoration: TextDecoration.underline, - ), + style: TextStyle(color: appTheme.blueA400, decorationColor: appTheme.blueA400, decoration: TextDecoration.underline), ), ), ], @@ -181,23 +163,22 @@ class _AuthScreenState extends State { textAlign: TextAlign.center, text: TextSpan( text: 'Нажимая "Войти" или "Зарегистрироваться", Вы соглашаетесь \n', - style: const TextStyle( - fontSize: 12, - color: Colors.black, - ), + style: const TextStyle(fontSize: 12, color: Colors.black), children: [ const TextSpan(text: 'c '), - TextSpan( - text: ' Публичной офертой', - style: TextStyle( - fontSize: 12, - color: appTheme.blueA400, - decorationColor: appTheme.blueA400, - decoration: TextDecoration.underline, + if (!hidePlans) ...[ + TextSpan( + text: ' Публичной офертой', + style: TextStyle( + fontSize: 12, + color: appTheme.blueA400, + 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('$baseUrl/oferta_bs.pdf')), - ), - const TextSpan(text: ' и '), + const TextSpan(text: ' и '), + ], TextSpan( text: 'Политикой конфиденциальности\nи защитой персональных данных', style: TextStyle( @@ -223,17 +204,12 @@ class _AuthScreenState extends State { highlightElevation: 0, elevation: 0, focusElevation: 0, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), - ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), backgroundColor: state is AuthStateLoading ? Colors.black.withAlpha(50) : appTheme.blueA400, onPressed: state is AuthStateLoading ? null : () async { - final isAuthenticated = await authCubit.getTokenByCredentials( - emailController.text, - passwordController.text, - ); + final isAuthenticated = await authCubit.getTokenByCredentials(emailController.text, passwordController.text); if (isAuthenticated) { if (context.mounted) { _reconnectWebSocket(context); @@ -242,10 +218,7 @@ class _AuthScreenState extends State { }, label: state is AuthStateLoading ? const CupertinoActivityIndicator() - : Text( - 'Войти', - style: theme.textTheme.titleSmall!.copyWith(color: Colors.white), - ), + : Text('Войти', style: theme.textTheme.titleSmall!.copyWith(color: Colors.white)), ), ), const SizedBox(width: 8), @@ -257,9 +230,7 @@ class _AuthScreenState extends State { highlightElevation: 0, elevation: 0, focusElevation: 0, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), backgroundColor: state is AuthStateLoading ? Colors.black.withAlpha(50) : appTheme.blueA400, onPressed: state is AuthStateLoading ? null @@ -274,14 +245,10 @@ class _AuthScreenState extends State { crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - const Text( - 'Где взять QR-код?\n', - style: TextStyle( - fontSize: 18, - ), - ), + const Text('Где взять QR-код?\n', style: TextStyle(fontSize: 18)), Text( - '1. Войдите на сайте $baseUrl в свой аккаунт\n\n2. Перейдите в настройки профиля\n\n3. В разделе "Доп информация" перейдите на "QR Login"'), + '1. Войдите на сайте $baseUrl в свой аккаунт\n\n2. Перейдите в настройки профиля\n\n3. В разделе "Доп информация" перейдите на "QR Login"', + ), const SizedBox(height: 12), Row( mainAxisAlignment: MainAxisAlignment.end, @@ -293,15 +260,10 @@ class _AuthScreenState extends State { }, child: Ink( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - ), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(8)), child: const Text( 'Отмена', - style: TextStyle( - color: Colors.black, - fontWeight: FontWeight.w400, - ), + style: TextStyle(color: Colors.black, fontWeight: FontWeight.w400), ), ), ), @@ -314,15 +276,10 @@ class _AuthScreenState extends State { }, child: Ink( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - ), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(8)), child: Text( 'Перейти', - style: TextStyle( - color: appTheme.blueA400, - fontWeight: FontWeight.w600, - ), + style: TextStyle(color: appTheme.blueA400, fontWeight: FontWeight.w600), ), ), ), @@ -337,10 +294,7 @@ class _AuthScreenState extends State { }, label: state is AuthStateLoading ? const CupertinoActivityIndicator() - : const Icon( - Icons.qr_code_scanner_rounded, - color: Colors.white, - ), + : const Icon(Icons.qr_code_scanner_rounded, color: Colors.white), ), ), ], 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 b5afd001..d44920cb 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 @@ -25,24 +25,16 @@ class _LastStepScreenState extends State { children: [ const Padding( padding: EdgeInsets.symmetric(horizontal: 16), - child: Text( - 'Последний шаг', - style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.w700, - height: 1.1, - ), - ), + child: Text('Последний шаг', style: TextStyle(fontSize: 24, fontWeight: FontWeight.w700, height: 1.1)), ), const SizedBox(height: 16), CheckboxListTile( value: checkboxState, - title: const Text( - 'Я ознакомлен и принимаю договор публичной офёрты', - style: TextStyle( - fontSize: 18, - height: 1, - ), + title: Text( + hidePlans + ? 'Я ознакомлен и принимаю политику конфиденциальности и защиту персональных данных' + : 'Я ознакомлен и принимаю договор публичной офёрты', + style: const TextStyle(fontSize: 18, height: 1), ), onChanged: (_) { setState(() { @@ -53,25 +45,14 @@ class _LastStepScreenState extends State { activeColor: appTheme.blueA400, contentPadding: const EdgeInsets.symmetric(horizontal: 16), ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 16, - ), - child: _divider(context), - ), + Padding(padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), child: _divider(context)), Padding( padding: const EdgeInsets.symmetric(horizontal: 4), child: TextButton( - onPressed: () async => await launchUrl(Uri.parse('$baseUrl/oferta_bs.pdf')), - child: const Text( - 'Договор офёрты', - style: TextStyle( - height: 1, - fontSize: 16, - color: Color(0xFF323F4B), - decoration: TextDecoration.underline, - ), + onPressed: () async => await launchUrl(Uri.parse(hidePlans ? '$baseUrl/docs/privacy.pdf' : '$baseUrl/oferta_bs.pdf')), + child: Text( + hidePlans ? 'Политика конфиденциальности и пользовательское соглашение' : 'Договор офёрты', + style: const TextStyle(height: 1, fontSize: 16, color: Color(0xFF323F4B), decoration: TextDecoration.underline), ), ), ), @@ -84,10 +65,7 @@ class _LastStepScreenState extends State { return Container( height: 0.4, width: MediaQuery.of(context).size.width - 32, - decoration: BoxDecoration( - color: Colors.black26, - borderRadius: BorderRadius.circular(16), - ), + decoration: BoxDecoration(color: Colors.black26, borderRadius: BorderRadius.circular(16)), ); } } diff --git a/lib/features/settings/presentation/pages/settings_screen.dart b/lib/features/settings/presentation/pages/settings_screen.dart index af99f901..0d39d303 100644 --- a/lib/features/settings/presentation/pages/settings_screen.dart +++ b/lib/features/settings/presentation/pages/settings_screen.dart @@ -51,9 +51,7 @@ class _SettingsScreenState extends State { builder: (BuildContext context, SettingsState state) { if (state is SettingsStateLoaded) { return Scaffold( - appBar: AppBar( - title: const Text('Настройки'), - ), + appBar: AppBar(title: const Text('Настройки')), body: SafeArea( child: SingleChildScrollView( padding: const EdgeInsets.symmetric(vertical: 16), @@ -64,8 +62,7 @@ class _SettingsScreenState extends State { const SizedBox(height: 12), _notifications(state), const SizedBox(height: 12), - _aboutEatvio(state), - const SizedBox(height: 12), + if (!hidePlans) ...[_aboutEatvio(state), const SizedBox(height: 12)], _becomeConsultantButton(state), _deleteAccountButton(state, settingsCubit), ], @@ -76,16 +73,12 @@ class _SettingsScreenState extends State { } if (state is SettingsStateError) { return Scaffold( - appBar: AppBar( - title: const Text('Настройки'), - ), + appBar: AppBar(title: const Text('Настройки')), body: DataErrorWidget(onReload: () => settingsCubit.init()), ); } return Scaffold( - appBar: AppBar( - title: const Text('Настройки'), - ), + appBar: AppBar(title: const Text('Настройки')), body: const Loader(), ); }, @@ -101,22 +94,13 @@ class _SettingsScreenState extends State { padding: EdgeInsets.symmetric(horizontal: 16), child: Text( 'Дополнительаная информация', - style: TextStyle( - fontSize: 16, - height: 1, - color: Colors.grey, - fontFamily: 'Mulish', - ), + style: TextStyle(fontSize: 16, height: 1, color: Colors.grey, fontFamily: 'Mulish'), ), ), _tile( onTap: () => GoRouter.of(context).pushNamed( 'generalInfoScreen', - extra: SettingsEntity( - profileSettingsModel: state.profileSettings, - profileDetailsModel: state.profileDetails, - settingsCubit: settingsCubit, - ), + extra: SettingsEntity(profileSettingsModel: state.profileSettings, profileDetailsModel: state.profileDetails, settingsCubit: settingsCubit), ), title: 'Общая информация', ), @@ -124,11 +108,7 @@ class _SettingsScreenState extends State { _tile( onTap: () => GoRouter.of(context).pushNamed( 'physicalFormScreen', - extra: SettingsEntity( - profileSettingsModel: state.profileSettings, - profileDetailsModel: state.profileDetails, - settingsCubit: settingsCubit, - ), + extra: SettingsEntity(profileSettingsModel: state.profileSettings, profileDetailsModel: state.profileDetails, settingsCubit: settingsCubit), ), title: 'Физическая форма', ), @@ -136,11 +116,7 @@ class _SettingsScreenState extends State { _tile( onTap: () => GoRouter.of(context).pushNamed( 'pressureScreen', - extra: SettingsEntity( - profileSettingsModel: state.profileSettings, - profileDetailsModel: state.profileDetails, - settingsCubit: settingsCubit, - ), + extra: SettingsEntity(profileSettingsModel: state.profileSettings, profileDetailsModel: state.profileDetails, settingsCubit: settingsCubit), ), title: 'Давление', ), @@ -148,11 +124,7 @@ class _SettingsScreenState extends State { _tile( onTap: () => GoRouter.of(context).pushNamed( 'otherInfoScreen', - extra: SettingsEntity( - profileSettingsModel: state.profileSettings, - profileDetailsModel: state.profileDetails, - settingsCubit: settingsCubit, - ), + extra: SettingsEntity(profileSettingsModel: state.profileSettings, profileDetailsModel: state.profileDetails, settingsCubit: settingsCubit), ), title: 'Прочая информация', ), @@ -160,31 +132,17 @@ class _SettingsScreenState extends State { _tile( onTap: () => GoRouter.of(context).pushNamed( 'physicalActivityScreen', - extra: SettingsEntity( - profileSettingsModel: state.profileSettings, - profileDetailsModel: state.profileDetails, - settingsCubit: settingsCubit, - ), + extra: SettingsEntity(profileSettingsModel: state.profileSettings, profileDetailsModel: state.profileDetails, settingsCubit: settingsCubit), ), title: 'Физическая нагрузка', ), _divider(context), - _tile( - onTap: () => GoRouter.of(context).pushNamed( - 'forgetPasswordScreen', - extra: true, - ), - title: 'Сменить пароль', - ), + _tile(onTap: () => GoRouter.of(context).pushNamed('forgetPasswordScreen', extra: true), title: 'Сменить пароль'), _divider(context), _tile( onTap: () => GoRouter.of(context).pushNamed( 'changeEmailScreen', - extra: SettingsEntity( - profileSettingsModel: state.profileSettings, - profileDetailsModel: state.profileDetails, - settingsCubit: settingsCubit, - ), + extra: SettingsEntity(profileSettingsModel: state.profileSettings, profileDetailsModel: state.profileDetails, settingsCubit: settingsCubit), ), title: 'Сменить Email', ), @@ -198,14 +156,7 @@ class _SettingsScreenState extends State { 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), @@ -213,11 +164,7 @@ class _SettingsScreenState extends State { setState(() { updatesNotificationsSwitchState = !updatesNotificationsSwitchState; }); - settingsCubit.updateProfileSettings( - profileSettingsModel: state.profileSettings.copyWith( - newUpdatesNotifications: updatesNotificationsSwitchState, - ), - ); + settingsCubit.updateProfileSettings(profileSettingsModel: state.profileSettings.copyWith(newUpdatesNotifications: updatesNotificationsSwitchState)); }, title: 'Уведомление о новых обновлениях', trailing: CupertinoSwitch( @@ -228,9 +175,7 @@ class _SettingsScreenState extends State { updatesNotificationsSwitchState = newValue; }); settingsCubit.updateProfileSettings( - profileSettingsModel: state.profileSettings.copyWith( - newUpdatesNotifications: updatesNotificationsSwitchState, - ), + profileSettingsModel: state.profileSettings.copyWith(newUpdatesNotifications: updatesNotificationsSwitchState), ); }, ), @@ -243,9 +188,7 @@ class _SettingsScreenState extends State { fillDiaryNotificationsSwitchState = !fillDiaryNotificationsSwitchState; }); settingsCubit.updateProfileSettings( - profileSettingsModel: state.profileSettings.copyWith( - diaryReminderNotifications: fillDiaryNotificationsSwitchState, - ), + profileSettingsModel: state.profileSettings.copyWith(diaryReminderNotifications: fillDiaryNotificationsSwitchState), ); }, title: 'Напоминание о заполнении дневника', @@ -257,9 +200,7 @@ class _SettingsScreenState extends State { fillDiaryNotificationsSwitchState = newValue; }); settingsCubit.updateProfileSettings( - profileSettingsModel: state.profileSettings.copyWith( - diaryReminderNotifications: fillDiaryNotificationsSwitchState, - ), + profileSettingsModel: state.profileSettings.copyWith(diaryReminderNotifications: fillDiaryNotificationsSwitchState), ); }, ), @@ -271,11 +212,7 @@ class _SettingsScreenState extends State { setState(() { moodSwitchState = !moodSwitchState; }); - settingsCubit.updateProfileSettings( - profileSettingsModel: state.profileSettings.copyWith( - isMoodFillable: moodSwitchState, - ), - ); + settingsCubit.updateProfileSettings(profileSettingsModel: state.profileSettings.copyWith(isMoodFillable: moodSwitchState)); }, title: 'Указывать “настроение” в заполнение дневника', trailing: CupertinoSwitch( @@ -285,11 +222,7 @@ class _SettingsScreenState extends State { setState(() { moodSwitchState = newValue; }); - settingsCubit.updateProfileSettings( - profileSettingsModel: state.profileSettings.copyWith( - isMoodFillable: moodSwitchState, - ), - ); + settingsCubit.updateProfileSettings(profileSettingsModel: state.profileSettings.copyWith(isMoodFillable: moodSwitchState)); }, ), ), @@ -300,11 +233,7 @@ class _SettingsScreenState extends State { setState(() { bedTimeSwitchState = !bedTimeSwitchState; }); - settingsCubit.updateProfileSettings( - profileSettingsModel: state.profileSettings.copyWith( - isBedtimeFillable: bedTimeSwitchState, - ), - ); + settingsCubit.updateProfileSettings(profileSettingsModel: state.profileSettings.copyWith(isBedtimeFillable: bedTimeSwitchState)); }, title: 'Указывать “отход ко сну” в заполнение дневника', trailing: CupertinoSwitch( @@ -314,11 +243,7 @@ class _SettingsScreenState extends State { setState(() { bedTimeSwitchState = newValue; }); - settingsCubit.updateProfileSettings( - profileSettingsModel: state.profileSettings.copyWith( - isBedtimeFillable: bedTimeSwitchState, - ), - ); + settingsCubit.updateProfileSettings(profileSettingsModel: state.profileSettings.copyWith(isBedtimeFillable: bedTimeSwitchState)); }, ), ), @@ -332,24 +257,11 @@ class _SettingsScreenState extends State { children: [ const Padding( padding: EdgeInsets.symmetric(horizontal: 16), - child: Text( - 'О Eatvio', - style: TextStyle( - fontSize: 16, - height: 1, - color: Colors.grey, - ), - ), - ), - _tile( - title: 'О компании', - onTap: () async => await launchUrl(Uri.parse('$baseUrl/ru/company-details')), + child: Text('О Eatvio', style: TextStyle(fontSize: 16, height: 1, color: Colors.grey)), ), + _tile(title: 'О компании', onTap: () async => await launchUrl(Uri.parse('$baseUrl/ru/company-details'))), _divider(context), - _tile( - title: 'Пользовательское соглашение', - onTap: () async => await launchUrl(Uri.parse('$baseUrl/oferta_bs.pdf')), - ), + _tile(title: 'Пользовательское соглашение', onTap: () async => await launchUrl(Uri.parse('$baseUrl/oferta_bs.pdf'))), ], ); } @@ -364,19 +276,9 @@ class _SettingsScreenState extends State { child: Ink( height: 52, width: MediaQuery.of(context).size.width - 16, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - color: appTheme.blueA400, - ), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: appTheme.blueA400), child: const Center( - child: Text( - 'Стать консультантом', - style: TextStyle( - color: Colors.white, - height: 1, - fontSize: 18, - ), - ), + child: Text('Стать консультантом', style: TextStyle(color: Colors.white, height: 1, fontSize: 18)), ), ), ), @@ -388,19 +290,9 @@ class _SettingsScreenState extends State { child: Ink( height: 52, width: MediaQuery.of(context).size.width - 16, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - color: const Color(0xFFEAEAEA), - ), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: const Color(0xFFEAEAEA)), child: const Center( - child: Text( - 'Данные на проверке', - style: TextStyle( - color: Colors.black, - height: 1, - fontSize: 18, - ), - ), + child: Text('Данные на проверке', style: TextStyle(color: Colors.black, height: 1, fontSize: 18)), ), ), ); @@ -413,19 +305,9 @@ class _SettingsScreenState extends State { child: Ink( height: 52, width: MediaQuery.of(context).size.width - 16, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - color: const Color(0x33FFCC00), - ), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: const Color(0x33FFCC00)), child: const Center( - child: Text( - 'Заявка обработана', - style: TextStyle( - color: Colors.black, - height: 1, - fontSize: 18, - ), - ), + child: Text('Заявка обработана', style: TextStyle(color: Colors.black, height: 1, fontSize: 18)), ), ), ), @@ -444,57 +326,28 @@ class _SettingsScreenState extends State { userRootNavigator: true, context: context, height: null, - child: DeleteAccountBottomSheet( - cubit: settingsCubit, - ), + child: DeleteAccountBottomSheet(cubit: settingsCubit), ); }, borderRadius: BorderRadius.circular(20), child: Ink( height: 52, width: MediaQuery.of(context).size.width - 16, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - color: const Color(0xFFEAEAEA), - ), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: const Color(0xFFEAEAEA)), child: const Center( - child: Text( - 'Удалить аккаунт', - style: TextStyle( - color: Colors.black, - height: 1, - fontSize: 18, - ), - ), + child: Text('Удалить аккаунт', style: TextStyle(color: Colors.black, height: 1, fontSize: 18)), ), ), ), ); } - ListTile _tile({ - required String title, - required Function() onTap, - Widget? trailing, - EdgeInsets? padding, - }) { + ListTile _tile({required String title, required Function() onTap, Widget? trailing, EdgeInsets? padding}) { return ListTile( contentPadding: padding, onTap: onTap, - title: Text( - title, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - height: 1, - ), - ), - trailing: trailing ?? - const Icon( - Icons.arrow_forward_ios_rounded, - size: 12, - color: Colors.black38, - ), + title: Text(title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500, height: 1)), + trailing: trailing ?? const Icon(Icons.arrow_forward_ios_rounded, size: 12, color: Colors.black38), ); } @@ -503,10 +356,7 @@ class _SettingsScreenState extends State { child: Container( height: 0.4, width: MediaQuery.of(context).size.width - 32, - decoration: BoxDecoration( - color: Colors.black26, - borderRadius: BorderRadius.circular(16), - ), + decoration: BoxDecoration(color: Colors.black26, borderRadius: BorderRadius.circular(16)), ), ); } @@ -530,33 +380,23 @@ class _DeleteAccountBottomSheet extends State { child: Column( mainAxisSize: MainAxisSize.min, children: [ - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), const BottomSheetHandlerBar(), - const SizedBox( - height: 20, - ), + const SizedBox(height: 20), Text(s.accountDeleting, style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), Text( s.youShureYouWantToDeleteAccount, style: TextStyles.textSMedium.copyWith(color: appTheme.black), textAlign: TextAlign.center, ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), Text( s.itCantBeRecovered, style: TextStyles.textSMedium.copyWith(color: appTheme.black), textAlign: TextAlign.center, ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), CustomOutlinedButton( text: s.delete, height: 52, @@ -573,9 +413,7 @@ class _DeleteAccountBottomSheet extends State { showErrorDialog(context, description: s.cannotDeleteAccount); }, ), - const SizedBox( - height: 40, - ), + const SizedBox(height: 40), ], ), ); diff --git a/lib/features/support/presentation/pages/support_screen.dart b/lib/features/support/presentation/pages/support_screen.dart index c6f7719a..1fc70bb0 100644 --- a/lib/features/support/presentation/pages/support_screen.dart +++ b/lib/features/support/presentation/pages/support_screen.dart @@ -69,24 +69,17 @@ class SupportScreen extends StatelessWidget { child: Text('Правовые документы', style: TextStyle(fontSize: 20, fontWeight: FontWeight.w500)), ), const SizedBox(height: 12), - InkWell( - onTap: () async => await launchUrl(Uri.parse('$baseUrl/docs/public_offer.pdf')), - borderRadius: BorderRadius.circular(16), - child: const Padding( - padding: EdgeInsets.symmetric(horizontal: 16, vertical: 4), - child: Text('Публичная офёрта', style: TextStyle(fontSize: 16)), + if (!hidePlans) ...[ + InkWell( + onTap: () async => await launchUrl(Uri.parse('$baseUrl/docs/privacy.pdf')), + borderRadius: BorderRadius.circular(16), + child: const Padding( + padding: EdgeInsets.symmetric(horizontal: 16, vertical: 4), + child: Text('Политика конфиденциальности', style: TextStyle(fontSize: 16)), + ), ), - ), - const SizedBox(height: 8), - InkWell( - onTap: () async => await launchUrl(Uri.parse('$baseUrl/docs/privacy.pdf')), - borderRadius: BorderRadius.circular(16), - child: const Padding( - padding: EdgeInsets.symmetric(horizontal: 16, vertical: 4), - child: Text('Политика конфиденциальности', style: TextStyle(fontSize: 16)), - ), - ), - const SizedBox(height: 8), + const SizedBox(height: 8), + ], InkWell( onTap: () async => await launchUrl(Uri.parse('$baseUrl/company-details')), borderRadius: BorderRadius.circular(16), From a10fe6d2f58d9ac9ba1c5c23f6d3cc12b3ecaed7 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Mon, 11 Aug 2025 17:19:14 +0300 Subject: [PATCH 097/134] fix: upd ver --- 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 2e0d56e4..bbf607e7 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -396,7 +396,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 67; + CURRENT_PROJECT_VERSION = 68; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -546,7 +546,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 67; + CURRENT_PROJECT_VERSION = 68; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -584,7 +584,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 67; + CURRENT_PROJECT_VERSION = 68; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index a1ded5ee..d9e83314 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -21,7 +21,7 @@ CFBundleSignature ???? CFBundleVersion - 67 + 68 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/pubspec.yaml b/pubspec.yaml index 3895a06b..85bbfc5f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: eatvio_app description: A new Flutter project. -version: 1.2.8+68 +version: 1.2.9+69 publish_to: 'none' environment: From c4dedac3f304e68f0686be7d432cedb295c03208 Mon Sep 17 00:00:00 2001 From: Username1548 Date: Tue, 12 Aug 2025 12:27:58 +0700 Subject: [PATCH 098/134] added build date --- .gitea/workflows/test.yml | 6 +++++- ios/fastlane/Fastfile | 4 +++- lib/core/const.dart | 1 + .../menu/presentation/pages/menu_screen.dart | 7 ++++++- .../support/presentation/pages/support_screen.dart | 13 +++++++++++-- 5 files changed, 26 insertions(+), 5 deletions(-) diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml index c7aefd5c..e6a74c64 100644 --- a/.gitea/workflows/test.yml +++ b/.gitea/workflows/test.yml @@ -144,7 +144,11 @@ jobs: run: flutter test - name: Build apk - run: flutter build apk --release --dart-define=HIDE_PLANS=false + run: | + BUILD_DATE=$(TZ=Europe/Moscow date +'%Y-%m-%d %H:%M:%S') + flutter build apk --release \ + --dart-define=HIDE_PLANS=false \ + --dart-define=BUILD_DATE="$BUILD_DATE" - name: Rename and move apk run: | diff --git a/ios/fastlane/Fastfile b/ios/fastlane/Fastfile index f7261196..ebd745be 100644 --- a/ios/fastlane/Fastfile +++ b/ios/fastlane/Fastfile @@ -46,13 +46,15 @@ platform :ios do match(type: "appstore", readonly: true) increment_build_number(xcodeproj: "Runner.xcodeproj") + + build_date = Time.now.getlocal("+03:00").strftime("%Y-%m-%d %H:%M:%S") build_app( workspace: "Runner.xcworkspace", scheme: "Runner", export_method: "app-store", codesigning_identity: "Apple Distribution", - xcargs: "--dart-define=HIDE_PLANS=true" + xcargs: "--dart-define=HIDE_PLANS=true --dart-define=BUILD_DATE='#{build_date}'" ) diff --git a/lib/core/const.dart b/lib/core/const.dart index efed7d49..98f7bc4a 100644 --- a/lib/core/const.dart +++ b/lib/core/const.dart @@ -2,6 +2,7 @@ import 'package:eatvio_app/core/env_config.dart'; String baseUrl = EnvConfig.baseUrl; bool hidePlans = const bool.fromEnvironment('HIDE_PLANS', defaultValue: false); +const buildDate = String.fromEnvironment('BUILD_DATE', defaultValue: ''); const List durationValues = [7, 14, 21]; diff --git a/lib/features/menu/presentation/pages/menu_screen.dart b/lib/features/menu/presentation/pages/menu_screen.dart index 90c866a8..d4ac69ef 100644 --- a/lib/features/menu/presentation/pages/menu_screen.dart +++ b/lib/features/menu/presentation/pages/menu_screen.dart @@ -156,7 +156,12 @@ class _AppInfoState extends State { alignment: Alignment.centerLeft, child: Padding( padding: const EdgeInsetsGeometry.only(left: 16), - child: Text('Версия приложения: $version ($buildNumber)', style: TextStyles.textXSSemibold.copyWith(color: appTheme.gray)), + child: Column( + children: [ + Text('Версия приложения: $version ($buildNumber)', style: TextStyles.textXSSemibold.copyWith(color: appTheme.gray)), + if (buildDate != '') Text('Дата сборки: $buildDate', style: TextStyles.textXSSemibold.copyWith(color: appTheme.gray)), + ], + ), ), ); } diff --git a/lib/features/support/presentation/pages/support_screen.dart b/lib/features/support/presentation/pages/support_screen.dart index 1fc70bb0..6aa44a42 100644 --- a/lib/features/support/presentation/pages/support_screen.dart +++ b/lib/features/support/presentation/pages/support_screen.dart @@ -71,15 +71,24 @@ class SupportScreen extends StatelessWidget { const SizedBox(height: 12), if (!hidePlans) ...[ InkWell( - onTap: () async => await launchUrl(Uri.parse('$baseUrl/docs/privacy.pdf')), + onTap: () async => await launchUrl(Uri.parse('$baseUrl/docs/public_offer.pdf')), borderRadius: BorderRadius.circular(16), child: const Padding( padding: EdgeInsets.symmetric(horizontal: 16, vertical: 4), - child: Text('Политика конфиденциальности', style: TextStyle(fontSize: 16)), + child: Text('Публичная офёрта', style: TextStyle(fontSize: 16)), ), ), const SizedBox(height: 8), ], + InkWell( + onTap: () async => await launchUrl(Uri.parse('$baseUrl/docs/privacy.pdf')), + borderRadius: BorderRadius.circular(16), + child: const Padding( + padding: EdgeInsets.symmetric(horizontal: 16, vertical: 4), + child: Text('Политика конфиденциальности', style: TextStyle(fontSize: 16)), + ), + ), + const SizedBox(height: 8), InkWell( onTap: () async => await launchUrl(Uri.parse('$baseUrl/company-details')), borderRadius: BorderRadius.circular(16), From 91bb5cf483042d443c12dda9a05d14fcd57add83 Mon Sep 17 00:00:00 2001 From: Username1548 Date: Tue, 12 Aug 2025 09:01:31 +0300 Subject: [PATCH 099/134] feat(menu): fixed indentation --- lib/features/menu/presentation/pages/menu_screen.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/features/menu/presentation/pages/menu_screen.dart b/lib/features/menu/presentation/pages/menu_screen.dart index d4ac69ef..7e4c358a 100644 --- a/lib/features/menu/presentation/pages/menu_screen.dart +++ b/lib/features/menu/presentation/pages/menu_screen.dart @@ -157,6 +157,7 @@ class _AppInfoState extends State { child: Padding( padding: const EdgeInsetsGeometry.only(left: 16), child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Версия приложения: $version ($buildNumber)', style: TextStyles.textXSSemibold.copyWith(color: appTheme.gray)), if (buildDate != '') Text('Дата сборки: $buildDate', style: TextStyles.textXSSemibold.copyWith(color: appTheme.gray)), From 48f5b642230c70fe3c32406c754028ff03975355 Mon Sep 17 00:00:00 2001 From: Username1548 Date: Thu, 14 Aug 2025 16:54:13 +0300 Subject: [PATCH 100/134] feat(emailVerification): added timer for email code resending --- .../pages/verify_email_screen.dart | 56 +++++++++++++++---- 1 file changed, 45 insertions(+), 11 deletions(-) diff --git a/lib/features/registration/presentation/pages/verify_email_screen.dart b/lib/features/registration/presentation/pages/verify_email_screen.dart index ed4ea11f..b05b40a0 100644 --- a/lib/features/registration/presentation/pages/verify_email_screen.dart +++ b/lib/features/registration/presentation/pages/verify_email_screen.dart @@ -1,9 +1,15 @@ +import 'dart:async'; + 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/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart'; import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_state.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'; class VerifyEmailScreen extends StatefulWidget { @@ -18,11 +24,15 @@ class _VerifyEmailScreenState extends State { final List _controllers = List.generate(4, (_) => TextEditingController()); String email = ''; late final GoRouter goRouter; - + late Timer _timer; + int _secondsRemaining = 60; + bool _canSendAgain = false; @override void initState() { super.initState(); goRouter = GoRouter.of(context); + FlutterNativeSplash.remove(); + _startTimer(); } @override @@ -36,6 +46,23 @@ class _VerifyEmailScreenState extends State { super.dispose(); } + void _startTimer() { + setState(() { + _canSendAgain = false; + _secondsRemaining = 60; + }); + _timer = Timer.periodic(const Duration(seconds: 1), (timer) { + setState(() { + if (_secondsRemaining > 0) { + _secondsRemaining--; + } else { + _canSendAgain = true; + _timer.cancel(); + } + }); + }); + } + void _checkAndSubmitCode(BuildContext context) { final code = _controllers.map((c) => c.text).join(); if (code.length == 4) { @@ -74,18 +101,25 @@ class _VerifyEmailScreenState extends State { children: [ Text("Код отправлен на почту\n$email", textAlign: TextAlign.center), const SizedBox(height: 20), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: List.generate(4, (index) => _buildCodeField(context, index)), - ), - const SizedBox(height: 20), - const Text("Запросить код повторно", style: TextStyle(color: Colors.grey)), - TextButton( - onPressed: () { - context.read().sendEmailCode(); + Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: List.generate(4, (index) => _buildCodeField(context, index))), + const SizedBox(height: 40), + CustomOutlinedButton( + text: 'Запросить код повторно', + height: 52, + isDisabled: !_canSendAgain, + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), + buttonStyle: ButtonStyle( + side: const WidgetStatePropertyAll(BorderSide.none), + backgroundColor: WidgetStatePropertyAll(_canSendAgain ? appTheme.blueA400 : appTheme.gray), + ), + onPressed: () async { + await context.read().sendEmailCode(); + _startTimer(); }, - child: const Text("Не приходит код?"), ), + const SizedBox(height: 8), + if (!_canSendAgain) + Text("Зпросить код повторно можно через $_secondsRemaining с.", style: TextStyles.textSMedium.copyWith(color: appTheme.gray)), ], ), ), From 62c0914fe27ce099a84ac6a7aae8a9934541940d Mon Sep 17 00:00:00 2001 From: Username1548 Date: Thu, 14 Aug 2025 16:55:15 +0300 Subject: [PATCH 101/134] fix --- .../registration/presentation/pages/verify_email_screen.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/features/registration/presentation/pages/verify_email_screen.dart b/lib/features/registration/presentation/pages/verify_email_screen.dart index b05b40a0..311eb219 100644 --- a/lib/features/registration/presentation/pages/verify_email_screen.dart +++ b/lib/features/registration/presentation/pages/verify_email_screen.dart @@ -30,8 +30,7 @@ class _VerifyEmailScreenState extends State { @override void initState() { super.initState(); - goRouter = GoRouter.of(context); - FlutterNativeSplash.remove(); + goRouter = GoRouter.of(context); _startTimer(); } From 45774c38755dd97abc0f5184e6d316ce7f6ad4e2 Mon Sep 17 00:00:00 2001 From: Username1548 Date: Thu, 14 Aug 2025 16:58:00 +0300 Subject: [PATCH 102/134] fix --- .../registration/presentation/pages/verify_email_screen.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/features/registration/presentation/pages/verify_email_screen.dart b/lib/features/registration/presentation/pages/verify_email_screen.dart index 311eb219..f50d4960 100644 --- a/lib/features/registration/presentation/pages/verify_email_screen.dart +++ b/lib/features/registration/presentation/pages/verify_email_screen.dart @@ -9,7 +9,6 @@ import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_ import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_state.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'; class VerifyEmailScreen extends StatefulWidget { From 2f74387df21178473e390605f95d1dbd3394e389 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Thu, 14 Aug 2025 18:13:34 +0300 Subject: [PATCH 103/134] fix: firebase notificatons when qr login --- lib/features/auth/presentation/pages/scan_qr_code_screen.dart | 2 ++ 1 file changed, 2 insertions(+) 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 411cb3a5..83296f0e 100644 --- a/lib/features/auth/presentation/pages/scan_qr_code_screen.dart +++ b/lib/features/auth/presentation/pages/scan_qr_code_screen.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:dio/dio.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'; import 'package:eatvio_app/core/utils/toasts.dart'; @@ -92,6 +93,7 @@ class _ScanQRCodeScreenState extends State { await sl().connectToWebSocket(); await sl().getNotifications(); await sl().connectToWebSocket(); + await sl().registerPushToken(); } catch (e) { rethrow; } From 4f249499a98cdeee89822dddb3c21326ff0046ec Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Fri, 15 Aug 2025 13:28:12 +0300 Subject: [PATCH 104/134] fix(notifications): Issue-191 --- .../models/notification_model.dart | 998 +++++++++++++++++- .../notifications_cubit.dart | 194 +++- .../widgets/notification_widget.dart | 482 +++++++++ .../service/notifications_service.dart | 237 ++++- 4 files changed, 1876 insertions(+), 35 deletions(-) diff --git a/lib/features/notifications/models/notification_model.dart b/lib/features/notifications/models/notification_model.dart index 31d74e10..55c79e51 100644 --- a/lib/features/notifications/models/notification_model.dart +++ b/lib/features/notifications/models/notification_model.dart @@ -548,13 +548,7 @@ class PlanSubscriptionExpiredNotification implements BaseNotificationModel { @override final DateTime createdAt; - PlanSubscriptionExpiredNotification({ - required this.type, - required this.planName, - required this.customerName, - required this.isRead, - required this.createdAt, - }); + PlanSubscriptionExpiredNotification({required this.type, required this.planName, required this.customerName, required this.isRead, required this.createdAt}); factory PlanSubscriptionExpiredNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { return PlanSubscriptionExpiredNotification( @@ -840,7 +834,937 @@ class UnknownNotification implements BaseNotificationModel { UnknownNotification({required this.type, this.isRead = true, DateTime? createdAt}) : createdAt = createdAt ?? DateTime.fromMillisecondsSinceEpoch(0); } +class DietCancelledNotification implements BaseNotificationModel { + @override + final NotificationType type; + @override + final bool isRead; + final String dietName; + @override + final DateTime createdAt; + + DietCancelledNotification({required this.type, required this.dietName, required this.isRead, required this.createdAt}); + + factory DietCancelledNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { + return DietCancelledNotification( + type: type, + dietName: json['diet_name'] ?? '', + isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), + ); + } +} + +class DietStartedNotification implements BaseNotificationModel { + @override + final NotificationType type; + @override + final bool isRead; + final String dietName; + @override + final DateTime createdAt; + + DietStartedNotification({required this.type, required this.dietName, required this.isRead, required this.createdAt}); + + factory DietStartedNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { + return DietStartedNotification( + type: type, + dietName: json['diet_name'] ?? '', + isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), + ); + } +} + +class DietExpiredNotification implements BaseNotificationModel { + @override + final NotificationType type; + @override + final bool isRead; + final String dietName; + @override + final DateTime createdAt; + + DietExpiredNotification({required this.type, required this.dietName, required this.isRead, required this.createdAt}); + + factory DietExpiredNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { + return DietExpiredNotification( + type: type, + dietName: json['diet_name'] ?? '', + isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), + ); + } +} + +class DietWillStartedInNotification implements BaseNotificationModel { + @override + final NotificationType type; + @override + final bool isRead; + final String dietName; + final String startDate; + @override + final DateTime createdAt; + + DietWillStartedInNotification({required this.type, required this.dietName, required this.startDate, required this.isRead, required this.createdAt}); + + factory DietWillStartedInNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { + return DietWillStartedInNotification( + type: type, + dietName: json['diet_name'] ?? '', + startDate: json['start_date'] ?? '', + isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), + ); + } +} + +class DietCanNotBeStartedNotification implements BaseNotificationModel { + @override + final NotificationType type; + @override + final bool isRead; + final String dietName; + final String reason; + @override + final DateTime createdAt; + + DietCanNotBeStartedNotification({required this.type, required this.dietName, required this.reason, required this.isRead, required this.createdAt}); + + factory DietCanNotBeStartedNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { + return DietCanNotBeStartedNotification( + type: type, + dietName: json['diet_name'] ?? '', + reason: json['reason'] ?? '', + isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), + ); + } +} + +class PersonalDietCreationNeededNotification implements BaseNotificationModel { + @override + final NotificationType type; + @override + final bool isRead; + final String planName; + final String clientName; + @override + final DateTime createdAt; + + PersonalDietCreationNeededNotification({required this.type, required this.planName, required this.clientName, required this.isRead, required this.createdAt}); + + factory PersonalDietCreationNeededNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { + return PersonalDietCreationNeededNotification( + type: type, + planName: json['plan_name'] ?? '', + clientName: json['client_name'] ?? '', + isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), + ); + } +} + +class ChosenPlanClientLastDayFinishedNotification implements BaseNotificationModel { + @override + final NotificationType type; + @override + final bool isRead; + final String planName; + @override + final DateTime createdAt; + + ChosenPlanClientLastDayFinishedNotification({required this.type, required this.planName, required this.isRead, required this.createdAt}); + + factory ChosenPlanClientLastDayFinishedNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { + return ChosenPlanClientLastDayFinishedNotification( + type: type, + planName: json['plan_name'] ?? '', + isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), + ); + } +} + +class ChosenPlanClientLastDayStartedNotification implements BaseNotificationModel { + @override + final NotificationType type; + @override + final bool isRead; + final String planName; + @override + final DateTime createdAt; + + ChosenPlanClientLastDayStartedNotification({required this.type, required this.planName, required this.isRead, required this.createdAt}); + + factory ChosenPlanClientLastDayStartedNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { + return ChosenPlanClientLastDayStartedNotification( + type: type, + planName: json['plan_name'] ?? '', + isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), + ); + } +} + +class StayOnTrackToAuthorNotification implements BaseNotificationModel { + @override + final NotificationType type; + @override + final bool isRead; + final String planName; + final String clientName; + @override + final DateTime createdAt; + + StayOnTrackToAuthorNotification({required this.type, required this.planName, required this.clientName, required this.isRead, required this.createdAt}); + + factory StayOnTrackToAuthorNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { + return StayOnTrackToAuthorNotification( + type: type, + planName: json['plan_name'] ?? '', + clientName: json['client_name'] ?? '', + isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), + ); + } +} + +class ClientStartPlanSubscriptionToSubscriberNotification implements BaseNotificationModel { + @override + final NotificationType type; + @override + final bool isRead; + final String planName; + @override + final DateTime createdAt; + + ClientStartPlanSubscriptionToSubscriberNotification({required this.type, required this.planName, required this.isRead, required this.createdAt}); + + factory ClientStartPlanSubscriptionToSubscriberNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { + return ClientStartPlanSubscriptionToSubscriberNotification( + type: type, + planName: json['plan_name'] ?? '', + isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), + ); + } +} + +class ConsultantConfirmPlanSubscriptionToSubscriberNotification implements BaseNotificationModel { + @override + final NotificationType type; + @override + final bool isRead; + final String planName; + final String consultantName; + @override + final DateTime createdAt; + + ConsultantConfirmPlanSubscriptionToSubscriberNotification({ + required this.type, + required this.planName, + required this.consultantName, + required this.isRead, + required this.createdAt, + }); + + factory ConsultantConfirmPlanSubscriptionToSubscriberNotification.fromJson( + Map json, + NotificationType type, { + bool? isRead, + String? createdAt, + }) { + return ConsultantConfirmPlanSubscriptionToSubscriberNotification( + type: type, + planName: json['plan_name'] ?? '', + consultantName: json['consultant_name'] ?? '', + isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), + ); + } +} + +class ClientPaidPlanSubscriptionToAuthorNotification implements BaseNotificationModel { + @override + final NotificationType type; + @override + final bool isRead; + final String planName; + final String clientName; + final double amount; + @override + final DateTime createdAt; + + ClientPaidPlanSubscriptionToAuthorNotification({ + required this.type, + required this.planName, + required this.clientName, + required this.amount, + required this.isRead, + required this.createdAt, + }); + + factory ClientPaidPlanSubscriptionToAuthorNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { + return ClientPaidPlanSubscriptionToAuthorNotification( + type: type, + planName: json['plan_name'] ?? '', + clientName: json['client_name'] ?? '', + amount: (json['amount'] as num?)?.toDouble() ?? 0.0, + isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), + ); + } +} + +class ConsultantLeftDiaryCommentNotification implements BaseNotificationModel { + @override + final NotificationType type; + @override + final bool isRead; + final String planName; + final String consultantName; + final DateTime diaryDate; + @override + final DateTime createdAt; + + ConsultantLeftDiaryCommentNotification({ + required this.type, + required this.planName, + required this.consultantName, + required this.diaryDate, + required this.isRead, + required this.createdAt, + }); + + factory ConsultantLeftDiaryCommentNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { + return ConsultantLeftDiaryCommentNotification( + type: type, + planName: json['plan_name'] ?? '', + consultantName: json['consultant_name'] ?? '', + diaryDate: parseDate(json['diary_date'] ?? DateTime.now().toIso8601String()), + isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), + ); + } +} + +class ConsultantConfirmPlanSubscriptionToAuthorNotification implements BaseNotificationModel { + @override + final NotificationType type; + @override + final bool isRead; + final String planName; + final String consultantName; + @override + final DateTime createdAt; + + ConsultantConfirmPlanSubscriptionToAuthorNotification({ + required this.type, + required this.planName, + required this.consultantName, + required this.isRead, + required this.createdAt, + }); + + factory ConsultantConfirmPlanSubscriptionToAuthorNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { + return ConsultantConfirmPlanSubscriptionToAuthorNotification( + type: type, + planName: json['plan_name'] ?? '', + consultantName: json['consultant_name'] ?? '', + isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), + ); + } +} + +class ClientPaidPlanSubscriptionToSubscriberNotification implements BaseNotificationModel { + @override + final NotificationType type; + @override + final bool isRead; + final String planName; + final double amount; + @override + final DateTime createdAt; + + ClientPaidPlanSubscriptionToSubscriberNotification({ + required this.type, + required this.planName, + required this.amount, + required this.isRead, + required this.createdAt, + }); + + factory ClientPaidPlanSubscriptionToSubscriberNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { + return ClientPaidPlanSubscriptionToSubscriberNotification( + type: type, + planName: json['plan_name'] ?? '', + amount: (json['amount'] as num?)?.toDouble() ?? 0.0, + isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), + ); + } +} + +class ClientStartPlanSubscriptionToAuthorNotification implements BaseNotificationModel { + @override + final NotificationType type; + @override + final bool isRead; + final String planName; + final String clientName; + @override + final DateTime createdAt; + + ClientStartPlanSubscriptionToAuthorNotification({ + required this.type, + required this.planName, + required this.clientName, + required this.isRead, + required this.createdAt, + }); + + factory ClientStartPlanSubscriptionToAuthorNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { + return ClientStartPlanSubscriptionToAuthorNotification( + type: type, + planName: json['plan_name'] ?? '', + clientName: json['client_name'] ?? '', + isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), + ); + } +} + +class ClientCancelPlanSubscriptionToAuthorNotification implements BaseNotificationModel { + @override + final NotificationType type; + @override + final bool isRead; + final String planName; + final String clientName; + @override + final DateTime createdAt; + + ClientCancelPlanSubscriptionToAuthorNotification({ + required this.type, + required this.planName, + required this.clientName, + required this.isRead, + required this.createdAt, + }); + + factory ClientCancelPlanSubscriptionToAuthorNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { + return ClientCancelPlanSubscriptionToAuthorNotification( + type: type, + planName: json['plan_name'] ?? '', + clientName: json['client_name'] ?? '', + isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), + ); + } +} + +class ClientCancelPlanSubscriptionToSubscriberNotification implements BaseNotificationModel { + @override + final NotificationType type; + @override + final bool isRead; + final String planName; + @override + final DateTime createdAt; + + ClientCancelPlanSubscriptionToSubscriberNotification({required this.type, required this.planName, required this.isRead, required this.createdAt}); + + factory ClientCancelPlanSubscriptionToSubscriberNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { + return ClientCancelPlanSubscriptionToSubscriberNotification( + type: type, + planName: json['plan_name'] ?? '', + isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), + ); + } +} + +class UserDailyReportCompletedNotification implements BaseNotificationModel { + @override + final NotificationType type; + @override + final bool isRead; + final String planName; + final String clientName; + final DateTime reportDate; + @override + final DateTime createdAt; + + UserDailyReportCompletedNotification({ + required this.type, + required this.planName, + required this.clientName, + required this.reportDate, + required this.isRead, + required this.createdAt, + }); + + factory UserDailyReportCompletedNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { + return UserDailyReportCompletedNotification( + type: type, + planName: json['plan_name'] ?? '', + clientName: json['client_name'] ?? '', + reportDate: parseDate(json['report_date'] ?? DateTime.now().toIso8601String()), + isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), + ); + } +} + +class StayOnTrackToCustomerNotification implements BaseNotificationModel { + @override + final NotificationType type; + @override + final bool isRead; + final String planName; + final String consultantName; + @override + final DateTime createdAt; + + StayOnTrackToCustomerNotification({required this.type, required this.planName, required this.consultantName, required this.isRead, required this.createdAt}); + + factory StayOnTrackToCustomerNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { + return StayOnTrackToCustomerNotification( + type: type, + planName: json['plan_name'] ?? '', + consultantName: json['consultant_name'] ?? '', + isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), + ); + } +} + +class PhoneChangedNotification implements BaseNotificationModel { + @override + final NotificationType type; + @override + final bool isRead; + final String oldPhone; + final String newPhone; + @override + final DateTime createdAt; + + PhoneChangedNotification({required this.type, required this.oldPhone, required this.newPhone, required this.isRead, required this.createdAt}); + + factory PhoneChangedNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { + return PhoneChangedNotification( + type: type, + oldPhone: json['old_phone'] ?? '', + newPhone: json['new_phone'] ?? '', + isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), + ); + } +} + +class SubscriptionNotification implements BaseNotificationModel { + @override + final NotificationType type; + @override + final bool isRead; + final String subscriptionType; + final String planName; + @override + final DateTime createdAt; + + SubscriptionNotification({required this.type, required this.subscriptionType, required this.planName, required this.isRead, required this.createdAt}); + + factory SubscriptionNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { + return SubscriptionNotification( + type: type, + subscriptionType: json['subscription_type'] ?? '', + planName: json['plan_name'] ?? '', + isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), + ); + } +} + +class BadgeAwardedNotification implements BaseNotificationModel { + @override + final NotificationType type; + @override + final bool isRead; + final String badgeName; + final String badgeDescription; + @override + final DateTime createdAt; + + BadgeAwardedNotification({required this.type, required this.badgeName, required this.badgeDescription, required this.isRead, required this.createdAt}); + + factory BadgeAwardedNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { + return BadgeAwardedNotification( + type: type, + badgeName: json['badge_name'] ?? '', + badgeDescription: json['badge_description'] ?? '', + isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), + ); + } +} + +class BadgeRevokedNotification implements BaseNotificationModel { + @override + final NotificationType type; + @override + final bool isRead; + final String badgeName; + final String reason; + @override + final DateTime createdAt; + + BadgeRevokedNotification({required this.type, required this.badgeName, required this.reason, required this.isRead, required this.createdAt}); + + factory BadgeRevokedNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { + return BadgeRevokedNotification( + type: type, + badgeName: json['badge_name'] ?? '', + reason: json['reason'] ?? '', + isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), + ); + } +} + +class ConsultantRequestStartedNotification implements BaseNotificationModel { + @override + final NotificationType type; + @override + final bool isRead; + final String consultantName; + @override + final DateTime createdAt; + + ConsultantRequestStartedNotification({required this.type, required this.consultantName, required this.isRead, required this.createdAt}); + + factory ConsultantRequestStartedNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { + return ConsultantRequestStartedNotification( + type: type, + consultantName: json['consultant_name'] ?? '', + isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), + ); + } +} + +class ClientCloseComplaintSuccessNotification implements BaseNotificationModel { + @override + final NotificationType type; + @override + final bool isRead; + final String complaintType; + final String entityName; + @override + final DateTime createdAt; + + ClientCloseComplaintSuccessNotification({ + required this.type, + required this.complaintType, + required this.entityName, + required this.isRead, + required this.createdAt, + }); + + factory ClientCloseComplaintSuccessNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { + return ClientCloseComplaintSuccessNotification( + type: type, + complaintType: json['complaint_type'] ?? '', + entityName: json['entity_name'] ?? '', + isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), + ); + } +} + +class ClientRefundSuccessNotification implements BaseNotificationModel { + @override + final NotificationType type; + @override + final bool isRead; + final String planName; + final double amount; + final String reason; + @override + final DateTime createdAt; + + ClientRefundSuccessNotification({ + required this.type, + required this.planName, + required this.amount, + required this.reason, + required this.isRead, + required this.createdAt, + }); + + factory ClientRefundSuccessNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { + return ClientRefundSuccessNotification( + type: type, + planName: json['plan_name'] ?? '', + amount: (json['amount'] as num?)?.toDouble() ?? 0.0, + reason: json['reason'] ?? '', + isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), + ); + } +} + +class ConsultantRefundSuccessNotification implements BaseNotificationModel { + @override + final NotificationType type; + @override + final bool isRead; + final String planName; + final String clientName; + final double amount; + @override + final DateTime createdAt; + + ConsultantRefundSuccessNotification({ + required this.type, + required this.planName, + required this.clientName, + required this.amount, + required this.isRead, + required this.createdAt, + }); + + factory ConsultantRefundSuccessNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { + return ConsultantRefundSuccessNotification( + type: type, + planName: json['plan_name'] ?? '', + clientName: json['client_name'] ?? '', + amount: (json['amount'] as num?)?.toDouble() ?? 0.0, + isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), + ); + } +} + +class ClientRejectSuccessNotification implements BaseNotificationModel { + @override + final NotificationType type; + @override + final bool isRead; + final String planName; + final String reason; + @override + final DateTime createdAt; + + ClientRejectSuccessNotification({required this.type, required this.planName, required this.reason, required this.isRead, required this.createdAt}); + + factory ClientRejectSuccessNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { + return ClientRejectSuccessNotification( + type: type, + planName: json['plan_name'] ?? '', + reason: json['reason'] ?? '', + isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), + ); + } +} + +class ConsultantRejectSuccessNotification implements BaseNotificationModel { + @override + final NotificationType type; + @override + final bool isRead; + final String planName; + final String clientName; + final String reason; + @override + final DateTime createdAt; + + ConsultantRejectSuccessNotification({ + required this.type, + required this.planName, + required this.clientName, + required this.reason, + required this.isRead, + required this.createdAt, + }); + + factory ConsultantRejectSuccessNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { + return ConsultantRejectSuccessNotification( + type: type, + planName: json['plan_name'] ?? '', + clientName: json['client_name'] ?? '', + reason: json['reason'] ?? '', + isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), + ); + } +} + +class ScoreSubstractedNotification implements BaseNotificationModel { + @override + final NotificationType type; + @override + final bool isRead; + final String scoreKey; + final int scorableId; + final String scorableType; + final String scorableName; + final String scorableSlug; + final int scoreValue; + @override + final DateTime createdAt; + + ScoreSubstractedNotification({ + required this.type, + required this.scoreKey, + required this.scorableId, + required this.scorableType, + required this.scorableName, + required this.scorableSlug, + required this.scoreValue, + required this.isRead, + required this.createdAt, + }); + + factory ScoreSubstractedNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { + return ScoreSubstractedNotification( + type: type, + scoreKey: json['score_key'], + scorableId: json['scorable_id'], + scorableType: json['scorable_type'], + scorableName: json['scorable_name'] ?? '', + scorableSlug: json['scorable_slug'] ?? '', + scoreValue: json['score_value'], + isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), + ); + } +} + +class ProductStatusUpdatedForAuthorNotification implements BaseNotificationModel { + @override + final NotificationType type; + @override + final bool isRead; + final String productName; + final String oldStatus; + final String newStatus; + @override + final DateTime createdAt; + + ProductStatusUpdatedForAuthorNotification({ + required this.type, + required this.productName, + required this.oldStatus, + required this.newStatus, + required this.isRead, + required this.createdAt, + }); + + factory ProductStatusUpdatedForAuthorNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { + return ProductStatusUpdatedForAuthorNotification( + type: type, + productName: json['product_name'] ?? '', + oldStatus: json['old_status'] ?? '', + newStatus: json['new_status'] ?? '', + isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), + ); + } +} + +class DiaryReminderForUsersNotification implements BaseNotificationModel { + @override + final NotificationType type; + @override + final bool isRead; + @override + final DateTime createdAt; + + DiaryReminderForUsersNotification({required this.type, required this.isRead, required this.createdAt}); + + factory DiaryReminderForUsersNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { + return DiaryReminderForUsersNotification( + type: type, + isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), + ); + } +} + enum NotificationType { + // Diet related + dietCancelled, + dietStarted, + dietExpired, + dietWillStartedIn, + dietCanNotBeStarted, + + // Plan related + personalDietCreationNeeded, + chosenPlanClientLastDayFinished, + chosenPlanConsultantLastDayStarted, + chosenPlanClientLastDayStarted, + stayOnTrackToAuthor, + chosenPlanStartedToClient, + clientStartPlanSubscriptionToSubscriber, + consultantConfirmPlanSubscriptionToSubscriber, + clientPaidPlanSubscriptionToAuthor, + chosenPlanTomorrowReminderToClient, + chosenPlanTomorrowReminderToConsultant, + chosenPlanStartedToConsultant, + consultantLeftDiaryComment, + chosenPlanFinishedToClient, + consultantConfirmPlanSubscriptionToAuthor, + clientPaidPlanSubscriptionToSubscriber, + clientStartPlanSubscriptionToAuthor, + clientCancelPlanSubscriptionToAuthor, + clientCancelPlanSubscriptionToSubscriber, + userDailyReportCompleted, + chosenPlanFinishedToConsultant, + stayOnTrackToCustomer, + + // Comments + entityCommented, + commentReplied, + + // Profile + phoneChanged, + + // Subscription + subscription, + + // Badges + badgeAwarded, + badgeRevoked, + + // Consultant + consultantRequestStarted, + + // Complaints + clientCloseComplaintSuccess, + complaintToModerationForUser, + + // Finance + clientRefundSuccess, + consultantRefundSuccess, + clientRejectSuccess, + consultantRejectSuccess, + + // Score + scoreAdded, + scoreSubstracted, + + // Product + productStatusUpdatedForAuthor, + + // Diary + diaryReminderForUsers, + + // Legacy types (keeping for backward compatibility) planSubscriptionStartedToAuthor, planSubscriptionCanceledToAuthor, chosenPlanStartedToClientNotification, @@ -848,20 +1772,11 @@ enum NotificationType { diaryReminderNotification, consultantLeftComment, personalizedDietIsDefined, - chosenPlanFinishedToClient, planSubscriptionCanceledToCustomer, chosenPlanStartedToConsultantNotification, chosenPlanTomorrowReminderToConsultantNotification, planSubscriptionPaidToAuthor, - chosenPlanFinishedToConsultant, chosenPlanLastDayFinishedForUser, - // Новые типы - chosenPlanConsultantLastDayStarted, - complaintToModerationForUser, - commentReplied, - scoreAdded, - entityCommented, - // Дополнительные типы уведомлений planSubscriptionExpired, planSubscriptionRenewed, consultantAssigned, @@ -874,10 +1789,57 @@ enum NotificationType { unknown; static NotificationType getTypeFromString(String type) => switch (type) { + // New type-based mapping + 'diet-cancelled' => NotificationType.dietCancelled, + 'entity-commented' => NotificationType.entityCommented, + 'comment-replied' => NotificationType.commentReplied, + 'phone-changed' => NotificationType.phoneChanged, + 'diet-started' => NotificationType.dietStarted, + 'personal-diet-creation-needed-to-author' => NotificationType.personalDietCreationNeeded, + 'chosen-plan-client-last-day-finished' => NotificationType.chosenPlanClientLastDayFinished, + 'chosen-plan-consultant-last-day-started' => NotificationType.chosenPlanConsultantLastDayStarted, + 'chosen-plan-client-last-day-started' => NotificationType.chosenPlanClientLastDayStarted, + 'subscription' => NotificationType.subscription, + 'badge-awarded' => NotificationType.badgeAwarded, + 'badge-revoked' => NotificationType.badgeRevoked, + 'diet-expired' => NotificationType.dietExpired, + 'consultant-request-started' => NotificationType.consultantRequestStarted, + 'diet-will-started-in' => NotificationType.dietWillStartedIn, + 'client-close-complaint-success' => NotificationType.clientCloseComplaintSuccess, + 'complaint-to-moderation-for-user' => NotificationType.complaintToModerationForUser, + 'client-refund-success' => NotificationType.clientRefundSuccess, + 'consultant-refund-success' => NotificationType.consultantRefundSuccess, + 'client-reject-success' => NotificationType.clientRejectSuccess, + 'consultant-reject-success' => NotificationType.consultantRejectSuccess, + 'score-added' => NotificationType.scoreAdded, + 'score-substracted' => NotificationType.scoreSubstracted, + 'product-status-updated-for-author' => NotificationType.productStatusUpdatedForAuthor, + 'diet-can-not-started' => NotificationType.dietCanNotBeStarted, + 'stay-on-track-to-author' => NotificationType.stayOnTrackToAuthor, + 'chosen-plan-started-to-client' => NotificationType.chosenPlanStartedToClient, + 'client-start-plan-subscription-to-subscriber' => NotificationType.clientStartPlanSubscriptionToSubscriber, + 'consultant-confirm-plan-subscription-to-subscriber' => NotificationType.consultantConfirmPlanSubscriptionToSubscriber, + 'client-paid-plan-subscription-to-author' => NotificationType.clientPaidPlanSubscriptionToAuthor, + 'chosen-plan-tomorrow-reminder-to-client' => NotificationType.chosenPlanTomorrowReminderToClient, + 'chosen-plan-tomorrow-reminder-to-consultant' => NotificationType.chosenPlanTomorrowReminderToConsultant, + 'chosen-plan-started-to-consultant' => NotificationType.chosenPlanStartedToConsultant, + 'consultant-left-diary-comment' => NotificationType.consultantLeftDiaryComment, + 'chosen-plan-finished-to-client' => NotificationType.chosenPlanFinishedToClient, + 'consultant-confirm-plan-subscription-to-author' => NotificationType.consultantConfirmPlanSubscriptionToAuthor, + 'client-paid-plan-subscription-to-subscriber' => NotificationType.clientPaidPlanSubscriptionToSubscriber, + 'client-start-plan-subscription-to-author' => NotificationType.clientStartPlanSubscriptionToAuthor, + 'client-cancel-plan-subscription-to-author' => NotificationType.clientCancelPlanSubscriptionToAuthor, + 'client-cancel-plan-subscription-to-subscriber' => NotificationType.clientCancelPlanSubscriptionToSubscriber, + 'user-daily-report-completed' => NotificationType.userDailyReportCompleted, + 'chosen-plan-finished-to-consultant' => NotificationType.chosenPlanFinishedToConsultant, + 'stay-on-track-to-customer' => NotificationType.stayOnTrackToCustomer, + 'diary-reminder-for-users' => NotificationType.diaryReminderForUsers, + + // Legacy mapping 'App\\Notifications\\Plan\\PlanSubscriptionStartedToAuthor' => NotificationType.planSubscriptionStartedToAuthor, 'App\\Notifications\\Plan\\PlanSubscriptionCanceledToAuthor' => NotificationType.planSubscriptionCanceledToAuthor, 'App\\Notifications\\Plan\\ChosenPlanStartedToClientNotification' => NotificationType.chosenPlanStartedToClientNotification, - 'App\\Notifications\\Plan\\ChosenPlanTomorrowReminderToClientNotification' => NotificationType.chosenPlanStartedToClientNotification, + 'App\\Notifications\\Plan\\ChosenPlanTomorrowReminderToClientNotification' => NotificationType.chosenPlanTomorrowReminderToClientNotification, 'App\\Notifications\\DiaryReminderNotification' => NotificationType.diaryReminderNotification, 'App\\Notifications\\Plan\\ConsultantLeftComment' => NotificationType.consultantLeftComment, 'App\\Notifications\\Plan\\PersonalizedDietIsDefined' => NotificationType.personalizedDietIsDefined, @@ -888,13 +1850,11 @@ enum NotificationType { 'App\\Notifications\\Plan\\PlanSubscriptionPaidToAuthor' => NotificationType.planSubscriptionPaidToAuthor, 'App\\Notifications\\Plan\\ChosenPlanFinishedToConsultant' => NotificationType.chosenPlanFinishedToConsultant, 'App\\Notifications\\ChosenPlan\\LastDayFinishedForUser' => NotificationType.chosenPlanLastDayFinishedForUser, - // Новые маппинги 'App\\Notifications\\ChosenPlan\\LastDayStartedForConsultant' => NotificationType.chosenPlanConsultantLastDayStarted, 'App\\Notifications\\ComplaintToModerationNotify' => NotificationType.complaintToModerationForUser, 'App\\Notifications\\Comment\\Replied' => NotificationType.commentReplied, 'App\\Notifications\\Score\\ScoreAdded' => NotificationType.scoreAdded, 'App\\Notifications\\Comment\\EntityCommented' => NotificationType.entityCommented, - // Дополнительные маппинги 'App\\Notifications\\Plan\\PlanSubscriptionExpired' => NotificationType.planSubscriptionExpired, 'App\\Notifications\\Plan\\PlanSubscriptionRenewed' => NotificationType.planSubscriptionRenewed, 'App\\Notifications\\Plan\\ConsultantAssigned' => NotificationType.consultantAssigned, diff --git a/lib/features/notifications/presentation/notifications_cubit/notifications_cubit.dart b/lib/features/notifications/presentation/notifications_cubit/notifications_cubit.dart index df921b4a..8c8c5aaa 100644 --- a/lib/features/notifications/presentation/notifications_cubit/notifications_cubit.dart +++ b/lib/features/notifications/presentation/notifications_cubit/notifications_cubit.dart @@ -66,12 +66,6 @@ class NotificationsCubit extends Cubit { isRead: isRead, createdAt: createdAt, ), - NotificationType.chosenPlanFinishedToClient => ChosenPlanFinishedToClientNotification.fromJson( - notificationJson, - type, - isRead: isRead, - createdAt: createdAt, - ), NotificationType.planSubscriptionCanceledToCustomer => PlanSubscriptionCanceledToAuthorNotification.fromJson( notificationJson, type, @@ -96,12 +90,6 @@ class NotificationsCubit extends Cubit { isRead: isRead, createdAt: createdAt, ), - NotificationType.chosenPlanFinishedToConsultant => ChosenPlanFinishedToConsultantNotification.fromJson( - notificationJson, - type, - isRead: isRead, - createdAt: createdAt, - ), NotificationType.chosenPlanLastDayFinishedForUser => ChosenPlanLastDayFinishedForUser.fromJson( notificationJson, type, @@ -123,6 +111,156 @@ class NotificationsCubit extends Cubit { NotificationType.goalAchieved => GoalAchievedNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt), NotificationType.workoutReminder => WorkoutReminderNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt), NotificationType.mealReminder => MealReminderNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt), + // New notification types + NotificationType.dietCancelled => DietCancelledNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt), + NotificationType.dietStarted => DietStartedNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt), + NotificationType.dietExpired => DietExpiredNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt), + NotificationType.dietWillStartedIn => DietWillStartedInNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt), + NotificationType.dietCanNotBeStarted => DietCanNotBeStartedNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt), + NotificationType.personalDietCreationNeeded => PersonalDietCreationNeededNotification.fromJson( + notificationJson, + type, + isRead: isRead, + createdAt: createdAt, + ), + NotificationType.chosenPlanClientLastDayFinished => ChosenPlanClientLastDayFinishedNotification.fromJson( + notificationJson, + type, + isRead: isRead, + createdAt: createdAt, + ), + NotificationType.chosenPlanClientLastDayStarted => ChosenPlanClientLastDayStartedNotification.fromJson( + notificationJson, + type, + isRead: isRead, + createdAt: createdAt, + ), + NotificationType.stayOnTrackToAuthor => StayOnTrackToAuthorNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt), + NotificationType.chosenPlanStartedToClient => ChosenPlanStartedToClientNotification.fromJson( + notificationJson, + type, + isRead: isRead, + createdAt: createdAt, + ), + NotificationType.clientStartPlanSubscriptionToSubscriber => ClientStartPlanSubscriptionToSubscriberNotification.fromJson( + notificationJson, + type, + isRead: isRead, + createdAt: createdAt, + ), + NotificationType.consultantConfirmPlanSubscriptionToSubscriber => ConsultantConfirmPlanSubscriptionToSubscriberNotification.fromJson( + notificationJson, + type, + isRead: isRead, + createdAt: createdAt, + ), + NotificationType.clientPaidPlanSubscriptionToAuthor => ClientPaidPlanSubscriptionToAuthorNotification.fromJson( + notificationJson, + type, + isRead: isRead, + createdAt: createdAt, + ), + NotificationType.chosenPlanTomorrowReminderToClient => ChosenPlanTomorrowReminderToClientNotification.fromJson( + notificationJson, + type, + isRead: isRead, + createdAt: createdAt, + ), + NotificationType.chosenPlanTomorrowReminderToConsultant => ChosenPlanTomorrowReminderToConsultantNotification.fromJson( + notificationJson, + type, + isRead: isRead, + createdAt: createdAt, + ), + NotificationType.chosenPlanStartedToConsultant => ChosenPlanStartedToConsultantNotification.fromJson( + notificationJson, + type, + isRead: isRead, + createdAt: createdAt, + ), + NotificationType.consultantLeftDiaryComment => ConsultantLeftDiaryCommentNotification.fromJson( + notificationJson, + type, + isRead: isRead, + createdAt: createdAt, + ), + NotificationType.chosenPlanFinishedToClient => ChosenPlanFinishedToClientNotification.fromJson( + notificationJson, + type, + isRead: isRead, + createdAt: createdAt, + ), + NotificationType.consultantConfirmPlanSubscriptionToAuthor => ConsultantConfirmPlanSubscriptionToAuthorNotification.fromJson( + notificationJson, + type, + isRead: isRead, + createdAt: createdAt, + ), + NotificationType.clientPaidPlanSubscriptionToSubscriber => ClientPaidPlanSubscriptionToSubscriberNotification.fromJson( + notificationJson, + type, + isRead: isRead, + createdAt: createdAt, + ), + NotificationType.clientStartPlanSubscriptionToAuthor => ClientStartPlanSubscriptionToAuthorNotification.fromJson( + notificationJson, + type, + isRead: isRead, + createdAt: createdAt, + ), + NotificationType.clientCancelPlanSubscriptionToAuthor => ClientCancelPlanSubscriptionToAuthorNotification.fromJson( + notificationJson, + type, + isRead: isRead, + createdAt: createdAt, + ), + NotificationType.clientCancelPlanSubscriptionToSubscriber => ClientCancelPlanSubscriptionToSubscriberNotification.fromJson( + notificationJson, + type, + isRead: isRead, + createdAt: createdAt, + ), + NotificationType.userDailyReportCompleted => UserDailyReportCompletedNotification.fromJson( + notificationJson, + type, + isRead: isRead, + createdAt: createdAt, + ), + NotificationType.chosenPlanFinishedToConsultant => ChosenPlanFinishedToConsultantNotification.fromJson( + notificationJson, + type, + isRead: isRead, + createdAt: createdAt, + ), + NotificationType.stayOnTrackToCustomer => StayOnTrackToCustomerNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt), + NotificationType.phoneChanged => PhoneChangedNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt), + NotificationType.subscription => SubscriptionNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt), + NotificationType.badgeAwarded => BadgeAwardedNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt), + NotificationType.badgeRevoked => BadgeRevokedNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt), + NotificationType.consultantRequestStarted => ConsultantRequestStartedNotification.fromJson( + notificationJson, + type, + isRead: isRead, + createdAt: createdAt, + ), + NotificationType.clientCloseComplaintSuccess => ClientCloseComplaintSuccessNotification.fromJson( + notificationJson, + type, + isRead: isRead, + createdAt: createdAt, + ), + NotificationType.clientRefundSuccess => ClientRefundSuccessNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt), + NotificationType.consultantRefundSuccess => ConsultantRefundSuccessNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt), + NotificationType.clientRejectSuccess => ClientRejectSuccessNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt), + NotificationType.consultantRejectSuccess => ConsultantRejectSuccessNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt), + NotificationType.scoreSubstracted => ScoreSubstractedNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt), + NotificationType.productStatusUpdatedForAuthor => ProductStatusUpdatedForAuthorNotification.fromJson( + notificationJson, + type, + isRead: isRead, + createdAt: createdAt, + ), + NotificationType.diaryReminderForUsers => DiaryReminderForUsersNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt), NotificationType.unknown => UnknownNotification(type: type), }; } catch (e) { @@ -132,8 +270,36 @@ class NotificationsCubit extends Cubit { void _handleNotification(String notificationData) { final json = jsonDecode(notificationData) as Map; - final stringifiedType = json['notification_class_name']; - final type = NotificationType.getTypeFromString(stringifiedType); + + // Улучшенная логика определения типа уведомления + String? stringifiedType; + + // Сначала проверяем поле 'type' в корне JSON + if (json.containsKey('type') && json['type'] != null) { + stringifiedType = json['type'].toString(); + log('Found type in root: $stringifiedType'); + } + // Затем проверяем поле 'notification_class_name' в корне JSON + else if (json.containsKey('notification_class_name') && json['notification_class_name'] != null) { + stringifiedType = json['notification_class_name'].toString(); + log('Found notification_class_name in root: $stringifiedType'); + } + // Если есть поле 'data', проверяем внутри него + else if (json.containsKey('data') && json['data'] is Map) { + final data = json['data'] as Map; + if (data.containsKey('type') && data['type'] != null) { + stringifiedType = data['type'].toString(); + log('Found type in data: $stringifiedType'); + } else if (data.containsKey('notification_class_name') && data['notification_class_name'] != null) { + stringifiedType = data['notification_class_name'].toString(); + log('Found notification_class_name in data: $stringifiedType'); + } + } + + // Если тип не найден, используем unknown + final type = stringifiedType != null ? NotificationType.getTypeFromString(stringifiedType) : NotificationType.unknown; + log('Final type: $type, stringifiedType: $stringifiedType'); + final notification = _parseNotification(json, type); _notifications.insert(0, notification); diff --git a/lib/features/notifications/presentation/widgets/notification_widget.dart b/lib/features/notifications/presentation/widgets/notification_widget.dart index eff047da..ed196b00 100644 --- a/lib/features/notifications/presentation/widgets/notification_widget.dart +++ b/lib/features/notifications/presentation/widgets/notification_widget.dart @@ -57,6 +57,62 @@ class NotificationWidget extends StatelessWidget { GoalAchievedNotification() => _GoalAchievedWidget(notification: notification as GoalAchievedNotification), WorkoutReminderNotification() => _WorkoutReminderWidget(notification: notification as WorkoutReminderNotification), MealReminderNotification() => _MealReminderWidget(notification: notification as MealReminderNotification), + // New notification types + DietCancelledNotification() => _DietCancelledWidget(notification: notification as DietCancelledNotification), + DietStartedNotification() => _DietStartedWidget(notification: notification as DietStartedNotification), + DietExpiredNotification() => _DietExpiredWidget(notification: notification as DietExpiredNotification), + DietWillStartedInNotification() => _DietWillStartedInWidget(notification: notification as DietWillStartedInNotification), + DietCanNotBeStartedNotification() => _DietCanNotBeStartedWidget(notification: notification as DietCanNotBeStartedNotification), + PersonalDietCreationNeededNotification() => _PersonalDietCreationNeededWidget(notification: notification as PersonalDietCreationNeededNotification), + ChosenPlanClientLastDayFinishedNotification() => _ChosenPlanClientLastDayFinishedWidget( + notification: notification as ChosenPlanClientLastDayFinishedNotification, + ), + ChosenPlanClientLastDayStartedNotification() => _ChosenPlanClientLastDayStartedWidget( + notification: notification as ChosenPlanClientLastDayStartedNotification, + ), + StayOnTrackToAuthorNotification() => _StayOnTrackToAuthorWidget(notification: notification as StayOnTrackToAuthorNotification), + ClientStartPlanSubscriptionToSubscriberNotification() => _ClientStartPlanSubscriptionToSubscriberWidget( + notification: notification as ClientStartPlanSubscriptionToSubscriberNotification, + ), + ConsultantConfirmPlanSubscriptionToSubscriberNotification() => _ConsultantConfirmPlanSubscriptionToSubscriberWidget( + notification: notification as ConsultantConfirmPlanSubscriptionToSubscriberNotification, + ), + ClientPaidPlanSubscriptionToAuthorNotification() => _ClientPaidPlanSubscriptionToAuthorWidget( + notification: notification as ClientPaidPlanSubscriptionToAuthorNotification, + ), + ConsultantLeftDiaryCommentNotification() => _ConsultantLeftDiaryCommentWidget(notification: notification as ConsultantLeftDiaryCommentNotification), + ConsultantConfirmPlanSubscriptionToAuthorNotification() => _ConsultantConfirmPlanSubscriptionToAuthorWidget( + notification: notification as ConsultantConfirmPlanSubscriptionToAuthorNotification, + ), + ClientPaidPlanSubscriptionToSubscriberNotification() => _ClientPaidPlanSubscriptionToSubscriberWidget( + notification: notification as ClientPaidPlanSubscriptionToSubscriberNotification, + ), + ClientStartPlanSubscriptionToAuthorNotification() => _ClientStartPlanSubscriptionToAuthorWidget( + notification: notification as ClientStartPlanSubscriptionToAuthorNotification, + ), + ClientCancelPlanSubscriptionToAuthorNotification() => _ClientCancelPlanSubscriptionToAuthorWidget( + notification: notification as ClientCancelPlanSubscriptionToAuthorNotification, + ), + ClientCancelPlanSubscriptionToSubscriberNotification() => _ClientCancelPlanSubscriptionToSubscriberWidget( + notification: notification as ClientCancelPlanSubscriptionToSubscriberNotification, + ), + UserDailyReportCompletedNotification() => _UserDailyReportCompletedWidget(notification: notification as UserDailyReportCompletedNotification), + StayOnTrackToCustomerNotification() => _StayOnTrackToCustomerWidget(notification: notification as StayOnTrackToCustomerNotification), + PhoneChangedNotification() => _PhoneChangedWidget(notification: notification as PhoneChangedNotification), + SubscriptionNotification() => _SubscriptionWidget(notification: notification as SubscriptionNotification), + BadgeAwardedNotification() => _BadgeAwardedWidget(notification: notification as BadgeAwardedNotification), + BadgeRevokedNotification() => _BadgeRevokedWidget(notification: notification as BadgeRevokedNotification), + ConsultantRequestStartedNotification() => _ConsultantRequestStartedWidget(notification: notification as ConsultantRequestStartedNotification), + ClientCloseComplaintSuccessNotification() => _ClientCloseComplaintSuccessWidget(notification: notification as ClientCloseComplaintSuccessNotification), + ClientRefundSuccessNotification() => _ClientRefundSuccessWidget(notification: notification as ClientRefundSuccessNotification), + ConsultantRefundSuccessNotification() => _ConsultantRefundSuccessWidget(notification: notification as ConsultantRefundSuccessNotification), + ClientRejectSuccessNotification() => _ClientRejectSuccessWidget(notification: notification as ClientRejectSuccessNotification), + ConsultantRejectSuccessNotification() => _ConsultantRejectSuccessWidget(notification: notification as ConsultantRejectSuccessNotification), + ScoreSubstractedNotification() => _ScoreSubstractedWidget(notification: notification as ScoreSubstractedNotification), + ProductStatusUpdatedForAuthorNotification() => _ProductStatusUpdatedForAuthorWidget( + notification: notification as ProductStatusUpdatedForAuthorNotification, + ), + DiaryReminderForUsersNotification() => _DiaryReminderForUsersWidget(notification: notification as DiaryReminderForUsersNotification), UnknownNotification() => _UnknownNotificationWidget(notification: notification as UnknownNotification), }; } @@ -571,3 +627,429 @@ class _MealReminderWidget extends StatelessWidget { ); } } + +// New notification widget classes +class _DietCancelledWidget extends StatelessWidget { + const _DietCancelledWidget({required this.notification}); + final DietCancelledNotification notification; + @override + Widget build(BuildContext context) { + return _BaseNotificationWidget( + isRead: notification.isRead, + notificationsText: 'Диета "${notification.dietName}" отменена', + createdAt: notification.createdAt, + ); + } +} + +class _DietStartedWidget extends StatelessWidget { + const _DietStartedWidget({required this.notification}); + final DietStartedNotification notification; + @override + Widget build(BuildContext context) { + return _BaseNotificationWidget( + isRead: notification.isRead, + notificationsText: 'Диета "${notification.dietName}" началась', + createdAt: notification.createdAt, + ); + } +} + +class _DietExpiredWidget extends StatelessWidget { + const _DietExpiredWidget({required this.notification}); + final DietExpiredNotification notification; + @override + Widget build(BuildContext context) { + return _BaseNotificationWidget( + isRead: notification.isRead, + notificationsText: 'Диета "${notification.dietName}" истекла', + createdAt: notification.createdAt, + ); + } +} + +class _DietWillStartedInWidget extends StatelessWidget { + const _DietWillStartedInWidget({required this.notification}); + final DietWillStartedInNotification notification; + @override + Widget build(BuildContext context) { + return _BaseNotificationWidget( + isRead: notification.isRead, + notificationsText: 'Диета "${notification.dietName}" начнется ${notification.startDate}', + createdAt: notification.createdAt, + ); + } +} + +class _DietCanNotBeStartedWidget extends StatelessWidget { + const _DietCanNotBeStartedWidget({required this.notification}); + final DietCanNotBeStartedNotification notification; + @override + Widget build(BuildContext context) { + return _BaseNotificationWidget( + isRead: notification.isRead, + notificationsText: 'Диета "${notification.dietName}" не может быть начата: ${notification.reason}', + createdAt: notification.createdAt, + ); + } +} + +class _PersonalDietCreationNeededWidget extends StatelessWidget { + const _PersonalDietCreationNeededWidget({required this.notification}); + final PersonalDietCreationNeededNotification notification; + @override + Widget build(BuildContext context) { + return _BaseNotificationWidget( + isRead: notification.isRead, + notificationsText: 'Требуется создание персональной диеты для клиента ${notification.clientName} по плану "${notification.planName}"', + createdAt: notification.createdAt, + ); + } +} + +class _ChosenPlanClientLastDayFinishedWidget extends StatelessWidget { + const _ChosenPlanClientLastDayFinishedWidget({required this.notification}); + final ChosenPlanClientLastDayFinishedNotification notification; + @override + Widget build(BuildContext context) { + return _BaseNotificationWidget( + isRead: notification.isRead, + notificationsText: 'Последний день плана "${notification.planName}" завершен', + createdAt: notification.createdAt, + ); + } +} + +class _ChosenPlanClientLastDayStartedWidget extends StatelessWidget { + const _ChosenPlanClientLastDayStartedWidget({required this.notification}); + final ChosenPlanClientLastDayStartedNotification notification; + @override + Widget build(BuildContext context) { + return _BaseNotificationWidget( + isRead: notification.isRead, + notificationsText: 'Последний день плана "${notification.planName}" начался', + createdAt: notification.createdAt, + ); + } +} + +class _StayOnTrackToAuthorWidget extends StatelessWidget { + const _StayOnTrackToAuthorWidget({required this.notification}); + final StayOnTrackToAuthorNotification notification; + @override + Widget build(BuildContext context) { + return _BaseNotificationWidget( + isRead: notification.isRead, + notificationsText: 'Клиент ${notification.clientName} остается на плане "${notification.planName}"', + createdAt: notification.createdAt, + ); + } +} + +class _ClientStartPlanSubscriptionToSubscriberWidget extends StatelessWidget { + const _ClientStartPlanSubscriptionToSubscriberWidget({required this.notification}); + final ClientStartPlanSubscriptionToSubscriberNotification notification; + @override + Widget build(BuildContext context) { + return _BaseNotificationWidget( + isRead: notification.isRead, + notificationsText: 'Подписка на план "${notification.planName}" началась', + createdAt: notification.createdAt, + ); + } +} + +class _ConsultantConfirmPlanSubscriptionToSubscriberWidget extends StatelessWidget { + const _ConsultantConfirmPlanSubscriptionToSubscriberWidget({required this.notification}); + final ConsultantConfirmPlanSubscriptionToSubscriberNotification notification; + @override + Widget build(BuildContext context) { + return _BaseNotificationWidget( + isRead: notification.isRead, + notificationsText: 'Консультант ${notification.consultantName} подтвердил подписку на план "${notification.planName}"', + createdAt: notification.createdAt, + ); + } +} + +class _ClientPaidPlanSubscriptionToAuthorWidget extends StatelessWidget { + const _ClientPaidPlanSubscriptionToAuthorWidget({required this.notification}); + final ClientPaidPlanSubscriptionToAuthorNotification notification; + @override + Widget build(BuildContext context) { + return _BaseNotificationWidget( + isRead: notification.isRead, + notificationsText: 'Клиент ${notification.clientName} оплатил подписку на план "${notification.planName}" на сумму ${notification.amount}', + createdAt: notification.createdAt, + ); + } +} + +class _ConsultantLeftDiaryCommentWidget extends StatelessWidget { + const _ConsultantLeftDiaryCommentWidget({required this.notification}); + final ConsultantLeftDiaryCommentNotification notification; + @override + Widget build(BuildContext context) { + return _BaseNotificationWidget( + isRead: notification.isRead, + notificationsText: 'Консультант ${notification.consultantName} оставил комментарий к дневнику по плану "${notification.planName}"', + createdAt: notification.createdAt, + ); + } +} + +class _ConsultantConfirmPlanSubscriptionToAuthorWidget extends StatelessWidget { + const _ConsultantConfirmPlanSubscriptionToAuthorWidget({required this.notification}); + final ConsultantConfirmPlanSubscriptionToAuthorNotification notification; + @override + Widget build(BuildContext context) { + return _BaseNotificationWidget( + isRead: notification.isRead, + notificationsText: 'Консультант ${notification.consultantName} подтвердил подписку на план "${notification.planName}"', + createdAt: notification.createdAt, + ); + } +} + +class _ClientPaidPlanSubscriptionToSubscriberWidget extends StatelessWidget { + const _ClientPaidPlanSubscriptionToSubscriberWidget({required this.notification}); + final ClientPaidPlanSubscriptionToSubscriberNotification notification; + @override + Widget build(BuildContext context) { + return _BaseNotificationWidget( + isRead: notification.isRead, + notificationsText: 'Оплачена подписка на план "${notification.planName}" на сумму ${notification.amount}', + createdAt: notification.createdAt, + ); + } +} + +class _ClientStartPlanSubscriptionToAuthorWidget extends StatelessWidget { + const _ClientStartPlanSubscriptionToAuthorWidget({required this.notification}); + final ClientStartPlanSubscriptionToAuthorNotification notification; + @override + Widget build(BuildContext context) { + return _BaseNotificationWidget( + isRead: notification.isRead, + notificationsText: 'Клиент ${notification.clientName} начал подписку на план "${notification.planName}"', + createdAt: notification.createdAt, + ); + } +} + +class _ClientCancelPlanSubscriptionToAuthorWidget extends StatelessWidget { + const _ClientCancelPlanSubscriptionToAuthorWidget({required this.notification}); + final ClientCancelPlanSubscriptionToAuthorNotification notification; + @override + Widget build(BuildContext context) { + return _BaseNotificationWidget( + isRead: notification.isRead, + notificationsText: 'Клиент ${notification.clientName} отменил подписку на план "${notification.planName}"', + createdAt: notification.createdAt, + ); + } +} + +class _ClientCancelPlanSubscriptionToSubscriberWidget extends StatelessWidget { + const _ClientCancelPlanSubscriptionToSubscriberWidget({required this.notification}); + final ClientCancelPlanSubscriptionToSubscriberNotification notification; + @override + Widget build(BuildContext context) { + return _BaseNotificationWidget( + isRead: notification.isRead, + notificationsText: 'Подписка на план "${notification.planName}" отменена', + createdAt: notification.createdAt, + ); + } +} + +class _UserDailyReportCompletedWidget extends StatelessWidget { + const _UserDailyReportCompletedWidget({required this.notification}); + final UserDailyReportCompletedNotification notification; + @override + Widget build(BuildContext context) { + return _BaseNotificationWidget( + isRead: notification.isRead, + notificationsText: 'Клиент ${notification.clientName} завершил ежедневный отчет по плану "${notification.planName}"', + createdAt: notification.createdAt, + ); + } +} + +class _StayOnTrackToCustomerWidget extends StatelessWidget { + const _StayOnTrackToCustomerWidget({required this.notification}); + final StayOnTrackToCustomerNotification notification; + @override + Widget build(BuildContext context) { + return _BaseNotificationWidget( + isRead: notification.isRead, + notificationsText: 'Консультант ${notification.consultantName} остается на плане "${notification.planName}"', + createdAt: notification.createdAt, + ); + } +} + +class _PhoneChangedWidget extends StatelessWidget { + const _PhoneChangedWidget({required this.notification}); + final PhoneChangedNotification notification; + @override + Widget build(BuildContext context) { + return _BaseNotificationWidget( + isRead: notification.isRead, + notificationsText: 'Номер телефона изменен с ${notification.oldPhone} на ${notification.newPhone}', + createdAt: notification.createdAt, + ); + } +} + +class _SubscriptionWidget extends StatelessWidget { + const _SubscriptionWidget({required this.notification}); + final SubscriptionNotification notification; + @override + Widget build(BuildContext context) { + return _BaseNotificationWidget( + isRead: notification.isRead, + notificationsText: 'Подписка ${notification.subscriptionType} на план "${notification.planName}"', + createdAt: notification.createdAt, + ); + } +} + +class _BadgeAwardedWidget extends StatelessWidget { + const _BadgeAwardedWidget({required this.notification}); + final BadgeAwardedNotification notification; + @override + Widget build(BuildContext context) { + return _BaseNotificationWidget( + isRead: notification.isRead, + notificationsText: 'Получен значок "${notification.badgeName}": ${notification.badgeDescription}', + createdAt: notification.createdAt, + ); + } +} + +class _BadgeRevokedWidget extends StatelessWidget { + const _BadgeRevokedWidget({required this.notification}); + final BadgeRevokedNotification notification; + @override + Widget build(BuildContext context) { + return _BaseNotificationWidget( + isRead: notification.isRead, + notificationsText: 'Отозван значок "${notification.badgeName}": ${notification.reason}', + createdAt: notification.createdAt, + ); + } +} + +class _ConsultantRequestStartedWidget extends StatelessWidget { + const _ConsultantRequestStartedWidget({required this.notification}); + final ConsultantRequestStartedNotification notification; + @override + Widget build(BuildContext context) { + return _BaseNotificationWidget( + isRead: notification.isRead, + notificationsText: 'Запрос к консультанту ${notification.consultantName} начат', + createdAt: notification.createdAt, + ); + } +} + +class _ClientCloseComplaintSuccessWidget extends StatelessWidget { + const _ClientCloseComplaintSuccessWidget({required this.notification}); + final ClientCloseComplaintSuccessNotification notification; + @override + Widget build(BuildContext context) { + return _BaseNotificationWidget( + isRead: notification.isRead, + notificationsText: 'Жалоба на ${notification.complaintType} "${notification.entityName}" успешно закрыта', + createdAt: notification.createdAt, + ); + } +} + +class _ClientRefundSuccessWidget extends StatelessWidget { + const _ClientRefundSuccessWidget({required this.notification}); + final ClientRefundSuccessNotification notification; + @override + Widget build(BuildContext context) { + return _BaseNotificationWidget( + isRead: notification.isRead, + notificationsText: 'Возврат средств по плану "${notification.planName}" на сумму ${notification.amount}: ${notification.reason}', + createdAt: notification.createdAt, + ); + } +} + +class _ConsultantRefundSuccessWidget extends StatelessWidget { + const _ConsultantRefundSuccessWidget({required this.notification}); + final ConsultantRefundSuccessNotification notification; + @override + Widget build(BuildContext context) { + return _BaseNotificationWidget( + isRead: notification.isRead, + notificationsText: 'Возврат средств клиенту ${notification.clientName} по плану "${notification.planName}" на сумму ${notification.amount}', + createdAt: notification.createdAt, + ); + } +} + +class _ClientRejectSuccessWidget extends StatelessWidget { + const _ClientRejectSuccessWidget({required this.notification}); + final ClientRejectSuccessNotification notification; + @override + Widget build(BuildContext context) { + return _BaseNotificationWidget( + isRead: notification.isRead, + notificationsText: 'Отклонение по плану "${notification.planName}": ${notification.reason}', + createdAt: notification.createdAt, + ); + } +} + +class _ConsultantRejectSuccessWidget extends StatelessWidget { + const _ConsultantRejectSuccessWidget({required this.notification}); + final ConsultantRejectSuccessNotification notification; + @override + Widget build(BuildContext context) { + return _BaseNotificationWidget( + isRead: notification.isRead, + notificationsText: 'Отклонение клиентом ${notification.clientName} по плану "${notification.planName}": ${notification.reason}', + createdAt: notification.createdAt, + ); + } +} + +class _ScoreSubstractedWidget extends StatelessWidget { + const _ScoreSubstractedWidget({required this.notification}); + final ScoreSubstractedNotification notification; + @override + Widget build(BuildContext context) { + return _BaseNotificationWidget( + isRead: notification.isRead, + notificationsText: 'Вычтено ${notification.scoreValue} баллов за ${notification.scorableName}', + createdAt: notification.createdAt, + ); + } +} + +class _ProductStatusUpdatedForAuthorWidget extends StatelessWidget { + const _ProductStatusUpdatedForAuthorWidget({required this.notification}); + final ProductStatusUpdatedForAuthorNotification notification; + @override + Widget build(BuildContext context) { + return _BaseNotificationWidget( + isRead: notification.isRead, + notificationsText: 'Статус продукта "${notification.productName}" изменен с ${notification.oldStatus} на ${notification.newStatus}', + createdAt: notification.createdAt, + ); + } +} + +class _DiaryReminderForUsersWidget extends StatelessWidget { + const _DiaryReminderForUsersWidget({required this.notification}); + final DiaryReminderForUsersNotification notification; + @override + Widget build(BuildContext context) { + return _BaseNotificationWidget(isRead: notification.isRead, notificationsText: 'Напоминание о заполнении дневника', createdAt: notification.createdAt); + } +} diff --git a/lib/features/notifications/service/notifications_service.dart b/lib/features/notifications/service/notifications_service.dart index 7fdcb4fe..5b3ca78b 100644 --- a/lib/features/notifications/service/notifications_service.dart +++ b/lib/features/notifications/service/notifications_service.dart @@ -13,10 +13,30 @@ class NotificationsService { final notifications = data.map((notificationJson) { try { - final stringifiedType = notificationJson['data']['notification_class_name']; + // Улучшенная логика определения типа уведомления + String? stringifiedType; + + // Сначала проверяем поле 'type' в корне JSON + if (notificationJson.containsKey('type') && notificationJson['type'] != null) { + stringifiedType = notificationJson['type'].toString(); + } + // Затем проверяем поле 'notification_class_name' в корне JSON + else if (notificationJson.containsKey('notification_class_name') && notificationJson['notification_class_name'] != null) { + stringifiedType = notificationJson['notification_class_name'].toString(); + } + // Если есть поле 'data', проверяем внутри него + else if (notificationJson.containsKey('data') && notificationJson['data'] is Map) { + final data = notificationJson['data'] as Map; + if (data.containsKey('type') && data['type'] != null) { + stringifiedType = data['type'].toString(); + } else if (data.containsKey('notification_class_name') && data['notification_class_name'] != null) { + stringifiedType = data['notification_class_name'].toString(); + } + } + final readDate = notificationJson['read_at']; final createdAt = notificationJson['created_at']; - final type = NotificationType.getTypeFromString(stringifiedType); + final type = stringifiedType != null ? NotificationType.getTypeFromString(stringifiedType) : NotificationType.unknown; return switch (type) { NotificationType.planSubscriptionStartedToAuthor => PlanSubscriptionStartedToAuthorNotification.fromJson( @@ -174,6 +194,219 @@ class NotificationsService { isRead: readDate != null, createdAt: createdAt, ), + // New notification types + NotificationType.dietCancelled => DietCancelledNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.dietStarted => DietStartedNotification.fromJson(notificationJson['data'], type, isRead: readDate != null, createdAt: createdAt), + NotificationType.dietExpired => DietExpiredNotification.fromJson(notificationJson['data'], type, isRead: readDate != null, createdAt: createdAt), + NotificationType.dietWillStartedIn => DietWillStartedInNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.dietCanNotBeStarted => DietCanNotBeStartedNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.personalDietCreationNeeded => PersonalDietCreationNeededNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.chosenPlanClientLastDayFinished => ChosenPlanClientLastDayFinishedNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.chosenPlanClientLastDayStarted => ChosenPlanClientLastDayStartedNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.stayOnTrackToAuthor => StayOnTrackToAuthorNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.chosenPlanStartedToClient => ChosenPlanStartedToClientNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.clientStartPlanSubscriptionToSubscriber => ClientStartPlanSubscriptionToSubscriberNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.consultantConfirmPlanSubscriptionToSubscriber => ConsultantConfirmPlanSubscriptionToSubscriberNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.clientPaidPlanSubscriptionToAuthor => ClientPaidPlanSubscriptionToAuthorNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.chosenPlanTomorrowReminderToClient => ChosenPlanTomorrowReminderToClientNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.chosenPlanTomorrowReminderToConsultant => ChosenPlanTomorrowReminderToConsultantNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.chosenPlanStartedToConsultant => ChosenPlanStartedToConsultantNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.consultantLeftDiaryComment => ConsultantLeftDiaryCommentNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.consultantConfirmPlanSubscriptionToAuthor => ConsultantConfirmPlanSubscriptionToAuthorNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.clientPaidPlanSubscriptionToSubscriber => ClientPaidPlanSubscriptionToSubscriberNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.clientStartPlanSubscriptionToAuthor => ClientStartPlanSubscriptionToAuthorNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.clientCancelPlanSubscriptionToAuthor => ClientCancelPlanSubscriptionToAuthorNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.clientCancelPlanSubscriptionToSubscriber => ClientCancelPlanSubscriptionToSubscriberNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.userDailyReportCompleted => UserDailyReportCompletedNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.stayOnTrackToCustomer => StayOnTrackToCustomerNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.phoneChanged => PhoneChangedNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.subscription => SubscriptionNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.badgeAwarded => BadgeAwardedNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.badgeRevoked => BadgeRevokedNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.consultantRequestStarted => ConsultantRequestStartedNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.clientCloseComplaintSuccess => ClientCloseComplaintSuccessNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.clientRefundSuccess => ClientRefundSuccessNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.consultantRefundSuccess => ConsultantRefundSuccessNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.clientRejectSuccess => ClientRejectSuccessNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.consultantRejectSuccess => ConsultantRejectSuccessNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.scoreSubstracted => ScoreSubstractedNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.productStatusUpdatedForAuthor => ProductStatusUpdatedForAuthorNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.diaryReminderForUsers => DiaryReminderForUsersNotification.fromJson( + notificationJson, + type, + isRead: readDate != null, + createdAt: createdAt, + ), NotificationType.unknown => UnknownNotification(type: type, createdAt: parseDate(createdAt)), }; } catch (e) { From ccf586234b8effccb41a4047545146af719b55e3 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Fri, 15 Aug 2025 14:13:30 +0300 Subject: [PATCH 105/134] fix: user charts in consultations --- .../repositories/profile_repository.dart | 55 +++++-------------- .../profile/services/profile_service.dart | 2 +- 2 files changed, 15 insertions(+), 42 deletions(-) diff --git a/lib/features/profile/repositories/profile_repository.dart b/lib/features/profile/repositories/profile_repository.dart index 05399457..531cf589 100644 --- a/lib/features/profile/repositories/profile_repository.dart +++ b/lib/features/profile/repositories/profile_repository.dart @@ -12,9 +12,7 @@ import 'package:eatvio_app/features/profile/services/profile_service.dart'; class ProfileRepository { final ProfileService profileService; - ProfileRepository({ - required this.profileService, - }); + ProfileRepository({required this.profileService}); Future getProfileBySlug(String slug) async { try { @@ -48,11 +46,7 @@ class ProfileRepository { } } - 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 { return (await profileService.addCircumference( chestCircumference: chestCircumference, @@ -64,9 +58,7 @@ class ProfileRepository { } } - Future addWeight({ - required double weight, - }) async { + Future addWeight({required double weight}) async { try { return (await profileService.addWeight(weight: weight)); } catch (e) { @@ -74,52 +66,32 @@ class ProfileRepository { } } - Future> getBlogPostsByAuthorSlug({ - required String slug, - }) async { + Future> getBlogPostsByAuthorSlug({required String slug}) async { try { - return (await profileService.getBlogPostByAuthorSlug( - slug: slug, - )); + return (await profileService.getBlogPostByAuthorSlug(slug: slug)); } catch (e) { rethrow; } } - Future> getWeight({ - required String period, - String? slug, - }) async { + Future> getWeight({required String period, String? slug}) async { try { if (slug != null) { - return (await profileService.getWeightBySlug( - period: period, - slug: slug, - )); + return (await profileService.getWeightBySlug(period: period, slug: slug)); } else { - return (await profileService.getWeight( - period: period, - )); + return (await profileService.getWeight(period: period)); } } catch (e) { rethrow; } } - Future> getCircumferences({ - required String period, - String? slug, - }) async { + Future> getCircumferences({required String period, String? slug}) async { try { if (slug != null) { - return (await profileService.getCircumferencesBySlug( - period: period, - slug: slug, - )); + return (await profileService.getCircumferencesBySlug(period: period, slug: slug)); } else { - return (await profileService.getCircumferences( - period: period, - )); + return (await profileService.getCircumferences(period: period)); } } catch (e) { rethrow; @@ -184,7 +156,8 @@ 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']!; + (await profileService.getPassKey(json: '{"route_name": "user_detailz.change-phone-verify", "route_params": {"phone": "$phone"}}')), + ))['key_is']!; } Future getMyPhone() async { @@ -206,7 +179,7 @@ class ProfileRepository { Future deleteAccount() async { try { - await profileService.deleteAccount(); + await profileService.deleteAccount(); } catch (e) { rethrow; } diff --git a/lib/features/profile/services/profile_service.dart b/lib/features/profile/services/profile_service.dart index a1208526..bddd46f8 100644 --- a/lib/features/profile/services/profile_service.dart +++ b/lib/features/profile/services/profile_service.dart @@ -64,7 +64,7 @@ abstract class ProfileService { @Path() required String period, }); - @GET('/api/profile/measure/circumferences/{period}/{user}') + @GET('/api/profile/measure/circumferences/{period}/{slug}') Future> getCircumferencesBySlug({ @Path() required String period, @Path() required String slug, From 442da406223100e7763da6b481da8f1551021f47 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Tue, 19 Aug 2025 14:24:14 +0300 Subject: [PATCH 106/134] fix: token storage --- .gitea/workflows/test.yml | 2 +- .gitignore | 4 +- android/app/build.gradle | 2 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- android/settings.gradle | 6 +-- lib/core/const.dart | 2 +- lib/core/credentials_storage.dart | 21 ++++----- lib/core/locator_service.dart | 4 +- lib/core/token_storage.dart | 23 +++++----- pubspec.yaml | 45 +++++++++---------- 10 files changed, 53 insertions(+), 58 deletions(-) diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml index e6a74c64..d209778b 100644 --- a/.gitea/workflows/test.yml +++ b/.gitea/workflows/test.yml @@ -3,7 +3,7 @@ name: Eatvio App Android Test on: [ pull_request ] env: - flutter_version: "3.32.8" + flutter_version: "3.35.1" java_version: "21.x" flutter_channel: "stable" my_local_cache_dir: "/opt/hostedtoolcache/" diff --git a/.gitignore b/.gitignore index 6960fad1..4f05e3a1 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,4 @@ *.mocks.dart android/build/ ios/XCBuildData/ -.env -env.example -env/.env +env/.env \ No newline at end of file diff --git a/android/app/build.gradle b/android/app/build.gradle index c9fe6d27..9ffa87c4 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -29,7 +29,7 @@ android { defaultConfig { applicationId = "com.eatvioapp.app" - minSdk = 23 + minSdkVersion flutter.minSdkVersion targetSdk = flutter.targetSdkVersion versionCode = flutter.versionCode versionName = flutter.versionName diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index ac3b4792..e603bd33 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-all.zip diff --git a/android/settings.gradle b/android/settings.gradle index 29eab0d3..e173b2c9 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -18,11 +18,11 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version "8.7.3" apply false + id "com.android.application" version "8.12.0" apply false // START: FlutterFire Configuration - id "com.google.gms.google-services" version "4.4.2" apply false + id "com.google.gms.google-services" version "4.4.3" apply false // END: FlutterFire Configuration - id "org.jetbrains.kotlin.android" version "2.1.0" apply false + id "org.jetbrains.kotlin.android" version "2.2.10" apply false } include ":app" 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/credentials_storage.dart b/lib/core/credentials_storage.dart index 6e9870e5..05001744 100644 --- a/lib/core/credentials_storage.dart +++ b/lib/core/credentials_storage.dart @@ -1,9 +1,10 @@ -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:eatvio_app/core/locator_service.dart'; +import 'package:shared_preferences/shared_preferences.dart'; abstract class CredentialsStorage { - static Future haveCredentialsInStorage() async { + static bool haveCredentialsInStorage() { try { - if (await const FlutterSecureStorage().read(key: 'email') == null || await const FlutterSecureStorage().read(key: 'password') == null) { + if (sl().getString('email') == null || sl().getString('password') == null) { return false; } else { return true; @@ -13,20 +14,20 @@ abstract class CredentialsStorage { } } - static Future> getCredentialsFromStorage() async { + static Map getCredentialsFromStorage() { return { - 'email': (await const FlutterSecureStorage().read(key: 'email'))!, - 'password': (await const FlutterSecureStorage().read(key: 'password'))!, + 'email': (sl().getString('email'))!, + 'password': (sl().getString('password'))!, }; } static Future saveCredentials(String email, String password) async { - await const FlutterSecureStorage().write(key: 'email', value: email); - await const FlutterSecureStorage().write(key: 'password', value: password); + await sl().setString('email', email); + await sl().setString('password', password); } static Future removeCredentials() async { - await const FlutterSecureStorage().delete(key: 'email'); - await const FlutterSecureStorage().delete(key: 'password'); + sl().remove('email'); + sl().remove('password'); } } diff --git a/lib/core/locator_service.dart b/lib/core/locator_service.dart index 68208bc5..54ed3a9d 100644 --- a/lib/core/locator_service.dart +++ b/lib/core/locator_service.dart @@ -107,9 +107,9 @@ import 'package:eatvio_app/features/settings/presentation/bloc/settings_cubit/se 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:flutter_secure_storage/flutter_secure_storage.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; @@ -252,5 +252,5 @@ Future init() async { ], ), ); - sl.registerLazySingleton(() async => const FlutterSecureStorage()); + sl.registerLazySingleton(() async => SharedPreferences.getInstance()); } diff --git a/lib/core/token_storage.dart b/lib/core/token_storage.dart index 30906264..fec596c5 100644 --- a/lib/core/token_storage.dart +++ b/lib/core/token_storage.dart @@ -1,9 +1,10 @@ -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'locator_service.dart'; abstract class TokenStorage { - static Future haveTokenInStorage() async { + static bool haveTokenInStorage() { try { - if (await const FlutterSecureStorage().read(key: 'token') == null) { + if (sl().getString('token') == null) { return false; } else { return true; @@ -13,19 +14,19 @@ abstract class TokenStorage { } } - static Future getTokenFromStorage() async => await const FlutterSecureStorage().read(key: 'token'); + static String? getTokenFromStorage() => sl().getString('token'); static Future saveTokenInStorage(String token) async { - await const FlutterSecureStorage().write(key: 'token', value: token); + await sl().setString('token', token); } - static Future removeTokenInStorage() async => (await const FlutterSecureStorage().delete(key: 'token')); + static Future removeTokenInStorage() async => (await sl().remove('token')); } abstract class SocketIdStorage { - static Future haveSocketIdInStorage() async { + static bool haveSocketIdInStorage() { try { - if (await const FlutterSecureStorage().read(key: 'socketId') == null) { + if (sl().getString('socketId') == null) { return false; } else { return true; @@ -35,11 +36,11 @@ abstract class SocketIdStorage { } } - static Future getSocketIdFromStorage() async => await const FlutterSecureStorage().read(key: 'socketId'); + static String? getSocketIdFromStorage() => sl().getString('socketId'); static Future saveSocketIdInStorage(String socketId) async { - await const FlutterSecureStorage().write(key: 'socketId', value: socketId); + await sl().setString('socketId', socketId); } - static Future removeSocketIdInStorage() async => (await const FlutterSecureStorage().delete(key: 'socketId')); + static Future removeSocketIdInStorage() async => (await sl().remove('socketId')); } diff --git a/pubspec.yaml b/pubspec.yaml index d9dda30d..c8638b7e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,11 +13,6 @@ dependencies: flutter_localizations: sdk: flutter - flutter_secure_storage: - git: - url: https://github.com/sherlockvn/flutter_secure_storage.git - ref: remove-old-encrypt-algorithm - path: flutter_secure_storage flutter_bloc: ^9.1.1 cached_network_image: ^3.4.1 flutter_svg: ^2.2.0 @@ -27,28 +22,28 @@ dependencies: validators: ^3.0.0 path_provider: ^2.1.5 carousel_slider_plus: ^7.1.1 - flutter_form_builder: ^10.1.0 - image_picker: ^1.1.2 + flutter_form_builder: ^10.2.0 + image_picker: ^1.2.0 flutter_typeahead: ^5.2.0 - go_router: ^16.0.0 + go_router: ^16.1.0 keyboard_dismisser: ^3.0.0 - retrofit: ^4.7.0 - get_it: ^8.1.0 - dio: ^5.8.0+1 + retrofit: ^4.7.1 + get_it: ^8.2.0 + dio: ^5.9.0 dart_pusher_channels: ^1.2.3 photo_view: ^0.15.0 dismissible_page: ^1.0.2 url_launcher: ^6.3.2 easy_debounce: ^2.0.3 badges: ^3.1.2 - google_fonts: ^6.2.1 - syncfusion_flutter_charts: ^30.1.42 - syncfusion_flutter_gauges: ^30.1.42 + google_fonts: ^6.3.0 + syncfusion_flutter_charts: ^30.2.5 + syncfusion_flutter_gauges: ^30.2.5 table_calendar: ^3.2.0 flutter_keyboard_visibility: ^6.0.0 - share_plus: ^11.0.0 + share_plus: ^11.1.0 flutter_html: ^3.0.0 - file_picker: ^10.2.0 + file_picker: ^10.3.1 audioplayers: ^6.5.0 intl: ^0.20.2 mask_text_input_formatter: ^2.9.0 @@ -59,31 +54,31 @@ dependencies: flutter_image_compress: ^2.4.0 image: ^4.5.4 dotted_border: ^3.1.0 - http: ^1.4.0 + http: ^1.5.0 flutter_native_splash: ^2.4.6 webview_flutter: ^4.13.0 - sentry_flutter: ^9.5.0 - connectivity_plus: ^6.1.4 + sentry_flutter: ^9.6.0 + connectivity_plus: ^6.1.5 visibility_detector: ^0.4.0+2 - firebase_core: ^3.15.2 - firebase_messaging: ^15.2.10 + firebase_core: ^4.0.0 + firebase_messaging: ^16.0.0 http_parser: ^4.1.2 path: ^1.9.1 collection: ^1.19.1 shared_preferences: ^2.5.3 version: ^3.0.2 - package_info_plus: ^8.3.0 + package_info_plus: ^8.3.1 device_info_plus: ^11.5.0 permission_handler: ^12.0.1 qr_code_scanner_plus: ^2.0.10+1 - flutter_dotenv: ^5.1.0 + flutter_dotenv: ^5.2.1 uuid: ^4.5.1 dev_dependencies: - build_runner: ^2.6.0 + build_runner: ^2.7.0 freezed: ^3.2.0 json_serializable: ^6.10.0 - retrofit_generator: ^10.0.1 + retrofit_generator: ^10.0.2 mockito: ^5.5.0 flutter_test: From 9d4687553aad8362f1fbadabe8c1004e98adc2d0 Mon Sep 17 00:00:00 2001 From: Alexander Date: Wed, 20 Aug 2025 11:34:28 +0300 Subject: [PATCH 107/134] fix: analyzer and token storage --- ios/Runner.xcodeproj/project.pbxproj | 6 +- ios/Runner/Info.plist | 2 +- lib/core/credentials_storage.dart | 33 ---- lib/core/locator_service.dart | 2 - lib/core/token_storage.dart | 45 +++-- lib/core/widgets/custom_drop_down.dart | 48 ++--- lib/core/widgets/custom_drop_down_button.dart | 17 +- .../pages/registration_as_screen.dart | 161 +++++++++-------- .../presentation/pages/gender_screen.dart | 78 ++++---- .../pages/physical_activity_screen.dart | 166 ++++++++++-------- pubspec.yaml | 2 +- 11 files changed, 274 insertions(+), 286 deletions(-) delete mode 100644 lib/core/credentials_storage.dart diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index bbf607e7..e484a472 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -396,7 +396,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 68; + CURRENT_PROJECT_VERSION = 69; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -546,7 +546,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 68; + CURRENT_PROJECT_VERSION = 69; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -584,7 +584,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 68; + CURRENT_PROJECT_VERSION = 69; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index d9e83314..7e814e8a 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -21,7 +21,7 @@ CFBundleSignature ???? CFBundleVersion - 68 + 69 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/lib/core/credentials_storage.dart b/lib/core/credentials_storage.dart deleted file mode 100644 index 05001744..00000000 --- a/lib/core/credentials_storage.dart +++ /dev/null @@ -1,33 +0,0 @@ -import 'package:eatvio_app/core/locator_service.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -abstract class CredentialsStorage { - static bool haveCredentialsInStorage() { - try { - if (sl().getString('email') == null || sl().getString('password') == null) { - return false; - } else { - return true; - } - } catch (e) { - return false; - } - } - - static Map getCredentialsFromStorage() { - return { - 'email': (sl().getString('email'))!, - 'password': (sl().getString('password'))!, - }; - } - - static Future saveCredentials(String email, String password) async { - await sl().setString('email', email); - await sl().setString('password', password); - } - - static Future removeCredentials() async { - sl().remove('email'); - sl().remove('password'); - } -} diff --git a/lib/core/locator_service.dart b/lib/core/locator_service.dart index 54ed3a9d..724d8b59 100644 --- a/lib/core/locator_service.dart +++ b/lib/core/locator_service.dart @@ -109,7 +109,6 @@ 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; @@ -252,5 +251,4 @@ Future init() async { ], ), ); - sl.registerLazySingleton(() async => SharedPreferences.getInstance()); } diff --git a/lib/core/token_storage.dart b/lib/core/token_storage.dart index fec596c5..f67e919c 100644 --- a/lib/core/token_storage.dart +++ b/lib/core/token_storage.dart @@ -1,10 +1,10 @@ import 'package:shared_preferences/shared_preferences.dart'; -import 'locator_service.dart'; abstract class TokenStorage { - static bool haveTokenInStorage() { + static Future haveTokenInStorage() async { + final sharedPreferences = await SharedPreferences.getInstance(); try { - if (sl().getString('token') == null) { + if (sharedPreferences.getString('token') == null) { return false; } else { return true; @@ -14,19 +14,27 @@ abstract class TokenStorage { } } - static String? getTokenFromStorage() => sl().getString('token'); - - static Future saveTokenInStorage(String token) async { - await sl().setString('token', token); + static Future getTokenFromStorage() async { + final sharedPreferences = await SharedPreferences.getInstance(); + return sharedPreferences.getString('token'); } - static Future removeTokenInStorage() async => (await sl().remove('token')); + static Future saveTokenInStorage(String token) async { + final sharedPreferences = await SharedPreferences.getInstance(); + await sharedPreferences.setString('token', token); + } + + static Future removeTokenInStorage() async { + final sharedPreferences = await SharedPreferences.getInstance(); + await sharedPreferences.remove('token'); + } } abstract class SocketIdStorage { - static bool haveSocketIdInStorage() { + static Future haveSocketIdInStorage() async { + final sharedPreferences = await SharedPreferences.getInstance(); try { - if (sl().getString('socketId') == null) { + if (sharedPreferences.getString('socketId') == null) { return false; } else { return true; @@ -36,11 +44,18 @@ abstract class SocketIdStorage { } } - static String? getSocketIdFromStorage() => sl().getString('socketId'); - - static Future saveSocketIdInStorage(String socketId) async { - await sl().setString('socketId', socketId); + static Future getSocketIdFromStorage() async { + final sharedPreferences = await SharedPreferences.getInstance(); + return sharedPreferences.getString('socketId'); } - static Future removeSocketIdInStorage() async => (await sl().remove('socketId')); + static Future saveSocketIdInStorage(String socketId) async { + final sharedPreferences = await SharedPreferences.getInstance(); + await sharedPreferences.setString('socketId', socketId); + } + + static Future removeSocketIdInStorage() async { + final sharedPreferences = await SharedPreferences.getInstance(); + await sharedPreferences.remove('socketId'); + } } diff --git a/lib/core/widgets/custom_drop_down.dart b/lib/core/widgets/custom_drop_down.dart index aca39b44..9580afe7 100644 --- a/lib/core/widgets/custom_drop_down.dart +++ b/lib/core/widgets/custom_drop_down.dart @@ -2,7 +2,8 @@ import 'package:eatvio_app/core/data/selectionPopupModel/selection_popup_model.d import 'package:flutter/material.dart'; class CustomDropDown extends StatelessWidget { - const CustomDropDown({super.key, + const CustomDropDown({ + super.key, this.alignment, this.width, this.focusNode, @@ -48,12 +49,7 @@ class CustomDropDown extends StatelessWidget { @override Widget build(BuildContext context) { - return alignment != null - ? Align( - alignment: alignment ?? Alignment.center, - child: _buildDropDownWidget(context), - ) - : _buildDropDownWidget(context); + return alignment != null ? Align(alignment: alignment ?? Alignment.center, child: _buildDropDownWidget(context)) : _buildDropDownWidget(context); } DropdownButtonFormField _buildDropDownWidget(BuildContext context) { @@ -61,7 +57,7 @@ class CustomDropDown extends StatelessWidget { focusNode: focusNode ?? FocusNode(), icon: const Icon(Icons.keyboard_arrow_down_rounded), autofocus: autofocus!, - value: value, + initialValue: value, isDense: true, isExpanded: true, items: items?.map((SelectionPopupModel item) { @@ -69,12 +65,7 @@ class CustomDropDown extends StatelessWidget { value: item, child: Text( item.title, - style: hintStyle ?? - const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w400, - color: Colors.black, - ), + style: hintStyle ?? const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, color: Colors.black), ), ); }).toList(), @@ -90,19 +81,9 @@ class CustomDropDown extends StatelessWidget { InputDecoration _buildInputDecoration(BuildContext context) { return InputDecoration( - labelStyle: const TextStyle( - fontSize: 16, - color: Colors.black, - fontWeight: FontWeight.w400, - ), + labelStyle: const TextStyle(fontSize: 16, color: Colors.black, fontWeight: FontWeight.w400), hintText: hintText ?? "", - hintStyle: hintStyle ?? - TextStyle( - fontSize: 16, - fontWeight: FontWeight.w400, - color: Colors.grey.shade400, - height: 1, - ), + hintStyle: hintStyle ?? TextStyle(fontSize: 16, fontWeight: FontWeight.w400, color: Colors.grey.shade400, height: 1), prefixIcon: prefix, prefixIconConstraints: prefixConstraints, suffixIcon: suffix, @@ -111,18 +92,9 @@ class CustomDropDown extends StatelessWidget { contentPadding: const EdgeInsets.symmetric(vertical: 16, horizontal: 12), fillColor: const Color(0xFFF6F6F6), filled: filled, - border: const OutlineInputBorder( - borderRadius: BorderRadius.all(Radius.circular(20)), - borderSide: BorderSide.none, - ), - enabledBorder: const OutlineInputBorder( - borderRadius: BorderRadius.all(Radius.circular(20)), - borderSide: BorderSide.none, - ), - focusedBorder: const OutlineInputBorder( - borderRadius: BorderRadius.all(Radius.circular(20)), - borderSide: BorderSide.none, - ), + border: const OutlineInputBorder(borderRadius: BorderRadius.all(Radius.circular(20)), borderSide: BorderSide.none), + enabledBorder: const OutlineInputBorder(borderRadius: BorderRadius.all(Radius.circular(20)), borderSide: BorderSide.none), + focusedBorder: const OutlineInputBorder(borderRadius: BorderRadius.all(Radius.circular(20)), borderSide: BorderSide.none), ); } } diff --git a/lib/core/widgets/custom_drop_down_button.dart b/lib/core/widgets/custom_drop_down_button.dart index 714382e7..ac8f3c2b 100644 --- a/lib/core/widgets/custom_drop_down_button.dart +++ b/lib/core/widgets/custom_drop_down_button.dart @@ -3,14 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; class CustomDropDownButton extends StatelessWidget { - const CustomDropDownButton({ - super.key, - required this.items, - required this.onChanged, - this.value, - this.hint, - this.hasError = false, - }); + const CustomDropDownButton({super.key, required this.items, required this.onChanged, this.value, this.hint, this.hasError = false}); final T? value; final List> items; final void Function(T?)? onChanged; @@ -20,11 +13,7 @@ class CustomDropDownButton extends StatelessWidget { Widget build(BuildContext context) { return DropdownButtonFormField( hint: hint, - icon: SvgPicture.asset( - 'assets/icons/arrow_down2.svg', - width: 12, - height: 8, - ), + icon: SvgPicture.asset('assets/icons/arrow_down2.svg', width: 12, height: 8), borderRadius: BorderRadius.circular(20), dropdownColor: appTheme.lightGray0, decoration: InputDecoration( @@ -43,7 +32,7 @@ class CustomDropDownButton extends StatelessWidget { fillColor: appTheme.lightGray0, filled: true, ), - value: value, + initialValue: value, items: items, onChanged: onChanged, ); diff --git a/lib/features/consultant/become_consultant/presentation/pages/registration_as_screen.dart b/lib/features/consultant/become_consultant/presentation/pages/registration_as_screen.dart index 76b90a26..b8fed20b 100644 --- a/lib/features/consultant/become_consultant/presentation/pages/registration_as_screen.dart +++ b/lib/features/consultant/become_consultant/presentation/pages/registration_as_screen.dart @@ -23,81 +23,105 @@ class _RegistrationAsScreenState extends State { children: [ const Padding( padding: EdgeInsets.symmetric(horizontal: 16), - child: Text( - 'Регистрация в качестве', - style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.w700, - height: 1.1, - ), - ), + child: Text('Регистрация в качестве', style: TextStyle(fontSize: 24, fontWeight: FontWeight.w700, height: 1.1)), ), const SizedBox(height: 16), - RadioListTile( - value: RegistrationAsType.physical, - groupValue: registrationAsType, - title: const Text( - 'Физического лица', - style: TextStyle( - fontSize: 18, - height: 1, + Column( + children: [ + Row( + children: [ + Radio( + value: RegistrationAsType.physical, + // ignore: deprecated_member_use + groupValue: registrationAsType, + activeColor: appTheme.blueA400, + // ignore: deprecated_member_use + onChanged: (value) { + setState(() { + registrationAsType = value!; + }); + }, + ), + const Expanded( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 8), + child: Text('Физического лица', style: TextStyle(fontSize: 18, height: 1)), + ), + ), + ], ), - ), - activeColor: appTheme.blueA400, - onChanged: (value) {}, - contentPadding: const EdgeInsets.symmetric(horizontal: 8), - ), - RadioListTile( - value: RegistrationAsType.selfBusy, - groupValue: registrationAsType, - title: const Text( - 'Самозанятого', - style: TextStyle( - fontSize: 18, - height: 1, + Row( + children: [ + Radio( + value: RegistrationAsType.selfBusy, + // ignore: deprecated_member_use + groupValue: registrationAsType, + activeColor: appTheme.blueA400, + // ignore: deprecated_member_use + onChanged: (value) { + setState(() { + registrationAsType = value!; + }); + }, + ), + const Expanded( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 8), + child: Text('Самозанятого', style: TextStyle(fontSize: 18, height: 1)), + ), + ), + ], ), - ), - onChanged: null, - contentPadding: const EdgeInsets.symmetric(horizontal: 8), - ), - RadioListTile( - value: RegistrationAsType.iP, - groupValue: registrationAsType, - title: const Text( - 'Индивидуального предпринимателя', - style: TextStyle( - fontSize: 18, - height: 1, + Row( + children: [ + Radio( + value: RegistrationAsType.iP, + // ignore: deprecated_member_use + groupValue: registrationAsType, + activeColor: appTheme.blueA400, + // ignore: deprecated_member_use + onChanged: (value) { + setState(() { + registrationAsType = value!; + }); + }, + ), + const Expanded( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 8), + child: Text('Индивидуального предпринимателя', style: TextStyle(fontSize: 18, height: 1)), + ), + ), + ], ), - ), - onChanged: null, - contentPadding: const EdgeInsets.symmetric(horizontal: 8), - ), - RadioListTile( - value: RegistrationAsType.ooo, - groupValue: registrationAsType, - title: const Text( - 'Общества с ограниченной ответственностью', - style: TextStyle( - fontSize: 18, - height: 1, + Row( + children: [ + Radio( + value: RegistrationAsType.ooo, + // ignore: deprecated_member_use + groupValue: registrationAsType, + activeColor: appTheme.blueA400, + // ignore: deprecated_member_use + onChanged: (value) { + setState(() { + registrationAsType = value!; + }); + }, + ), + const Expanded( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 8), + child: Text('Общества с ограниченной ответственностью', style: TextStyle(fontSize: 18, height: 1)), + ), + ), + ], ), - ), - onChanged: null, - contentPadding: const EdgeInsets.symmetric(horizontal: 8), - ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 16, - ), - child: _divider(context), + ], ), + Padding(padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), child: _divider(context)), const Padding( padding: EdgeInsets.symmetric(horizontal: 16), - child: Text( - '* На данный момент регистрация доступна только в качестве физического лица. Остальные варианты скоро появятся', - ), + child: Text('* На данный момент регистрация доступна только в качестве физического лица. Остальные варианты скоро появятся'), ), ], ), @@ -108,10 +132,7 @@ class _RegistrationAsScreenState extends State { return Container( height: 0.4, width: MediaQuery.of(context).size.width - 32, - decoration: BoxDecoration( - color: Colors.black26, - borderRadius: BorderRadius.circular(16), - ), + decoration: BoxDecoration(color: Colors.black26, borderRadius: BorderRadius.circular(16)), ); } } diff --git a/lib/features/registration/presentation/pages/gender_screen.dart b/lib/features/registration/presentation/pages/gender_screen.dart index 4403b54e..e58df3d4 100644 --- a/lib/features/registration/presentation/pages/gender_screen.dart +++ b/lib/features/registration/presentation/pages/gender_screen.dart @@ -27,48 +27,46 @@ class _GenderScreenState extends State { padding: const EdgeInsets.only(bottom: 80, top: 16, left: 16, right: 16), child: Column( children: [ - RadioListTile( - contentPadding: EdgeInsets.zero, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - title: const Text('Мужской'), - value: 'male', - groupValue: currentValue, - onChanged: (newValue) { - final registrationCubit = context.read(); - registrationCubit.updateSheet(registrationCubit.state.sheet.copyWith(gender: 'male')); - setState(() { - currentValue = 'male'; - }); - }, - secondary: Image.asset( - 'assets/images/img_male.png', - width: 40, - height: 40, - ), + Row( + children: [ + Radio( + value: 'male', + // ignore: deprecated_member_use + groupValue: currentValue, + // ignore: deprecated_member_use + onChanged: (newValue) { + final registrationCubit = context.read(); + registrationCubit.updateSheet(registrationCubit.state.sheet.copyWith(gender: 'male')); + setState(() { + currentValue = 'male'; + }); + }, + ), + Expanded( + child: Row(children: [Image.asset('assets/images/img_male.png', width: 40, height: 40), const SizedBox(width: 8), const Text('Мужской')]), + ), + ], ), const SizedBox(height: 16), - RadioListTile( - contentPadding: EdgeInsets.zero, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - title: const Text('Женский'), - value: 'female', - groupValue: currentValue, - onChanged: (newValue) { - final registrationCubit = context.read(); - registrationCubit.updateSheet(registrationCubit.state.sheet.copyWith(gender: 'female')); - setState(() { - currentValue = 'female'; - }); - }, - secondary: Image.asset( - 'assets/images/img_female.png', - width: 40, - height: 40, - ), + Row( + children: [ + Radio( + value: 'female', + // ignore: deprecated_member_use + groupValue: currentValue, + // ignore: deprecated_member_use + onChanged: (newValue) { + final registrationCubit = context.read(); + registrationCubit.updateSheet(registrationCubit.state.sheet.copyWith(gender: 'female')); + setState(() { + currentValue = 'female'; + }); + }, + ), + Expanded( + child: Row(children: [Image.asset('assets/images/img_female.png', width: 40, height: 40), const SizedBox(width: 8), const Text('Женский')]), + ), + ], ), ], ), diff --git a/lib/features/settings/presentation/pages/physical_activity_screen.dart b/lib/features/settings/presentation/pages/physical_activity_screen.dart index 3c6b000c..df91a4f6 100644 --- a/lib/features/settings/presentation/pages/physical_activity_screen.dart +++ b/lib/features/settings/presentation/pages/physical_activity_screen.dart @@ -7,10 +7,7 @@ import 'package:go_router/go_router.dart'; class PhysicalActivityScreen extends StatefulWidget { final SettingsEntity settingsEntity; - const PhysicalActivityScreen({ - super.key, - required this.settingsEntity, - }); + const PhysicalActivityScreen({super.key, required this.settingsEntity}); @override State createState() => _PhysicalActivityScreenState(); @@ -46,69 +43,108 @@ class _PhysicalActivityScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - title: const Text('Физическая нагрузка'), - ), + appBar: AppBar(title: const Text('Физическая нагрузка')), bottomSheet: _saveButton(), body: SingleChildScrollView( physics: const BouncingScrollPhysics(), padding: const EdgeInsets.only(top: 16, bottom: 80), child: Column( children: [ - RadioListTile( - contentPadding: const EdgeInsets.only(left: 8, right: 16), - title: const Text('1,2 — минимальный (сидячая работа, отсутствие физических нагрузок);'), - value: PhysicalActivity.minimum, - groupValue: _physicalActivity, - onChanged: (PhysicalActivity? value) { - setState(() { - _physicalActivity = value; - }); - }, + Row( + children: [ + Radio( + value: PhysicalActivity.minimum, + // ignore: deprecated_member_use + groupValue: _physicalActivity, + // ignore: deprecated_member_use + onChanged: (PhysicalActivity? value) { + setState(() { + _physicalActivity = value; + }); + }, + ), + const Expanded( + child: Padding( + padding: EdgeInsets.only(left: 8, right: 16), + child: Text('1,2 — минимальный (сидячая работа, отсутствие физических нагрузок);'), + ), + ), + ], ), - RadioListTile( - contentPadding: const EdgeInsets.only(left: 8, right: 16), - title: const Text('1,375 — низкий (тренировки не менее 20 мин 1–3 раза в неделю);'), - value: PhysicalActivity.low, - groupValue: _physicalActivity, - onChanged: (PhysicalActivity? value) { - setState(() { - _physicalActivity = value; - }); - }, + Row( + children: [ + Radio( + value: PhysicalActivity.low, + // ignore: deprecated_member_use + groupValue: _physicalActivity, + // ignore: deprecated_member_use + onChanged: (PhysicalActivity? value) { + setState(() { + _physicalActivity = value; + }); + }, + ), + const Expanded( + child: Padding(padding: EdgeInsets.only(left: 8, right: 16), child: Text('1,375 — низкий (тренировки не менее 20 мин 1–3 раза в неделю);')), + ), + ], ), - RadioListTile( - contentPadding: const EdgeInsets.only(left: 8, right: 16), - title: const Text('1,55 — умеренный (тренировки 30–60 мин 3–4 раза в неделю);'), - value: PhysicalActivity.medium, - groupValue: _physicalActivity, - onChanged: (PhysicalActivity? value) { - setState(() { - _physicalActivity = value; - }); - }, + Row( + children: [ + Radio( + value: PhysicalActivity.medium, + // ignore: deprecated_member_use + groupValue: _physicalActivity, + // ignore: deprecated_member_use + onChanged: (PhysicalActivity? value) { + setState(() { + _physicalActivity = value; + }); + }, + ), + const Expanded( + child: Padding(padding: EdgeInsets.only(left: 8, right: 16), child: Text('1,55 — умеренный (тренировки 30–60 мин 3–4 раза в неделю);')), + ), + ], ), - RadioListTile( - contentPadding: const EdgeInsets.only(left: 8, right: 16), - title: const Text('1,7 — высокий (тренировки 30–60 мин 5–7 раза в неделю; тяжёлая физическая работа);'), - value: PhysicalActivity.high, - groupValue: _physicalActivity, - onChanged: (PhysicalActivity? value) { - setState(() { - _physicalActivity = value; - }); - }, + Row( + children: [ + Radio( + value: PhysicalActivity.high, + // ignore: deprecated_member_use + groupValue: _physicalActivity, + // ignore: deprecated_member_use + onChanged: (PhysicalActivity? value) { + setState(() { + _physicalActivity = value; + }); + }, + ), + const Expanded( + child: Padding( + padding: EdgeInsets.only(left: 8, right: 16), + child: Text('1,7 — высокий (тренировки 30–60 мин 5–7 раза в неделю; тяжёлая физическая работа);'), + ), + ), + ], ), - RadioListTile( - contentPadding: const EdgeInsets.only(left: 8, right: 16), - title: const Text('1,9 — экстремальный (несколько интенсивных тренировок в день'), - value: PhysicalActivity.extreme, - groupValue: _physicalActivity, - onChanged: (PhysicalActivity? value) { - setState(() { - _physicalActivity = value; - }); - }, + Row( + children: [ + Radio( + value: PhysicalActivity.extreme, + // ignore: deprecated_member_use + groupValue: _physicalActivity, + // ignore: deprecated_member_use + onChanged: (PhysicalActivity? value) { + setState(() { + _physicalActivity = value; + }); + }, + ), + const Expanded( + child: Padding(padding: EdgeInsets.only(left: 8, right: 16), child: Text('1,9 — экстремальный (несколько интенсивных тренировок в день')), + ), + ], ), ], ), @@ -141,8 +177,8 @@ class _PhysicalActivityScreenState extends State { } widget.settingsEntity.settingsCubit.updateProfileDetails( profileDetailsModel: (widget.settingsEntity.settingsCubit.state as SettingsStateLoaded).profileDetails.copyWith( - activityCoefficient: activityCoefficient, - ), + activityCoefficient: activityCoefficient, + ), ); GoRouter.of(context).pop(); }, @@ -150,19 +186,11 @@ class _PhysicalActivityScreenState extends State { child: Ink( height: 52, width: MediaQuery.of(context).size.width - 32, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - color: appTheme.blueA400, - ), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: appTheme.blueA400), child: const Center( child: Text( 'Сохранить', - style: TextStyle( - color: Colors.white, - height: 1, - fontWeight: FontWeight.w600, - fontSize: 18, - ), + style: TextStyle(color: Colors.white, height: 1, fontWeight: FontWeight.w600, fontSize: 18), ), ), ), diff --git a/pubspec.yaml b/pubspec.yaml index c8638b7e..3b33303b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: eatvio_app description: A new Flutter project. -version: 1.2.9+69 +version: 1.2.10+70 publish_to: 'none' environment: From 7dfac3f792bd7e4c27f544cc6646cfc57eaabc83 Mon Sep 17 00:00:00 2001 From: Alexander Date: Wed, 20 Aug 2025 11:57:06 +0300 Subject: [PATCH 108/134] fix: baseUrl -> prod --- lib/core/const.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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: ''); From 5b74ae45b34a72a1edd567f6366c07ca1e4d8451 Mon Sep 17 00:00:00 2001 From: Alexander Date: Wed, 20 Aug 2025 12:42:39 +0300 Subject: [PATCH 109/134] fix: Issue-613 --- .../presentation/pages/settings_screen.dart | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/lib/features/settings/presentation/pages/settings_screen.dart b/lib/features/settings/presentation/pages/settings_screen.dart index 0d39d303..b7aa0d59 100644 --- a/lib/features/settings/presentation/pages/settings_screen.dart +++ b/lib/features/settings/presentation/pages/settings_screen.dart @@ -1,5 +1,4 @@ import 'package:eatvio_app/core/app_router.dart'; -import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/widgets/bottom_sheet_handler_bar.dart'; import 'package:eatvio_app/core/widgets/bottom_sheet_with_blur.dart'; @@ -18,7 +17,6 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; -import 'package:url_launcher/url_launcher.dart'; class SettingsScreen extends StatefulWidget { const SettingsScreen({super.key}); @@ -62,7 +60,6 @@ class _SettingsScreenState extends State { const SizedBox(height: 12), _notifications(state), const SizedBox(height: 12), - if (!hidePlans) ...[_aboutEatvio(state), const SizedBox(height: 12)], _becomeConsultantButton(state), _deleteAccountButton(state, settingsCubit), ], @@ -251,21 +248,6 @@ class _SettingsScreenState extends State { ); } - Column _aboutEatvio(SettingsStateLoaded state) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Padding( - padding: EdgeInsets.symmetric(horizontal: 16), - child: Text('О Eatvio', style: TextStyle(fontSize: 16, height: 1, color: Colors.grey)), - ), - _tile(title: 'О компании', onTap: () async => await launchUrl(Uri.parse('$baseUrl/ru/company-details'))), - _divider(context), - _tile(title: 'Пользовательское соглашение', onTap: () async => await launchUrl(Uri.parse('$baseUrl/oferta_bs.pdf'))), - ], - ); - } - SingleChildRenderObjectWidget _becomeConsultantButton(SettingsStateLoaded state) { if (state.consultationRequestModel == null) { return Padding( From f76b142117d3b106db20d1883f2fc5a7b30adfc5 Mon Sep 17 00:00:00 2001 From: Alexander Date: Wed, 20 Aug 2025 12:54:54 +0300 Subject: [PATCH 110/134] fix: ios build --- ios/Podfile | 4 ++-- ios/Runner.xcodeproj/project.pbxproj | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ios/Podfile b/ios/Podfile index 53a8d390..3d294b11 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -platform :ios, '14.0' +platform :ios, '12.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' @@ -39,7 +39,7 @@ post_install do |installer| flutter_additional_ios_build_settings(target) target.build_configurations.each do |config| config.build_settings["EXCLUDED_ARCHS[sdk=iphonesimulator*]"] = "arm64" - config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '14.0' + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '12.0' config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [ '$(inherited)', ## dart: PermissionGroup.camera diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index e484a472..b145a3fd 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -405,7 +405,7 @@ ); INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Eatvio; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -555,7 +555,7 @@ ); INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Eatvio; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -593,7 +593,7 @@ ); INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Eatvio; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", From 88558d62effd1116746b01136d206d152c698fca Mon Sep 17 00:00:00 2001 From: Alexander Date: Wed, 20 Aug 2025 12:58:48 +0300 Subject: [PATCH 111/134] fix: ios build --- ios/Flutter/AppFrameworkInfo.plist | 2 +- ios/Podfile | 4 ++-- ios/Runner.xcodeproj/project.pbxproj | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist index 8c6e5614..d57061dd 100644 --- a/ios/Flutter/AppFrameworkInfo.plist +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 12.0 + 13.0 diff --git a/ios/Podfile b/ios/Podfile index 3d294b11..f55aa008 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -platform :ios, '12.0' +platform :ios, '13.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' @@ -39,7 +39,7 @@ post_install do |installer| flutter_additional_ios_build_settings(target) target.build_configurations.each do |config| config.build_settings["EXCLUDED_ARCHS[sdk=iphonesimulator*]"] = "arm64" - config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '12.0' + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '13.0' config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [ '$(inherited)', ## dart: PermissionGroup.camera diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index b145a3fd..b14cc3e7 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -378,7 +378,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -405,7 +405,7 @@ ); INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Eatvio; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -474,7 +474,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -526,7 +526,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -555,7 +555,7 @@ ); INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Eatvio; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -593,7 +593,7 @@ ); INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Eatvio; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", From 7fd24f57e3fb642a402fb78f15039bbdf5311681 Mon Sep 17 00:00:00 2001 From: Alexander Date: Wed, 20 Aug 2025 13:03:16 +0300 Subject: [PATCH 112/134] fix: build ios --- ios/Flutter/AppFrameworkInfo.plist | 2 +- ios/Podfile | 4 ++-- ios/Runner.xcodeproj/project.pbxproj | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist index d57061dd..86343d86 100644 --- a/ios/Flutter/AppFrameworkInfo.plist +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 13.0 + 16.0 diff --git a/ios/Podfile b/ios/Podfile index f55aa008..4fe34ec9 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -platform :ios, '13.0' +platform :ios, '16.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' @@ -39,7 +39,7 @@ post_install do |installer| flutter_additional_ios_build_settings(target) target.build_configurations.each do |config| config.build_settings["EXCLUDED_ARCHS[sdk=iphonesimulator*]"] = "arm64" - config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '13.0' + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '16.0' config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [ '$(inherited)', ## dart: PermissionGroup.camera diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index b14cc3e7..d6e7401a 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -378,7 +378,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -405,7 +405,7 @@ ); INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Eatvio; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -474,7 +474,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -526,7 +526,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -555,7 +555,7 @@ ); INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Eatvio; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -593,7 +593,7 @@ ); INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Eatvio; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", From 98803e56cfad8ffe77471c25947c53d381c09577 Mon Sep 17 00:00:00 2001 From: Alexander Date: Wed, 20 Aug 2025 15:51:46 +0300 Subject: [PATCH 113/134] fix: Issue-618 --- lib/core/utils/date_time_utils.dart | 35 +++++++++++++------ .../menu/presentation/pages/menu_screen.dart | 3 +- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/lib/core/utils/date_time_utils.dart b/lib/core/utils/date_time_utils.dart index bcc82aeb..701fa80e 100644 --- a/lib/core/utils/date_time_utils.dart +++ b/lib/core/utils/date_time_utils.dart @@ -3,11 +3,30 @@ import 'package:intl/intl.dart'; const String dateTimeFormatPattern = 'dd/MM/yyyy'; +/// Преобразует дату из формата гггг-мм-дд чч:мм:сс в дд.мм.гггг +String formatBuildDate(String dateString) { + if (dateString.isEmpty) return ''; + + try { + final dateTimeParts = dateString.split(' '); + if (dateTimeParts.isNotEmpty) { + final datePart = dateTimeParts[0]; + final dateParts = datePart.split('-'); + if (dateParts.length == 3) { + final year = dateParts[0]; + final month = dateParts[1]; + final day = dateParts[2]; + return '$day.$month.$year'; + } + } + } catch (e) { + return dateString; + } + return dateString; +} + extension DateTimeExtension on DateTime { - String format([ - String pattern = dateTimeFormatPattern, - String? locale, - ]) { + String format([String pattern = dateTimeFormatPattern, String? locale]) { final localDate = isUtc ? toLocal() : this; if (locale != null && locale.isNotEmpty) { initializeDateFormatting(locale); @@ -19,17 +38,13 @@ extension DateTimeExtension on DateTime { final localDate = isUtc ? toLocal() : this; final now = DateTime.now(); final yesterday = DateTime(now.year, now.month, now.day).subtract(const Duration(days: 1)); - return localDate.year == yesterday.year && - localDate.month == yesterday.month && - localDate.day == yesterday.day; + return localDate.year == yesterday.year && localDate.month == yesterday.month && localDate.day == yesterday.day; } bool isToday() { final localDate = isUtc ? toLocal() : this; final now = DateTime.now(); - return now.year == localDate.year && - now.month == localDate.month && - now.day == localDate.day; + return now.year == localDate.year && now.month == localDate.month && now.day == localDate.day; } } diff --git a/lib/features/menu/presentation/pages/menu_screen.dart b/lib/features/menu/presentation/pages/menu_screen.dart index 7e4c358a..8aa16842 100644 --- a/lib/features/menu/presentation/pages/menu_screen.dart +++ b/lib/features/menu/presentation/pages/menu_screen.dart @@ -3,6 +3,7 @@ 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'; import 'package:eatvio_app/core/utils/image_constant.dart'; +import 'package:eatvio_app/core/utils/date_time_utils.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/consultant_finances_screen.dart'; @@ -160,7 +161,7 @@ class _AppInfoState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Версия приложения: $version ($buildNumber)', style: TextStyles.textXSSemibold.copyWith(color: appTheme.gray)), - if (buildDate != '') Text('Дата сборки: $buildDate', style: TextStyles.textXSSemibold.copyWith(color: appTheme.gray)), + if (buildDate != '') Text('Дата сборки: ${formatBuildDate(buildDate)}', style: TextStyles.textXSSemibold.copyWith(color: appTheme.gray)), ], ), ), From 766d06fc4d4a946cc99ea33b6f8d921b0bd2acf9 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Fri, 22 Aug 2025 14:17:11 +0300 Subject: [PATCH 114/134] fix: Issue-592 --- .../feed/presentation/widgets/blog_post_widget.dart | 6 +++--- lib/features/feed/presentation/widgets/recipes_widget.dart | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/features/feed/presentation/widgets/blog_post_widget.dart b/lib/features/feed/presentation/widgets/blog_post_widget.dart index 6479d0d9..23eaeded 100644 --- a/lib/features/feed/presentation/widgets/blog_post_widget.dart +++ b/lib/features/feed/presentation/widgets/blog_post_widget.dart @@ -245,9 +245,9 @@ class _BlogPostWidgetState extends State { const SizedBox(width: 8), FeedIconButton( svgIcon: SvgPicture.asset( - height: 20, - width: 20, - 'assets/icons/share.svg', + height: 15, + width: 12, + 'assets/icons/repost.svg', colorFilter: ColorFilter.mode(appTheme.gray600, BlendMode.srcIn), ), onTap: () { diff --git a/lib/features/feed/presentation/widgets/recipes_widget.dart b/lib/features/feed/presentation/widgets/recipes_widget.dart index 7720c6cd..c0dbeb39 100644 --- a/lib/features/feed/presentation/widgets/recipes_widget.dart +++ b/lib/features/feed/presentation/widgets/recipes_widget.dart @@ -231,7 +231,7 @@ class _RecipesWidgetState extends State { final box = context.findRenderObject() as RenderBox?; SharePlus.instance.share( ShareParams( - uri: Uri.parse('$baseUrl/ru/blog_post/${widget.recipe.id}'), + uri: Uri.parse('$baseUrl/ru/recipe/${widget.recipe.id}'), sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, ), ); From a416912d2a694a35b28c7d9734fd65305513bf10 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Mon, 25 Aug 2025 11:26:14 +0300 Subject: [PATCH 115/134] fix: add product modal and auth error --- .../auth/presentation/pages/auth_screen.dart | 14 ++- .../widgets/add_product_modal.dart | 118 +++++------------- .../presentation/widgets/diets_widget.dart | 2 +- 3 files changed, 45 insertions(+), 89 deletions(-) diff --git a/lib/features/auth/presentation/pages/auth_screen.dart b/lib/features/auth/presentation/pages/auth_screen.dart index 20b0e26c..73162bd2 100644 --- a/lib/features/auth/presentation/pages/auth_screen.dart +++ b/lib/features/auth/presentation/pages/auth_screen.dart @@ -47,12 +47,20 @@ class _AuthScreenState extends State { super.initState(); var keyboardVisibilityController = KeyboardVisibilityController(); keyboardSubscription = keyboardVisibilityController.onChange.listen((bool visible) { - setState(() { - isFocused = visible; - }); + if (mounted) { + setState(() { + isFocused = visible; + }); + } }); } + @override + void dispose() { + keyboardSubscription.cancel(); + super.dispose(); + } + Future _reconnectWebSocket(BuildContext context) async { try { await sl().clear(); diff --git a/lib/features/diary/presentation/widgets/add_product_modal.dart b/lib/features/diary/presentation/widgets/add_product_modal.dart index b55a979f..9b6c2536 100644 --- a/lib/features/diary/presentation/widgets/add_product_modal.dart +++ b/lib/features/diary/presentation/widgets/add_product_modal.dart @@ -14,12 +14,7 @@ class AddProductModal extends StatefulWidget { final DiaryStateLoaded state; final DiaryCubit diaryCubit; - const AddProductModal({ - super.key, - this.portion, - required this.state, - required this.diaryCubit, - }); + const AddProductModal({super.key, this.portion, required this.state, required this.diaryCubit}); @override State createState() => _AddProductModalState(); @@ -58,75 +53,62 @@ class _AddProductModalState extends State { return StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { 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( mainAxisSize: MainAxisSize.min, children: [ SizedBox( - width: MediaQuery.of(context).size.width * 0.8 - 32, + width: MediaQuery.of(context).size.width * 0.76 - 32, child: CustomTextFormField( controller: productNameController, autofocus: true, hintText: 'Введите название продукта/блюда', onChanged: (query) { - EasyDebounce.debounce( - 'glossary-search', - const Duration(milliseconds: 250), - () async { - selectedGlossary = null; - if (query.isEmpty) { - glossariesList = []; - setState(() {}); - return; - } - glossariesList = await widget.diaryCubit.getGlossaryListByQuery(query); + EasyDebounce.debounce('glossary-search', const Duration(milliseconds: 250), () async { + selectedGlossary = null; + if (query.isEmpty) { + glossariesList = []; setState(() {}); - }, - ); + return; + } + glossariesList = await widget.diaryCubit.getGlossaryListByQuery(query); + setState(() {}); + }); }, ), ), const SizedBox(width: 4), SizedBox( - width: MediaQuery.of(context).size.width * 0.2 - 4, + width: MediaQuery.of(context).size.width * 0.24 - 4, child: CustomTextFormField( controller: quantityController, keyboardType: TextInputType.number, 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, @@ -141,28 +123,12 @@ class _AddProductModalState 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), ); }, @@ -188,14 +154,8 @@ class _AddProductModalState extends State { 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.portion != null) const SizedBox(width: 8), @@ -209,8 +169,8 @@ class _AddProductModalState extends State { portion: selectedGlossary == null ? 'gr' : selectedGlossary!.grInPortion == 0 - ? 'gr' - : 'pcs', + ? 'gr' + : 'pcs', quantity: int.parse(quantityController.text), productNutritionalValueId: selectedGlossary?.id, ); @@ -223,8 +183,8 @@ class _AddProductModalState extends State { portion: selectedGlossary == null ? 'gr' : selectedGlossary!.grInPortion == 0 - ? 'gr' - : 'pcs', + ? 'gr' + : 'pcs', quantity: int.parse(quantityController.text), productNutritionalValueId: selectedGlossary?.id, portionState: widget.portion!.portionState, @@ -237,13 +197,8 @@ class _AddProductModalState extends State { useRootNavigator: true, context: context, builder: (context) => Padding( - padding: EdgeInsets.only( - bottom: MediaQuery.of(context).viewInsets.bottom, - ), - child: GlossaryModerationRequestModalWidget( - productName: productNameController.text, - diaryCubit: widget.diaryCubit, - ), + padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), + child: GlossaryModerationRequestModalWidget(productName: productNameController.text, diaryCubit: widget.diaryCubit), ), ); } @@ -252,18 +207,11 @@ class _AddProductModalState extends State { child: Ink( width: MediaQuery.of(context).size.width - (widget.portion == 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.portion == null ? 'Добавить' : 'Сохранить', - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - color: Colors.white, - ), + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Colors.white), ), ), ), diff --git a/lib/features/feed/presentation/widgets/diets_widget.dart b/lib/features/feed/presentation/widgets/diets_widget.dart index 34964d1a..d91cdd74 100644 --- a/lib/features/feed/presentation/widgets/diets_widget.dart +++ b/lib/features/feed/presentation/widgets/diets_widget.dart @@ -17,7 +17,7 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:go_router/go_router.dart'; import 'package:share_plus/share_plus.dart'; -class DietsWidget extends StatefulWidget { +class DietsWidget extends StatefulWidget { final Diet diet; final int index; const DietsWidget({ From 886cfa8f1c386fde94a340fe866364a5b2523b9f Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Mon, 25 Aug 2025 13:05:53 +0300 Subject: [PATCH 116/134] feat: AppMetrica --- lib/core/env_config.dart | 3 +++ lib/main.dart | 4 ++++ pubspec.yaml | 1 + 3 files changed, 8 insertions(+) diff --git a/lib/core/env_config.dart b/lib/core/env_config.dart index b363ad70..a3404aeb 100644 --- a/lib/core/env_config.dart +++ b/lib/core/env_config.dart @@ -7,6 +7,9 @@ class EnvConfig { // Sentry Configuration static String get sentryDsn => dotenv.env['SENTRY_DSN'] ?? ''; + // AppMetrica Configuration + static String get appMetricaApiKey => dotenv.env['APPMETRICA_KEY'] ?? ''; + // Firebase Configuration static String get firebaseProjectId => dotenv.env['FIREBASE_PROJECT_ID'] ?? ''; static String get firebaseDatabaseUrl => dotenv.env['FIREBASE_DATABASE_URL'] ?? ''; diff --git a/lib/main.dart b/lib/main.dart index 6b44348c..468de90f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -23,6 +23,7 @@ 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(); @@ -38,6 +39,9 @@ Future main() async { 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; diff --git a/pubspec.yaml b/pubspec.yaml index 3b33303b..10fafb98 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -73,6 +73,7 @@ dependencies: qr_code_scanner_plus: ^2.0.10+1 flutter_dotenv: ^5.2.1 uuid: ^4.5.1 + appmetrica_plugin: ^3.3.0 dev_dependencies: build_runner: ^2.7.0 From 5a8fa7089ce7da0f492ca6916c8c95699b6bf6be Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Mon, 25 Aug 2025 14:24:06 +0300 Subject: [PATCH 117/134] feat: appmetrica --- .../registration_cubit.dart | 73 +++++---- .../verify_email_cubit.dart | 6 +- .../pages/registration_screen.dart | 145 ++++-------------- .../pages/verify_email_screen.dart | 5 +- .../presentation/pages/verify_screen.dart | 44 ++---- .../pages/webview_registration_screen.dart | 18 +-- .../registration_analytics_service.dart | 30 ++++ 7 files changed, 120 insertions(+), 201 deletions(-) create mode 100644 lib/features/registration/services/registration_analytics_service.dart diff --git a/lib/features/registration/presentation/bloc/registration_cubit/registration_cubit.dart b/lib/features/registration/presentation/bloc/registration_cubit/registration_cubit.dart index c9426674..ccf9cae4 100644 --- a/lib/features/registration/presentation/bloc/registration_cubit/registration_cubit.dart +++ b/lib/features/registration/presentation/bloc/registration_cubit/registration_cubit.dart @@ -8,62 +8,61 @@ import 'package:eatvio_app/core/token_storage.dart'; import 'package:eatvio_app/features/registration/models/user_registration_model.dart'; import 'package:eatvio_app/features/registration/presentation/bloc/registration_cubit/registration_state.dart'; import 'package:eatvio_app/features/registration/repositories/registration_repository.dart'; +import 'package:eatvio_app/features/registration/services/registration_analytics_service.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; class RegistrationCubit extends Cubit { final RegistrationRepository registrationRepository; - RegistrationCubit({ - required this.registrationRepository, - }) : super( - const RegistrationState( - sheet: UserRegistrationSheet( - firstName: '', - lastName: '', - email: '', - password: '', - passwordConfirmation: '', - birthdate: '', - gender: '', - location: '', - responseBody: '{}', - lang: 'Русский', - weight: 0, - height: 0, - chestCircumference: 0, - waistCircumference: 0, - hipCircumference: 0, - bloodPressureUp: 0, - bloodPressureDown: 0, - hasHypo: 0, - hasHyper: 0, - aboutAllergy: '', - aboutTakingPils: '', - aboutSportHobbies: '', - favoriteFoods: '', - unfavoriteFoods: '', - additionalInfo: '', - phone: '', - ), + RegistrationCubit({required this.registrationRepository}) + : super( + const RegistrationState( + sheet: UserRegistrationSheet( + firstName: '', + lastName: '', + email: '', + password: '', + passwordConfirmation: '', + birthdate: '', + gender: '', + location: '', + responseBody: '{}', + lang: 'Русский', + weight: 0, + height: 0, + chestCircumference: 0, + waistCircumference: 0, + hipCircumference: 0, + bloodPressureUp: 0, + bloodPressureDown: 0, + hasHypo: 0, + hasHyper: 0, + aboutAllergy: '', + aboutTakingPils: '', + aboutSportHobbies: '', + favoriteFoods: '', + unfavoriteFoods: '', + additionalInfo: '', + phone: '', ), - ); + ), + ); void updateSheet(UserRegistrationSheet sheet) => emit(RegistrationState(sheet: sheet)); Future tryRegistration() async { try { final token = await registrationRepository.tryRegister( - state.sheet.copyWith( - phone: state.sheet.phone.replaceAll(' ', '').replaceAll('(', '').replaceAll(')', ''), - responseBody: '{}', - ), + state.sheet.copyWith(phone: state.sheet.phone.replaceAll(' ', '').replaceAll('(', '').replaceAll(')', ''), responseBody: '{}'), ); await TokenStorage.saveTokenInStorage(token); await sl().registerPushToken(); + RegistrationAnalyticsService.reportRegistrationComplete(); AppRouter.navigatorKey.currentContext?.replaceNamed('verifyEmailScreen'); } catch (e, st) { log(e.toString(), stackTrace: st); + RegistrationAnalyticsService.reportRegistrationError(e.toString()); emit(RegistrationState(sheet: state.sheet, error: e.toString())); } } diff --git a/lib/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart b/lib/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart index 649c1fb8..d1f547f8 100644 --- a/lib/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart +++ b/lib/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart @@ -1,13 +1,12 @@ import 'package:eatvio_app/features/profile/repositories/profile_repository.dart'; import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_state.dart'; +import 'package:eatvio_app/features/registration/services/registration_analytics_service.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class VerifyEmailCubit extends Cubit { final ProfileRepository profileRepository; - VerifyEmailCubit({ - required this.profileRepository, - }) : super(const VerifyEmailState.loading()); + VerifyEmailCubit({required this.profileRepository}) : super(const VerifyEmailState.loading()); Future sendEmailCode() async { try { @@ -24,6 +23,7 @@ class VerifyEmailCubit extends Cubit { emit(const VerifyEmailState.verifyLoading()); final result = await profileRepository.tryVerifyEmailByCode(code); if (result == true) { + RegistrationAnalyticsService.reportEmailVerification(); emit(const VerifyEmailState.verifySuccess()); } else { emit(const VerifyEmailState.verifyError()); diff --git a/lib/features/registration/presentation/pages/registration_screen.dart b/lib/features/registration/presentation/pages/registration_screen.dart index 86514be8..9f30ca33 100644 --- a/lib/features/registration/presentation/pages/registration_screen.dart +++ b/lib/features/registration/presentation/pages/registration_screen.dart @@ -17,6 +17,7 @@ import 'package:eatvio_app/features/registration/presentation/pages/medicine_int import 'package:eatvio_app/features/registration/presentation/pages/name_screen.dart'; import 'package:eatvio_app/features/registration/presentation/pages/phone_screen.dart'; import 'package:eatvio_app/features/registration/presentation/pages/weight_screen.dart'; +import 'package:eatvio_app/features/registration/services/registration_analytics_service.dart'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; @@ -51,12 +52,7 @@ class _RegistrationScreenState extends State { builder: (context) => AlertDialog( title: const Text('Ошибка при регистрации'), content: Text(state.error!), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text('OK'), - ), - ], + actions: [TextButton(onPressed: () => Navigator.pop(context), child: const Text('OK'))], ), ); } @@ -76,22 +72,14 @@ class _RegistrationScreenState extends State { highlightElevation: 0, focusElevation: 0, elevation: 0, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), - ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), backgroundColor: const Color(0xFFEAF2FD), onPressed: () async { setState(() { - pageController.previousPage( - duration: const Duration(milliseconds: 350), - curve: Curves.easeInOut, - ); + pageController.previousPage(duration: const Duration(milliseconds: 350), curve: Curves.easeInOut); }); }, - label: Icon( - Icons.arrow_back_ios_new_rounded, - color: appTheme.blueA400, - ), + label: Icon(Icons.arrow_back_ios_new_rounded, color: appTheme.blueA400), ), ), Container( @@ -103,24 +91,16 @@ class _RegistrationScreenState extends State { highlightElevation: 0, elevation: 0, focusElevation: 0, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), - ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), backgroundColor: appTheme.blueA400, onPressed: () async { if (await canGoNext(state, context)) { setState(() { - pageController.nextPage( - duration: const Duration(milliseconds: 350), - curve: Curves.easeInOut, - ); + pageController.nextPage(duration: const Duration(milliseconds: 350), curve: Curves.easeInOut); }); } }, - label: Text( - 'Продолжить', - style: theme.textTheme.titleSmall!.copyWith(color: Colors.white), - ), + label: Text('Продолжить', style: theme.textTheme.titleSmall!.copyWith(color: Colors.white)), ), ), ], @@ -128,23 +108,11 @@ class _RegistrationScreenState extends State { appBar: AppBar( automaticallyImplyLeading: false, leading: Builder( - builder: (context) => IconButton( - icon: const Icon(Icons.close_rounded), - onPressed: () => GoRouter.of(context).pop(), - ), + builder: (context) => IconButton(icon: const Icon(Icons.close_rounded), onPressed: () => GoRouter.of(context).pop()), ), centerTitle: true, - title: SvgPicture.asset( - ImageConstant.imgLogoEatvio, - height: 48, - fit: BoxFit.contain, - ), - actions: [ - Padding( - padding: const EdgeInsets.only(right: 16), - child: Text('шаг $step из 15'), - ), - ], + title: SvgPicture.asset(ImageConstant.imgLogoEatvio, height: 48, fit: BoxFit.contain), + actions: [Padding(padding: const EdgeInsets.only(right: 16), child: Text('шаг $step из 15'))], bottom: PreferredSize( preferredSize: Size(MediaQuery.of(context).size.width, 32), child: Padding( @@ -152,10 +120,7 @@ class _RegistrationScreenState extends State { child: TweenAnimationBuilder( duration: const Duration(milliseconds: 350), curve: Curves.easeInOut, - tween: Tween( - begin: 1 / 15, - end: progressValue, - ), + tween: Tween(begin: 1 / 15, end: progressValue), builder: (context, value, _) => LinearProgressIndicator( value: value, borderRadius: BorderRadius.circular(16), @@ -175,6 +140,8 @@ class _RegistrationScreenState extends State { step = index + 1; progressValue = step / 15; }); + // Report analytics for step completion + RegistrationAnalyticsService.reportRegistrationStep(step); }, children: [ // LanguageSelectScreen(sheet: state.sheet), @@ -211,12 +178,7 @@ class _RegistrationScreenState extends State { builder: (context) => AlertDialog( title: const Text('Ошибка'), content: const Text('Введите корректный номер телефона'), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text('OK'), - ), - ], + actions: [TextButton(onPressed: () => Navigator.pop(context), child: const Text('OK'))], ), ); return false; @@ -224,12 +186,7 @@ class _RegistrationScreenState extends State { return true; } case 2: - List errors = validateCredentialsFields( - state.sheet.email, - state.sheet.password, - state.sheet.passwordConfirmation, - context, - ); + List errors = validateCredentialsFields(state.sheet.email, state.sheet.password, state.sheet.passwordConfirmation, context); if (errors.isEmpty) { return true; } else { @@ -244,22 +201,13 @@ class _RegistrationScreenState extends State { builder: (context) => AlertDialog( title: const Text('Ошибка'), content: Text(errorMessage), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text('OK'), - ), - ], + actions: [TextButton(onPressed: () => Navigator.pop(context), child: const Text('OK'))], ), ); return false; } case 3: - List errors = validateNameFields( - state.sheet.firstName.toString(), - state.sheet.lastName.toString(), - context, - ); + List errors = validateNameFields(state.sheet.firstName.toString(), state.sheet.lastName.toString(), context); if (errors.isEmpty) { return true; } else { @@ -274,12 +222,7 @@ class _RegistrationScreenState extends State { builder: (context) => AlertDialog( title: const Text('Ошибка'), content: Text(errorMessage), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text('OK'), - ), - ], + actions: [TextButton(onPressed: () => Navigator.pop(context), child: const Text('OK'))], ), ); return false; @@ -293,12 +236,7 @@ class _RegistrationScreenState extends State { builder: (context) => AlertDialog( title: const Text('Ошибка'), content: const Text('Выберете пол'), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text('OK'), - ), - ], + actions: [TextButton(onPressed: () => Navigator.pop(context), child: const Text('OK'))], ), ); return false; @@ -312,12 +250,7 @@ class _RegistrationScreenState extends State { builder: (context) => AlertDialog( title: const Text('Ошибка'), content: const Text('Заполните все поля'), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text('OK'), - ), - ], + actions: [TextButton(onPressed: () => Navigator.pop(context), child: const Text('OK'))], ), ); return false; @@ -343,12 +276,7 @@ class _RegistrationScreenState extends State { builder: (context) => AlertDialog( title: const Text('Ошибка'), content: Text(errorMessage), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text('OK'), - ), - ], + actions: [TextButton(onPressed: () => Navigator.pop(context), child: const Text('OK'))], ), ); return false; @@ -359,10 +287,7 @@ class _RegistrationScreenState extends State { barrierDismissible: false, useSafeArea: false, context: context, - builder: (context) => const PopScope( - canPop: false, - child: Loader(), - ), + builder: (context) => const PopScope(canPop: false, child: Loader()), ); await registrationCubit.tryRegistration(); if (context.mounted) { @@ -373,9 +298,7 @@ class _RegistrationScreenState extends State { Navigator.pop(context); showDialog( context: context, - builder: (context) => const AlertDialog( - title: Text('Ошибка при попытке регистрации'), - ), + builder: (context) => const AlertDialog(title: Text('Ошибка при попытке регистрации')), ); } } @@ -385,12 +308,7 @@ class _RegistrationScreenState extends State { } } - List validateCircumferencesFields( - String chestCircumference, - String waistCircumference, - String hipCircumference, - BuildContext context, - ) { + List validateCircumferencesFields(String chestCircumference, String waistCircumference, String hipCircumference, BuildContext context) { final s = S.of(context); List errors = []; @@ -406,12 +324,7 @@ class _RegistrationScreenState extends State { return errors; } - List validateCredentialsFields( - String email, - String password, - String passwordConfirmation, - BuildContext context, - ) { + List validateCredentialsFields(String email, String password, String passwordConfirmation, BuildContext context) { final s = S.of(context); List errors = []; @@ -435,11 +348,7 @@ class _RegistrationScreenState extends State { return errors; } - List validateNameFields( - String firstName, - String lastName, - BuildContext context, - ) { + List validateNameFields(String firstName, String lastName, BuildContext context) { List errors = []; if (firstName.isEmpty) { diff --git a/lib/features/registration/presentation/pages/verify_email_screen.dart b/lib/features/registration/presentation/pages/verify_email_screen.dart index f50d4960..e91b9e9e 100644 --- a/lib/features/registration/presentation/pages/verify_email_screen.dart +++ b/lib/features/registration/presentation/pages/verify_email_screen.dart @@ -7,6 +7,7 @@ import 'package:eatvio_app/core/utils/toasts.dart'; import 'package:eatvio_app/core/widgets/custom_outlined_button.dart'; import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart'; import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_state.dart'; +import 'package:eatvio_app/features/registration/services/registration_analytics_service.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; @@ -29,8 +30,10 @@ class _VerifyEmailScreenState extends State { @override void initState() { super.initState(); - goRouter = GoRouter.of(context); + goRouter = GoRouter.of(context); _startTimer(); + // Report email verification screen opened + RegistrationAnalyticsService.reportRegistrationStep(16); // Email verification step } @override diff --git a/lib/features/registration/presentation/pages/verify_screen.dart b/lib/features/registration/presentation/pages/verify_screen.dart index 637dba5b..8d5df9df 100644 --- a/lib/features/registration/presentation/pages/verify_screen.dart +++ b/lib/features/registration/presentation/pages/verify_screen.dart @@ -1,6 +1,7 @@ import 'package:eatvio_app/core/const.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'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:lottie/lottie.dart'; @@ -9,10 +10,7 @@ import 'package:webview_flutter/webview_flutter.dart'; class VerifyScreen extends StatefulWidget { final String passKey; - const VerifyScreen({ - super.key, - required this.passKey, - }); + const VerifyScreen({super.key, required this.passKey}); @override State createState() => _VerifyScreenState(); @@ -25,6 +23,8 @@ class _VerifyScreenState extends State { @override void initState() { super.initState(); + // Report phone verification screen opened + RegistrationAnalyticsService.reportRegistrationStep(17); // Phone verification step _controller = WebViewController() ..setJavaScriptMode(JavaScriptMode.unrestricted) ..setNavigationDelegate( @@ -36,6 +36,8 @@ class _VerifyScreenState extends State { }, onNavigationRequest: (NavigationRequest request) async { if (request.url.contains('user_detailz') && !request.url.contains('change-phone-verify') || request.url.contains('email/verify')) { + // Report phone verification success + RegistrationAnalyticsService.reportPhoneVerification(); GoRouter.of(context).pop(); await showModalBottomSheet( useRootNavigator: true, @@ -50,27 +52,15 @@ class _VerifyScreenState extends State { const Text( 'Номер телефона\nуспешно привязан', textAlign: TextAlign.center, - style: TextStyle( - fontSize: 20, - height: 1, - fontWeight: FontWeight.w600, - ), + style: TextStyle(fontSize: 20, height: 1, fontWeight: FontWeight.w600), ), const SizedBox(height: 16), - Lottie.asset( - 'assets/lottie_animations/success.json', - width: 160, - height: 160, - ), + Lottie.asset('assets/lottie_animations/success.json', width: 160, height: 160), const SizedBox(height: 16), const Text( 'Теперь Вам доступен весь функционал!', textAlign: TextAlign.center, - style: TextStyle( - fontSize: 16, - height: 1, - fontWeight: FontWeight.w500, - ), + style: TextStyle(fontSize: 16, height: 1, fontWeight: FontWeight.w500), ), const SizedBox(height: 16), InkWell( @@ -80,18 +70,11 @@ class _VerifyScreenState extends State { child: Ink( width: MediaQuery.of(context).size.width - 32, height: 52, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - color: appTheme.blueA400, - ), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: appTheme.blueA400), child: const Center( child: Text( 'Закрыть', - style: TextStyle( - color: Colors.white, - fontSize: 18, - fontWeight: FontWeight.w600, - ), + style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.w600), ), ), ), @@ -117,10 +100,7 @@ class _VerifyScreenState extends State { child: Stack( children: [ WebViewWidget(controller: _controller), - if (_isLoading) - const Center( - child: Loader(), - ), + if (_isLoading) const Center(child: Loader()), ], ), ), diff --git a/lib/features/registration/presentation/pages/webview_registration_screen.dart b/lib/features/registration/presentation/pages/webview_registration_screen.dart index abb6537d..05c0ffc1 100644 --- a/lib/features/registration/presentation/pages/webview_registration_screen.dart +++ b/lib/features/registration/presentation/pages/webview_registration_screen.dart @@ -4,6 +4,7 @@ import 'dart:developer'; import 'package:eatvio_app/core/const.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'; import 'package:flutter/material.dart'; import 'package:webview_flutter/webview_flutter.dart'; @@ -24,6 +25,8 @@ class _WebViewRegistrationScreenState extends State { void initState() { super.initState(); authCubit = sl(); + // Report webview registration started + RegistrationAnalyticsService.reportRegistrationStep(18); // Webview registration step _controller = WebViewController() ..setJavaScriptMode(JavaScriptMode.unrestricted) ..addJavaScriptChannel( @@ -43,10 +46,9 @@ class _WebViewRegistrationScreenState extends State { NavigationDelegate( onNavigationRequest: (NavigationRequest request) { if (request.url.contains('profile')) { - authCubit.getTokenByCredentials( - email!, - password!, - ); + // Report webview registration completion + RegistrationAnalyticsService.reportRegistrationComplete(); + authCubit.getTokenByCredentials(email!, password!); } return NavigationDecision.navigate; }, @@ -86,12 +88,8 @@ class _WebViewRegistrationScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - toolbarHeight: 0, - ), - body: SafeArea( - child: WebViewWidget(controller: _controller), - ), + appBar: AppBar(toolbarHeight: 0), + body: SafeArea(child: WebViewWidget(controller: _controller)), ); } } diff --git a/lib/features/registration/services/registration_analytics_service.dart b/lib/features/registration/services/registration_analytics_service.dart new file mode 100644 index 00000000..3bfc69d1 --- /dev/null +++ b/lib/features/registration/services/registration_analytics_service.dart @@ -0,0 +1,30 @@ +import 'package:appmetrica_plugin/appmetrica_plugin.dart'; + +class RegistrationAnalyticsService { + static const String _eventPrefix = 'registration_step'; + + /// Reports registration step completion + static void reportRegistrationStep(int stepNumber) { + AppMetrica.reportEvent('$_eventPrefix $stepNumber'); + } + + /// Reports email verification completion + static void reportEmailVerification() { + AppMetrica.reportEvent('registration_email_verify'); + } + + /// Reports phone verification completion + static void reportPhoneVerification() { + AppMetrica.reportEvent('registration_phone_verify'); + } + + /// Reports registration completion + static void reportRegistrationComplete() { + AppMetrica.reportEvent('registration_complete'); + } + + /// Reports registration error + static void reportRegistrationError(String error) { + AppMetrica.reportEvent('registration_error'); + } +} From 6061e413b2203eb1ef20a88e8ecd0c83ffe127d7 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Mon, 25 Aug 2025 14:40:50 +0300 Subject: [PATCH 118/134] fix: upd ver to 71 --- 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 d6e7401a..4447f176 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -396,7 +396,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 69; + CURRENT_PROJECT_VERSION = 70; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -546,7 +546,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 69; + CURRENT_PROJECT_VERSION = 70; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -584,7 +584,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 69; + CURRENT_PROJECT_VERSION = 70; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 7e814e8a..f0baee2b 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -21,7 +21,7 @@ CFBundleSignature ???? CFBundleVersion - 69 + 70 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/pubspec.yaml b/pubspec.yaml index 10fafb98..1505f232 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: eatvio_app description: A new Flutter project. -version: 1.2.10+70 +version: 1.2.11+71 publish_to: 'none' environment: From bae79b60fdabbefa2682aadd48e2090d78bd7098 Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 25 Aug 2025 17:29:07 +0300 Subject: [PATCH 119/134] fix: testflight --- ios/fastlane/Fastfile | 1 - 1 file changed, 1 deletion(-) diff --git a/ios/fastlane/Fastfile b/ios/fastlane/Fastfile index ebd745be..7ccc8b7a 100644 --- a/ios/fastlane/Fastfile +++ b/ios/fastlane/Fastfile @@ -54,7 +54,6 @@ platform :ios do scheme: "Runner", export_method: "app-store", codesigning_identity: "Apple Distribution", - xcargs: "--dart-define=HIDE_PLANS=true --dart-define=BUILD_DATE='#{build_date}'" ) From b5edf13bd9857a394cb98c7efac0208b3ba83872 Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 25 Aug 2025 17:42:05 +0300 Subject: [PATCH 120/134] fix: testflight --- ios/Runner.xcodeproj/project.pbxproj | 1 - ios/fastlane/Fastfile | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 4447f176..824c4906 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -569,7 +569,6 @@ PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; diff --git a/ios/fastlane/Fastfile b/ios/fastlane/Fastfile index 7ccc8b7a..588f4fc9 100644 --- a/ios/fastlane/Fastfile +++ b/ios/fastlane/Fastfile @@ -54,6 +54,7 @@ platform :ios do scheme: "Runner", export_method: "app-store", codesigning_identity: "Apple Distribution", + xcargs: "--dart-define=HIDE_PLANS=false --dart-define=BUILD_DATE='#{build_date}'" ) From bc6469a6aec42abefee691c7a90060add438cb4d Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Mon, 25 Aug 2025 17:55:34 +0300 Subject: [PATCH 121/134] fix: testflight --- ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 9c12df59..1a6f2553 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -41,7 +41,7 @@ Date: Mon, 25 Aug 2025 18:11:04 +0300 Subject: [PATCH 122/134] fix: testflight --- ios/Flutter/AppFrameworkInfo.plist | 2 +- ios/Podfile | 4 ++-- ios/Runner.xcodeproj/project.pbxproj | 19 ++++++++++--------- ios/Runner/Info.plist | 2 +- ios/fastlane/Fastfile | 3 --- 5 files changed, 14 insertions(+), 16 deletions(-) diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist index 86343d86..8c6e5614 100644 --- a/ios/Flutter/AppFrameworkInfo.plist +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 16.0 + 12.0 diff --git a/ios/Podfile b/ios/Podfile index 4fe34ec9..53a8d390 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -platform :ios, '16.0' +platform :ios, '14.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' @@ -39,7 +39,7 @@ post_install do |installer| flutter_additional_ios_build_settings(target) target.build_configurations.each do |config| config.build_settings["EXCLUDED_ARCHS[sdk=iphonesimulator*]"] = "arm64" - config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '16.0' + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '14.0' config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [ '$(inherited)', ## dart: PermissionGroup.camera diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 824c4906..1da8a682 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -378,7 +378,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -396,7 +396,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 70; + CURRENT_PROJECT_VERSION = 71; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -405,7 +405,7 @@ ); INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Eatvio; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -474,7 +474,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -526,7 +526,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -546,7 +546,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 70; + CURRENT_PROJECT_VERSION = 71; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -555,7 +555,7 @@ ); INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Eatvio; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -569,6 +569,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; @@ -583,7 +584,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 70; + CURRENT_PROJECT_VERSION = 71; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -592,7 +593,7 @@ ); INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Eatvio; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index f0baee2b..5cba2fb5 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -21,7 +21,7 @@ CFBundleSignature ???? CFBundleVersion - 70 + 71 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/ios/fastlane/Fastfile b/ios/fastlane/Fastfile index 588f4fc9..9d9b239d 100644 --- a/ios/fastlane/Fastfile +++ b/ios/fastlane/Fastfile @@ -46,15 +46,12 @@ platform :ios do match(type: "appstore", readonly: true) increment_build_number(xcodeproj: "Runner.xcodeproj") - - build_date = Time.now.getlocal("+03:00").strftime("%Y-%m-%d %H:%M:%S") build_app( workspace: "Runner.xcworkspace", scheme: "Runner", export_method: "app-store", codesigning_identity: "Apple Distribution", - xcargs: "--dart-define=HIDE_PLANS=false --dart-define=BUILD_DATE='#{build_date}'" ) From 997da4f94055bc20e8be697d4df53f3bf11263f4 Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 25 Aug 2025 18:15:49 +0300 Subject: [PATCH 123/134] fix: testflight --- .gitea/workflows/test-flight-cd.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitea/workflows/test-flight-cd.yaml b/.gitea/workflows/test-flight-cd.yaml index ac54d483..181b9362 100644 --- a/.gitea/workflows/test-flight-cd.yaml +++ b/.gitea/workflows/test-flight-cd.yaml @@ -60,7 +60,8 @@ jobs: flutter pub upgrade flutter pub run build_runner build --delete-conflicting-outputs cd ios - pod update + rm -rf Pods Podfile.lock + pod install fastlane beta env: LANG: en_US.UTF-8 From 964550a32582ca33bc77a75788161141d29e9a02 Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 25 Aug 2025 18:19:18 +0300 Subject: [PATCH 124/134] fix: testflight --- ios/Flutter/AppFrameworkInfo.plist | 2 +- ios/Podfile | 4 ++-- ios/Runner.xcodeproj/project.pbxproj | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist index 8c6e5614..86343d86 100644 --- a/ios/Flutter/AppFrameworkInfo.plist +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 12.0 + 16.0 diff --git a/ios/Podfile b/ios/Podfile index 53a8d390..4fe34ec9 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -platform :ios, '14.0' +platform :ios, '16.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' @@ -39,7 +39,7 @@ post_install do |installer| flutter_additional_ios_build_settings(target) target.build_configurations.each do |config| config.build_settings["EXCLUDED_ARCHS[sdk=iphonesimulator*]"] = "arm64" - config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '14.0' + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '16.0' config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [ '$(inherited)', ## dart: PermissionGroup.camera diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 1da8a682..080fb053 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -378,7 +378,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -474,7 +474,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -526,7 +526,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; From dd5692407670effabaaeb7a060fffc537a60609f Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 25 Aug 2025 18:28:15 +0300 Subject: [PATCH 125/134] fix: test-flight --- .gitea/workflows/test-flight-cd.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.gitea/workflows/test-flight-cd.yaml b/.gitea/workflows/test-flight-cd.yaml index 181b9362..a987ba88 100644 --- a/.gitea/workflows/test-flight-cd.yaml +++ b/.gitea/workflows/test-flight-cd.yaml @@ -61,6 +61,11 @@ jobs: flutter pub run build_runner build --delete-conflicting-outputs cd ios rm -rf Pods Podfile.lock + rm -rf .symlinks + rm -rf Flutter/Flutter.framework + rm -rf Flutter/Flutter.podspec + rm -rf Flutter/Generated.xcconfig + rm -rf ~/Library/Developer/Xcode/DerivedData/Runner-* pod install fastlane beta env: From 020682ae8045a630302de21d3ecb0fd194c00d13 Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 25 Aug 2025 18:31:37 +0300 Subject: [PATCH 126/134] fix: testflight --- .gitea/workflows/test-flight-cd.yaml | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/.gitea/workflows/test-flight-cd.yaml b/.gitea/workflows/test-flight-cd.yaml index a987ba88..ac54d483 100644 --- a/.gitea/workflows/test-flight-cd.yaml +++ b/.gitea/workflows/test-flight-cd.yaml @@ -60,13 +60,7 @@ jobs: flutter pub upgrade flutter pub run build_runner build --delete-conflicting-outputs cd ios - rm -rf Pods Podfile.lock - rm -rf .symlinks - rm -rf Flutter/Flutter.framework - rm -rf Flutter/Flutter.podspec - rm -rf Flutter/Generated.xcconfig - rm -rf ~/Library/Developer/Xcode/DerivedData/Runner-* - pod install + pod update fastlane beta env: LANG: en_US.UTF-8 From d8324c1635c52e85d242d3362fbb70f199ebaaf7 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Wed, 27 Aug 2025 11:33:06 +0300 Subject: [PATCH 127/134] feat(verify_email): Issue-645 --- .../repositories/profile_repository.dart | 8 +- .../profile/services/profile_service.dart | 66 ++----- .../verify_email_cubit.dart | 12 ++ .../verify_email_state.dart | 14 +- .../pages/verify_email_screen.dart | 125 ++++++++----- .../widgets/change_email_dialog.dart | 174 ++++++++++++++++++ 6 files changed, 293 insertions(+), 106 deletions(-) create mode 100644 lib/features/registration/presentation/widgets/change_email_dialog.dart diff --git a/lib/features/profile/repositories/profile_repository.dart b/lib/features/profile/repositories/profile_repository.dart index 531cf589..4867d85a 100644 --- a/lib/features/profile/repositories/profile_repository.dart +++ b/lib/features/profile/repositories/profile_repository.dart @@ -164,8 +164,12 @@ class ProfileRepository { return (jsonDecode(await profileService.getMyPhone())['phone']); } - Future sendEmailCode() async { - return await profileService.sendEmailCode(); + Future sendEmailCode({String? newEmail}) async { + if (newEmail != null) { + return await profileService.sendEmailCode(data: {'new_email': newEmail}); + } else { + return await profileService.sendEmailCode(); + } } Future tryVerifyEmailByCode(String code) async { diff --git a/lib/features/profile/services/profile_service.dart b/lib/features/profile/services/profile_service.dart index bddd46f8..b313f27f 100644 --- a/lib/features/profile/services/profile_service.dart +++ b/lib/features/profile/services/profile_service.dart @@ -16,9 +16,7 @@ abstract class ProfileService { factory ProfileService(Dio dio, {String baseUrl}) = _ProfileService; @GET('/api/profile/show/{slug}') - Future getProfileBySlug({ - @Path() required String slug, - }); + Future getProfileBySlug({@Path() required String slug}); @GET('/api/profile/index') Future getMyProfile(); @@ -27,9 +25,7 @@ abstract class ProfileService { Future getMyProfileDetails(); @GET('/api/profile/detailz/{slug}') - Future getProfileDetailsBySlug({ - @Path() required String slug, - }); + Future getProfileDetailsBySlug({@Path() required String slug}); @POST('/api/profile/addCircumference') Future addCircumference({ @@ -39,64 +35,38 @@ abstract class ProfileService { }); @POST('/api/profile/addWeight') - Future addWeight({ - @Part() required double weight, - }); + Future addWeight({@Part() required double weight}); @GET('/api/blogPosts/search_by_author/{slug}') - Future> getBlogPostByAuthorSlug({ - @Path() required String slug, - }); + Future> getBlogPostByAuthorSlug({@Path() required String slug}); @GET('/api/profile/measure/weight/{period}') - Future> getWeight({ - @Path() required String period, - }); + Future> getWeight({@Path() required String period}); @GET('/api/profile/measure/weight/{period}/{slug}') - Future> getWeightBySlug({ - @Path() required String period, - @Path() required String slug, - }); + Future> getWeightBySlug({@Path() required String period, @Path() required String slug}); @GET('/api/profile/measure/circumferences/{period}') - Future> getCircumferences({ - @Path() required String period, - }); + Future> getCircumferences({@Path() required String period}); @GET('/api/profile/measure/circumferences/{period}/{slug}') - Future> getCircumferencesBySlug({ - @Path() required String period, - @Path() required String slug, - }); + Future> getCircumferencesBySlug({@Path() required String period, @Path() required String slug}); @GET('/api/profile/{slug}/subscribers') - Future getSubscribers({ - @Path() required String slug, - @Query('filters[name][\$startsWith]') String? query, - }); + Future getSubscribers({@Path() required String slug, @Query('filters[name][\$startsWith]') String? query}); @GET('/api/profile/{slug}/subscriptions') - Future getSubscriptions({ - @Path() required String slug, - @Query('filters[name][\$startsWith]') String? query, - }); + Future getSubscriptions({@Path() required String slug, @Query('filters[name][\$startsWith]') String? query}); @POST('/api/profile/subscribe/{slug}') - Future subscribe({ - @Path() required String slug, - }); + Future subscribe({@Path() required String slug}); @POST('/api/profile/unsubscribe/{slug}') - Future unsubscribe({ - @Path() required String slug, - }); + Future unsubscribe({@Path() required String slug}); @POST('/api/profile/photo/upload') @MultiPart() - Future uploadPhoto({ - @Part(name: 'photo') required File photo, - }); + Future uploadPhoto({@Part(name: 'photo') required File photo}); @DELETE('/api/profile/photo/delete') Future deletePhoto(); @@ -105,20 +75,16 @@ abstract class ProfileService { Future getChosenDiets(); @POST('/api/profile/make_pass_key') - Future getPassKey({ - @Body() required String json, - }); + Future getPassKey({@Body() required String json}); @GET('/api/profile/my_phone') Future getMyPhone(); @POST('/api/profile/send_email_code') - Future sendEmailCode(); + Future sendEmailCode({@Body() Map data = const {}}); @POST('/api/profile/verificate_email_code') - Future tryVerifyEmailByCode({ - @Body() required String code, - }); + Future tryVerifyEmailByCode({@Body() required String code}); @DELETE('/api/profile/delete_my_account') Future deleteAccount(); diff --git a/lib/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart b/lib/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart index d1f547f8..57554130 100644 --- a/lib/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart +++ b/lib/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart @@ -32,4 +32,16 @@ class VerifyEmailCubit extends Cubit { emit(const VerifyEmailState.verifyError()); } } + + Future changeEmail(String newEmail) async { + try { + emit(const VerifyEmailState.changeEmailLoading()); + await profileRepository.sendEmailCode(newEmail: newEmail); + emit(const VerifyEmailState.changeEmailSuccess()); + // Обновляем email в состоянии после успешной смены + emit(VerifyEmailState.loaded(email: newEmail)); + } catch (e) { + emit(const VerifyEmailState.changeEmailError()); + } + } } diff --git a/lib/features/registration/presentation/bloc/verify_email_cubit/verify_email_state.dart b/lib/features/registration/presentation/bloc/verify_email_cubit/verify_email_state.dart index 718b2cce..6cb91a32 100644 --- a/lib/features/registration/presentation/bloc/verify_email_cubit/verify_email_state.dart +++ b/lib/features/registration/presentation/bloc/verify_email_cubit/verify_email_state.dart @@ -6,13 +6,9 @@ part 'verify_email_state.freezed.dart'; abstract class VerifyEmailState with _$VerifyEmailState { const factory VerifyEmailState.loading() = VerifyEmailStateLoading; - const factory VerifyEmailState.loaded({ - required String email, - }) = VerifyEmailStateLoaded; + const factory VerifyEmailState.loaded({required String email}) = VerifyEmailStateLoaded; - const factory VerifyEmailState.error({ - required String errorMessage, - }) = VerifyEmailStateError; + const factory VerifyEmailState.error({required String errorMessage}) = VerifyEmailStateError; const factory VerifyEmailState() = _VerifyEmailState; @@ -21,4 +17,10 @@ abstract class VerifyEmailState with _$VerifyEmailState { const factory VerifyEmailState.verifySuccess() = VerifyEmailStateVerifySuccess; const factory VerifyEmailState.verifyError() = VerifyEmailStateVerifyError; + + const factory VerifyEmailState.changeEmailLoading() = VerifyEmailStateChangeEmailLoading; + + const factory VerifyEmailState.changeEmailSuccess() = VerifyEmailStateChangeEmailSuccess; + + const factory VerifyEmailState.changeEmailError() = VerifyEmailStateChangeEmailError; } diff --git a/lib/features/registration/presentation/pages/verify_email_screen.dart b/lib/features/registration/presentation/pages/verify_email_screen.dart index e91b9e9e..8d1ef619 100644 --- a/lib/features/registration/presentation/pages/verify_email_screen.dart +++ b/lib/features/registration/presentation/pages/verify_email_screen.dart @@ -7,6 +7,7 @@ import 'package:eatvio_app/core/utils/toasts.dart'; import 'package:eatvio_app/core/widgets/custom_outlined_button.dart'; import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart'; import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_state.dart'; +import 'package:eatvio_app/features/registration/presentation/widgets/change_email_dialog.dart'; import 'package:eatvio_app/features/registration/services/registration_analytics_service.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -27,6 +28,7 @@ class _VerifyEmailScreenState extends State { late Timer _timer; int _secondsRemaining = 60; bool _canSendAgain = false; + @override void initState() { super.initState(); @@ -75,55 +77,82 @@ class _VerifyEmailScreenState extends State { Widget build(BuildContext context) { return BlocProvider( create: (_) => sl()..sendEmailCode(), - child: BlocConsumer( - listener: (context, state) { - if (state is VerifyEmailStateLoaded) { - setState(() { - email = state.email; - }); - } else if (state is VerifyEmailStateVerifySuccess) { - showSuccessToast('Почта успешно подтверждена'); - goRouter.pushNamed('feedScreen'); - } else if (state is VerifyEmailStateVerifyError) { - showErrorToast("Неверный код. Попробуйте снова."); - for (var controller in _controllers) { - controller.clear(); - } - FocusScope.of(context).requestFocus(_focusNodes[0]); - } - }, - builder: (context, state) { - return Scaffold( - appBar: AppBar(title: const Text("Подтвердите почту")), - body: Padding( - padding: const EdgeInsets.all(24.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text("Код отправлен на почту\n$email", textAlign: TextAlign.center), - const SizedBox(height: 20), - Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: List.generate(4, (index) => _buildCodeField(context, index))), - const SizedBox(height: 40), - CustomOutlinedButton( - text: 'Запросить код повторно', - height: 52, - isDisabled: !_canSendAgain, - buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), - buttonStyle: ButtonStyle( - side: const WidgetStatePropertyAll(BorderSide.none), - backgroundColor: WidgetStatePropertyAll(_canSendAgain ? appTheme.blueA400 : appTheme.gray), - ), - onPressed: () async { - await context.read().sendEmailCode(); - _startTimer(); - }, + child: Builder( + builder: (context) { + return BlocConsumer( + listener: (context, state) { + if (state is VerifyEmailStateLoaded) { + setState(() { + email = state.email; + }); + _startTimer(); + } else if (state is VerifyEmailStateVerifySuccess) { + showSuccessToast('Почта успешно подтверждена'); + goRouter.pushNamed('feedScreen'); + } else if (state is VerifyEmailStateVerifyError) { + showErrorToast("Неверный код. Попробуйте снова."); + for (var controller in _controllers) { + controller.clear(); + } + FocusScope.of(context).requestFocus(_focusNodes[0]); + } else if (state is VerifyEmailStateChangeEmailSuccess) { + showSuccessToast('Код подтверждения отправлен на новый email'); + // Диалог закроется автоматически в ChangeEmailDialog при успешной смене + } else if (state is VerifyEmailStateChangeEmailError) { + showErrorToast('Ошибка при смене email. Попробуйте снова.'); + } + }, + builder: (context, state) { + return Scaffold( + appBar: AppBar(title: const Text("Подтвердите почту")), + body: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text("Код отправлен на почту\n$email", textAlign: TextAlign.center), + const SizedBox(height: 20), + Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: List.generate(4, (index) => _buildCodeField(context, index))), + const SizedBox(height: 40), + CustomOutlinedButton( + text: 'Запросить код повторно', + height: 52, + isDisabled: !_canSendAgain, + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), + buttonStyle: ButtonStyle( + side: const WidgetStatePropertyAll(BorderSide.none), + backgroundColor: WidgetStatePropertyAll(_canSendAgain ? appTheme.blueA400 : appTheme.gray), + ), + onPressed: () async { + await context.read().sendEmailCode(); + _startTimer(); + }, + ), + const SizedBox(height: 8), + if (!_canSendAgain) + Text("Зпросить код повторно можно через $_secondsRemaining с.", style: TextStyles.textSMedium.copyWith(color: appTheme.gray)), + const SizedBox(height: 16), + CustomOutlinedButton( + text: 'Сменить email', + height: 52, + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.blueA400), + buttonStyle: ButtonStyle( + side: WidgetStatePropertyAll(BorderSide(color: appTheme.blueA400)), + backgroundColor: const WidgetStatePropertyAll(Colors.transparent), + ), + onPressed: () { + final verifyEmailCubit = context.read(); + showDialog( + context: context, + builder: (context) => ChangeEmailDialog(currentEmail: email, verifyEmailCubit: verifyEmailCubit), + ); + }, + ), + ], ), - const SizedBox(height: 8), - if (!_canSendAgain) - Text("Зпросить код повторно можно через $_secondsRemaining с.", style: TextStyles.textSMedium.copyWith(color: appTheme.gray)), - ], - ), - ), + ), + ); + }, ); }, ), diff --git a/lib/features/registration/presentation/widgets/change_email_dialog.dart b/lib/features/registration/presentation/widgets/change_email_dialog.dart new file mode 100644 index 00000000..266e9f04 --- /dev/null +++ b/lib/features/registration/presentation/widgets/change_email_dialog.dart @@ -0,0 +1,174 @@ +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/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart'; +import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_state.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class ChangeEmailDialog extends StatefulWidget { + final String currentEmail; + final VerifyEmailCubit verifyEmailCubit; + + const ChangeEmailDialog({super.key, required this.currentEmail, required this.verifyEmailCubit}); + + @override + State createState() => _ChangeEmailDialogState(); +} + +class _ChangeEmailDialogState extends State { + final TextEditingController _emailController = TextEditingController(); + final FocusNode _emailFocusNode = FocusNode(); + bool _isLoading = false; + String? _errorText; + + @override + void initState() { + super.initState(); + _emailController.text = widget.currentEmail; + } + + @override + void dispose() { + _emailController.dispose(); + _emailFocusNode.dispose(); + super.dispose(); + } + + bool _isValidEmail(String email) { + return RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(email); + } + + void _validateEmail() { + final email = _emailController.text.trim(); + if (email.isEmpty) { + setState(() { + _errorText = 'Введите email'; + }); + } else if (!_isValidEmail(email)) { + setState(() { + _errorText = 'Введите корректный email'; + }); + } else if (email == widget.currentEmail) { + setState(() { + _errorText = 'Новый email должен отличаться от текущего'; + }); + } else { + setState(() { + _errorText = null; + }); + } + } + + Future _changeEmail() async { + _validateEmail(); + if (_errorText != null) return; + + setState(() { + _isLoading = true; + }); + + try { + await widget.verifyEmailCubit.changeEmail(_emailController.text.trim()); + // Диалог закроется автоматически при успешной смене email через основной экран + } catch (e) { + if (mounted) { + showErrorToast('Ошибка при смене email. Попробуйте снова.'); + setState(() { + _isLoading = false; + }); + } + } + } + + @override + Widget build(BuildContext context) { + return BlocProvider.value( + value: widget.verifyEmailCubit, + child: BlocListener( + listener: (context, state) { + if (state is VerifyEmailStateChangeEmailSuccess) { + Navigator.of(context).pop(); // Закрываем диалог при успешной смене + } else if (state is VerifyEmailStateChangeEmailError) { + setState(() { + _isLoading = false; + }); + } + }, + child: Dialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Сменить email', style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)), + const SizedBox(height: 16), + Text('Введите новый email для получения кода подтверждения', style: TextStyles.textSMedium.copyWith(color: appTheme.gray)), + const SizedBox(height: 20), + TextField( + controller: _emailController, + focusNode: _emailFocusNode, + keyboardType: TextInputType.emailAddress, + decoration: InputDecoration( + hintText: 'Введите новый email', + hintStyle: TextStyles.textSMedium.copyWith(color: appTheme.gray), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: appTheme.gray), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: appTheme.blueA400), + ), + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: appTheme.red), + ), + errorText: _errorText, + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), + ), + onChanged: (_) => _validateEmail(), + onSubmitted: (_) => _changeEmail(), + ), + const SizedBox(height: 24), + Row( + children: [ + Expanded( + child: CustomOutlinedButton( + text: 'Отмена', + height: 52, + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.black), + buttonStyle: ButtonStyle( + side: const WidgetStatePropertyAll(BorderSide.none), + backgroundColor: WidgetStatePropertyAll(appTheme.lightGray0), + ), + onPressed: () => Navigator.of(context).pop(), + ), + ), + const SizedBox(width: 12), + Expanded( + child: CustomOutlinedButton( + text: _isLoading ? 'Отправка...' : 'Сменить', + height: 52, + isDisabled: _isLoading, + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), + buttonStyle: ButtonStyle( + side: const WidgetStatePropertyAll(BorderSide.none), + backgroundColor: WidgetStatePropertyAll(_isLoading ? appTheme.gray : appTheme.blueA400), + ), + onPressed: _isLoading ? null : _changeEmail, + ), + ), + ], + ), + ], + ), + ), + ), + ), + ); + } +} From 1680bd2556d9002d95909c9403788537574797f4 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Wed, 27 Aug 2025 12:04:20 +0300 Subject: [PATCH 128/134] fix: testflight --- .gitea/workflows/test-flight-cd.yaml | 2 ++ ios/Runner.xcodeproj/project.pbxproj | 8 -------- ios/fastlane/Fastfile | 4 ++++ 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/.gitea/workflows/test-flight-cd.yaml b/.gitea/workflows/test-flight-cd.yaml index ac54d483..4353f3be 100644 --- a/.gitea/workflows/test-flight-cd.yaml +++ b/.gitea/workflows/test-flight-cd.yaml @@ -59,6 +59,8 @@ 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 update fastlane beta diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 080fb053..7d740632 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -213,14 +213,10 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; @@ -287,14 +283,10 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); name = "[CP] Copy Pods Resources"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", ); - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; diff --git a/ios/fastlane/Fastfile b/ios/fastlane/Fastfile index 9d9b239d..3a74adf3 100644 --- a/ios/fastlane/Fastfile +++ b/ios/fastlane/Fastfile @@ -44,6 +44,10 @@ platform :ios do UI.message "Using auth_key_path: #{auth_key_path}" + # Configure Flutter for release mode + UI.message "Configuring Flutter for release mode..." + sh("cd .. && flutter build ios --config-only --release") + match(type: "appstore", readonly: true) increment_build_number(xcodeproj: "Runner.xcodeproj") From 12bf9f9e5f7177d40e357c33e0707faa5155079c Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Wed, 27 Aug 2025 14:39:22 +0300 Subject: [PATCH 129/134] feat(verify_email): add dialog --- .../pages/verify_email_screen.dart | 17 +++- .../widgets/code_not_received_dialog.dart | 82 +++++++++++++++++++ 2 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 lib/features/registration/presentation/widgets/code_not_received_dialog.dart diff --git a/lib/features/registration/presentation/pages/verify_email_screen.dart b/lib/features/registration/presentation/pages/verify_email_screen.dart index 8d1ef619..4ade2581 100644 --- a/lib/features/registration/presentation/pages/verify_email_screen.dart +++ b/lib/features/registration/presentation/pages/verify_email_screen.dart @@ -8,6 +8,7 @@ import 'package:eatvio_app/core/widgets/custom_outlined_button.dart'; import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart'; import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_state.dart'; import 'package:eatvio_app/features/registration/presentation/widgets/change_email_dialog.dart'; +import 'package:eatvio_app/features/registration/presentation/widgets/code_not_received_dialog.dart'; import 'package:eatvio_app/features/registration/services/registration_analytics_service.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -113,7 +114,21 @@ class _VerifyEmailScreenState extends State { Text("Код отправлен на почту\n$email", textAlign: TextAlign.center), const SizedBox(height: 20), Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: List.generate(4, (index) => _buildCodeField(context, index))), - const SizedBox(height: 40), + const SizedBox(height: 16), + GestureDetector( + onTap: () { + final verifyEmailCubit = context.read(); + showDialog(context: context, builder: (context) => CodeNotReceivedDialog( + currentEmail: email, + verifyEmailCubit: verifyEmailCubit, + )); + }, + child: Text( + 'Не приходит код?', + style: TextStyles.textSMedium.copyWith(color: appTheme.blueA400, decoration: TextDecoration.underline), + ), + ), + const SizedBox(height: 24), CustomOutlinedButton( text: 'Запросить код повторно', height: 52, diff --git a/lib/features/registration/presentation/widgets/code_not_received_dialog.dart b/lib/features/registration/presentation/widgets/code_not_received_dialog.dart new file mode 100644 index 00000000..c59d80d9 --- /dev/null +++ b/lib/features/registration/presentation/widgets/code_not_received_dialog.dart @@ -0,0 +1,82 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/gestures.dart'; +import 'package:eatvio_app/core/theme/custom_text_style.dart'; +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/features/registration/presentation/widgets/change_email_dialog.dart'; +import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart'; + +class CodeNotReceivedDialog extends StatelessWidget { + final String currentEmail; + final VerifyEmailCubit verifyEmailCubit; + + const CodeNotReceivedDialog({ + super.key, + required this.currentEmail, + required this.verifyEmailCubit + }); + + void _showChangeEmailDialog(BuildContext context) { + Navigator.of(context).pop(); // Закрываем текущий диалог + showDialog( + context: context, + builder: (context) => ChangeEmailDialog( + currentEmail: currentEmail, + verifyEmailCubit: verifyEmailCubit, + ), + ); + } + + @override + Widget build(BuildContext context) { + return Dialog( + backgroundColor: Colors.transparent, + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16.0), + border: Border.all(color: Colors.grey[800]!, width: 2.0), + ), + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Почему может не приходить код?', style: TextStyles.textLSemibold.copyWith(color: appTheme.black)), + const SizedBox(height: 16), + RichText( + text: TextSpan( + style: TextStyles.textMMedium.copyWith(color: appTheme.gray600), + children: [ + const TextSpan(text: 'Опечатка в email: Если адрес введён неправильно, код не придёт. '), + TextSpan( + text: 'Ввести email повторно.', + style: TextStyles.textMMedium.copyWith( + color: appTheme.blueA400, + decoration: TextDecoration.underline, + ), + recognizer: TapGestureRecognizer() + ..onTap = () => _showChangeEmailDialog(context), + ), + const TextSpan(text: '\n\nПроверьте спам/промо/мусорную папку: Код может попасть туда из-за фильтров почтового провайдера\n\nПодождите 1-2 минуты: Иногда email задерживается из-за сетевых проблем.\n\nУбедитесь в стабильном соединении: Проверьте интернет и уведомления на устройстве.'), + ], + ), + ), + const SizedBox(height: 24), + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () => Navigator.of(context).pop(), + style: ElevatedButton.styleFrom( + backgroundColor: appTheme.blueA400, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + ), + child: Text('Понятно', style: TextStyles.textMSemibold.copyWith(color: Colors.white)), + ), + ), + ], + ), + ), + ); + } +} From d13eb78f0e660b9019977ff463bd733240e38db6 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Fri, 29 Aug 2025 14:25:13 +0300 Subject: [PATCH 130/134] feat(notifications): Issue-236 --- lib/core/app_router.dart | 177 +++++------- lib/core/locator_service.dart | 41 ++- .../models/notification_settings_model.dart | 39 +++ .../notification_settings_cubit.dart | 33 +++ .../notification_settings_state.dart | 31 +++ .../menu/presentation/pages/menu_screen.dart | 2 + .../pages/notifications_settings_screen.dart | 262 ++++++++++++++++++ .../notification_settings_repository.dart | 16 ++ .../notification_settings_service.dart | 16 ++ .../presentation/pages/settings_screen.dart | 5 +- 10 files changed, 483 insertions(+), 139 deletions(-) create mode 100644 lib/features/menu/models/notification_settings_model.dart create mode 100644 lib/features/menu/presentation/bloc/notification_settings_cubit/notification_settings_cubit.dart create mode 100644 lib/features/menu/presentation/bloc/notification_settings_cubit/notification_settings_state.dart create mode 100644 lib/features/menu/presentation/pages/notifications_settings_screen.dart create mode 100644 lib/features/menu/repositories/notification_settings_repository.dart create mode 100644 lib/features/menu/services/notification_settings_service.dart diff --git a/lib/core/app_router.dart b/lib/core/app_router.dart index c691b0b2..cc028255 100644 --- a/lib/core/app_router.dart +++ b/lib/core/app_router.dart @@ -24,6 +24,7 @@ import 'package:eatvio_app/features/consultations/presentation/pages/consultatio 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'; @@ -111,23 +112,13 @@ class AppRouter { name: 'authScreen', builder: (context, state) => const AuthScreen(), routes: [ - GoRoute( - path: 'registration-screen', - name: 'registrationScreen', - builder: (context, state) => const RegistrationScreen(), - ), + 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(), + builder: (context, state) => ForgetPasswordScreen(isAuth: state.extra as bool), ), + GoRoute(path: 'scan-qr-code-screen', name: 'scanQRCodeScreen', builder: (context, state) => const ScanQRCodeScreen()), ], ), ShellRoute( @@ -165,19 +156,23 @@ class AppRouter { GoRoute( path: '/diary-screen', name: 'diaryScreen', - pageBuilder: (context, state) => const NoTransitionPage( - child: 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())), + path: ConsultationsScreen.path, + name: ConsultationsScreen.name, + pageBuilder: (context, state) => const NoTransitionPage(child: ConsultationsScreen()), + ), GoRoute( - path: '/notifications-screen', - name: 'notificationsScreen', - pageBuilder: (context, state) => const NoTransitionPage(child: NotificationsScreen())), + 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', @@ -194,11 +189,7 @@ class AppRouter { ), ], ), - GoRoute( - name: 'blockedUsersScreen', - path: '/blocked-users-screen', - builder: (context, state) => const BlockedUsersScreen(), - ), + GoRoute(name: 'blockedUsersScreen', path: '/blocked-users-screen', builder: (context, state) => const BlockedUsersScreen()), GoRoute( path: '/welcome-screen', name: 'welcomeScreen', @@ -241,26 +232,10 @@ class AppRouter { } }, ), - 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: 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, @@ -275,11 +250,7 @@ class AppRouter { builder: (context, state) { final slug = state.uri.queryParameters['slug']; if (slug != null) { - return AboutPlanScreen( - slug: slug, - navigateToComments: false, - index: null, - ); + return AboutPlanScreen(slug: slug, navigateToComments: false, index: null); } final data = state.extra as Map; @@ -293,29 +264,23 @@ class AppRouter { builder: (context, state) { final slug = state.uri.queryParameters['slug']; if (slug != null) { - return AboutRecipeScreen( - recipeSlug: slug, - navigateToComments: false, - index: 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, - ); + 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, - ), + 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: '/profile-screen', name: 'profileScreen', builder: (context, state) => ProfileScreen(author: state.extra as Author)), GoRoute( path: '/subs-screen/:slug', name: 'subsScreen', @@ -327,11 +292,7 @@ class AppRouter { builder: (context, state) { final slug = state.uri.queryParameters['slug']; if (slug != null) { - return AboutDietScreen( - slug: slug, - index: null, - navigateToComments: false, - ); + 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); @@ -343,19 +304,18 @@ class AppRouter { 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, - ); - } + 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); - }), + return BlogPostScreen(id: state.extra as int); + }, + ), GoRoute( path: '/chat-screen', name: 'chatScreen', @@ -363,10 +323,7 @@ class AppRouter { final data = state.extra as Map; final uuid = data['uuid'] as String; final sendInitialMessage = data['sendInitialMessage'] as bool?; - return ChatScreen( - uuid: uuid, - sendInitialMessage: sendInitialMessage, - ); + return ChatScreen(uuid: uuid, sendInitialMessage: sendInitialMessage); }, ), GoRoute( @@ -399,7 +356,11 @@ class AppRouter { 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: 'pressure-screen', + name: 'pressureScreen', + builder: (context, state) => PressureScreen(settingsEntity: state.extra as SettingsEntity), + ), GoRoute( path: 'change-password-screen', name: 'changePasswordScreen', @@ -416,9 +377,7 @@ class AppRouter { GoRoute( path: '/stepper-become-consultant-screen', name: 'stepperBecomeConsultantScreen', - builder: (context, state) => StepperBecomeConsultantScreen( - slug: state.extra == null ? null : state.extra as String, - ), + 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()), @@ -446,7 +405,11 @@ class AppRouter { 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: 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( @@ -456,10 +419,7 @@ class AppRouter { final data = state.extra as Map; final planId = data['planId'] as int; final successfulPayment = data['successfulPayment'] as bool?; - return InfoByOrderScreen( - planId: planId, - successfulPayment: successfulPayment, - ); + return InfoByOrderScreen(planId: planId, successfulPayment: successfulPayment); }, ), GoRoute( @@ -469,10 +429,7 @@ class AppRouter { final data = state.extra as Map; final planId = data['planId'] as int; final url = data['url'] as String; - return PaymentScreen( - url: url, - planId: planId, - ); + return PaymentScreen(url: url, planId: planId); }, ), GoRoute( @@ -482,10 +439,7 @@ class AppRouter { final data = state.extra as Map; final diaryId = data['diaryId'] as int; final currentPlanDay = data['currentDiaryDay'] as int; - return UserDiaryScreen( - diaryId: diaryId, - currentPlanDay: currentPlanDay, - ); + return UserDiaryScreen(diaryId: diaryId, currentPlanDay: currentPlanDay); }, ), GoRoute( @@ -493,16 +447,9 @@ class AppRouter { 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: '/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()), ], ); } diff --git a/lib/core/locator_service.dart b/lib/core/locator_service.dart index 9d0b5297..971ebb2c 100644 --- a/lib/core/locator_service.dart +++ b/lib/core/locator_service.dart @@ -43,8 +43,11 @@ import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/new_ 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'; @@ -136,14 +139,16 @@ Future init() async { sl.registerFactory(() => PlansCubit(feedRepository: sl(), planRepository: sl())); sl.registerFactory(() => SettingsCubit(profileRepository: sl(), settingsRepository: sl(), consultantRepository: sl())); sl.registerFactory(() => RegisterConsultantCubit(consultantRepository: sl())); - sl.registerFactory(() => DiaryCubit( - chatRepository: sl(), - diaryRepository: sl(), - glossaryRepository: sl(), - profileRepository: sl(), - dietRepository: sl(), - settingsRepository: 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())); @@ -172,6 +177,7 @@ Future init() async { sl.registerFactory(() => ResetPasswordCubit(resetPasswordRepository: sl())); sl.registerFactory(() => BlogPostCubit(sl())); sl.registerFactory(() => BlockedUsersCubit(blockedUsersRepository: sl())); + sl.registerFactory(() => NotificationSettingsCubit(notificationSettingsRepository: sl())); sl.registerFactory(() => ConsultantRatingCubit(sl())); // Repository @@ -204,6 +210,7 @@ Future init() async { sl.registerFactory(() => ResetPasswordRepository(resetPasswordService: sl())); sl.registerLazySingleton(() => BlockedUsersRepository(blockedUserService: sl())); sl.registerLazySingleton(() => CommentsRepository(service: sl())); + sl.registerLazySingleton(() => NotificationSettingsRepository(sl())); sl.registerFactory(() => ConsultantRatingRepository(sl())); // Services @@ -239,22 +246,12 @@ Future init() async { sl.registerLazySingleton(() => CommentsService(sl())); sl.registerLazySingleton(() => MyRecipesService(sl())); sl.registerLazySingleton(() => ConsultantRatingService(sl())); + sl.registerLazySingleton(() => NotificationSettingsService(sl())); // External sl.registerLazySingleton( - () => Dio( - BaseOptions( - baseUrl: baseUrl, - headers: { - 'accept': 'application/json', - 'content-type': 'application/json', - }, - ), - )..interceptors.addAll( - [ - TokenInterceptor(), - PhoneVerifyErrorInterceptor(), - ], - ), + () => + Dio(BaseOptions(baseUrl: baseUrl, headers: {'accept': 'application/json', 'content-type': 'application/json'})) + ..interceptors.addAll([TokenInterceptor(), PhoneVerifyErrorInterceptor()]), ); } diff --git a/lib/features/menu/models/notification_settings_model.dart b/lib/features/menu/models/notification_settings_model.dart new file mode 100644 index 00000000..2879a0ab --- /dev/null +++ b/lib/features/menu/models/notification_settings_model.dart @@ -0,0 +1,39 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'notification_settings_model.g.dart'; +part 'notification_settings_model.freezed.dart'; + +@freezed +abstract class NotificationSettingsModel with _$NotificationSettingsModel { + const factory NotificationSettingsModel({ + @JsonKey(name: 'new_updates_notification') required bool newUpdatesNotification, + @JsonKey(name: 'weight_measurements_notification') required bool weightMeasurementsNotification, + @JsonKey(name: 'is_email_notifications_enabled') required bool isEmailNotificationsEnabled, + @JsonKey(name: 'is_push_notifications_enabled') required bool isPushNotificationsEnabled, + @JsonKey(name: 'diary_reminder_notifications') required DiaryReminderNotifications diaryReminderNotifications, + }) = _NotificationSettingsModel; + + factory NotificationSettingsModel.fromJson(Map json) => _$NotificationSettingsModelFromJson(json); +} + +@freezed +abstract class DiaryReminderNotifications with _$DiaryReminderNotifications { + const factory DiaryReminderNotifications({ + @JsonKey(name: 'first_reminder') required ReminderSettings firstReminder, + @JsonKey(name: 'second_reminder') required ReminderSettings secondReminder, + @JsonKey(name: 'third_reminder') required ReminderSettings thirdReminder, + }) = _DiaryReminderNotifications; + + factory DiaryReminderNotifications.fromJson(Map json) => _$DiaryReminderNotificationsFromJson(json); +} + +@freezed +abstract class ReminderSettings with _$ReminderSettings { + const factory ReminderSettings({ + required bool enabled, + @JsonKey(name: 'local_time_hours') required int localTimeHours, + @JsonKey(name: 'local_time_mins') required int localTimeMins, + }) = _ReminderSettings; + + factory ReminderSettings.fromJson(Map json) => _$ReminderSettingsFromJson(json); +} diff --git a/lib/features/menu/presentation/bloc/notification_settings_cubit/notification_settings_cubit.dart b/lib/features/menu/presentation/bloc/notification_settings_cubit/notification_settings_cubit.dart new file mode 100644 index 00000000..6c5c567c --- /dev/null +++ b/lib/features/menu/presentation/bloc/notification_settings_cubit/notification_settings_cubit.dart @@ -0,0 +1,33 @@ +import 'dart:developer'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:eatvio_app/features/menu/presentation/bloc/notification_settings_cubit/notification_settings_state.dart'; +import 'package:eatvio_app/features/menu/repositories/notification_settings_repository.dart'; + +class NotificationSettingsCubit extends Cubit { + final NotificationSettingsRepository _notificationSettingsRepository; + + NotificationSettingsCubit({required NotificationSettingsRepository notificationSettingsRepository}) + : _notificationSettingsRepository = notificationSettingsRepository, + super(const NotificationSettingsStateLoading()); + + Future getNotificationSettings() async { + emit(const NotificationSettingsStateLoading()); + try { + final notificationSettings = await _notificationSettingsRepository.getNotificationSettings(); + emit(NotificationSettingsStateLoaded(notificationSettings: notificationSettings)); + } catch (e, stackTrace) { + log('Error getting notification settings: $e', stackTrace: stackTrace); + emit(NotificationSettingsStateError(errorMessage: e.toString())); + } + } + + Future updateNotificationSettings(Map data) async { + try { + final notificationSettings = await _notificationSettingsRepository.updateNotificationSettings(data); + emit(NotificationSettingsStateLoaded(notificationSettings: notificationSettings)); + } catch (e, stackTrace) { + log('Error updating notification settings: $e', stackTrace: stackTrace); + emit(NotificationSettingsStateError(errorMessage: e.toString())); + } + } +} diff --git a/lib/features/menu/presentation/bloc/notification_settings_cubit/notification_settings_state.dart b/lib/features/menu/presentation/bloc/notification_settings_cubit/notification_settings_state.dart new file mode 100644 index 00000000..ef461f7c --- /dev/null +++ b/lib/features/menu/presentation/bloc/notification_settings_cubit/notification_settings_state.dart @@ -0,0 +1,31 @@ +import 'package:equatable/equatable.dart'; +import 'package:eatvio_app/features/menu/models/notification_settings_model.dart'; + +abstract class NotificationSettingsState extends Equatable { + const NotificationSettingsState(); + + @override + List get props => []; +} + +class NotificationSettingsStateLoading extends NotificationSettingsState { + const NotificationSettingsStateLoading(); +} + +class NotificationSettingsStateLoaded extends NotificationSettingsState { + final NotificationSettingsModel notificationSettings; + + const NotificationSettingsStateLoaded({required this.notificationSettings}); + + @override + List get props => [notificationSettings]; +} + +class NotificationSettingsStateError extends NotificationSettingsState { + final String errorMessage; + + const NotificationSettingsStateError({required this.errorMessage}); + + @override + List get props => [errorMessage]; +} diff --git a/lib/features/menu/presentation/pages/menu_screen.dart b/lib/features/menu/presentation/pages/menu_screen.dart index 2ebce8f4..5c350d3c 100644 --- a/lib/features/menu/presentation/pages/menu_screen.dart +++ b/lib/features/menu/presentation/pages/menu_screen.dart @@ -67,6 +67,8 @@ class MenuScreen extends StatelessWidget { _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')), diff --git a/lib/features/menu/presentation/pages/notifications_settings_screen.dart b/lib/features/menu/presentation/pages/notifications_settings_screen.dart new file mode 100644 index 00000000..2c7f4e04 --- /dev/null +++ b/lib/features/menu/presentation/pages/notifications_settings_screen.dart @@ -0,0 +1,262 @@ +import 'package:eatvio_app/core/locator_service.dart'; +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:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class NotificationsSettingsScreen extends StatelessWidget { + const NotificationsSettingsScreen({super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider(create: (context) => sl()..getNotificationSettings(), child: const NotificationsSettingsContent()); + } +} + +class NotificationsSettingsContent extends StatelessWidget { + const NotificationsSettingsContent({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Уведомления')), + body: BlocBuilder( + builder: (context, state) { + if (state is NotificationSettingsStateLoading) { + return const Center(child: CircularProgressIndicator()); + } else if (state is NotificationSettingsStateError) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('Ошибка: ${state.errorMessage}'), + ElevatedButton( + onPressed: () { + context.read().getNotificationSettings(); + }, + child: const Text('Повторить'), + ), + ], + ), + ); + } else if (state is NotificationSettingsStateLoaded) { + return _buildSettingsContent(context, state.notificationSettings); + } + return const Center(child: Text('Нет данных')); + }, + ), + ); + } + + Widget _buildSettingsContent(BuildContext context, NotificationSettingsModel notificationSettings) { + final cubit = context.read(); + return Column( + children: [ + _tile( + padding: const EdgeInsets.symmetric(horizontal: 16), + onTap: () {}, + title: 'Уведомление о новых обновлениях', + subtitle: null, + trailing: CupertinoSwitch( + value: notificationSettings.newUpdatesNotification, + activeTrackColor: appTheme.blueA400, + onChanged: (newValue) { + _updateNotificationSetting(cubit, {'new_updates_notification': newValue}); + }, + ), + ), + _divider(context), + _tile( + padding: const EdgeInsets.symmetric(horizontal: 16), + onTap: () {}, + title: 'Уведомления о взвешивании', + subtitle: null, + trailing: CupertinoSwitch( + value: notificationSettings.weightMeasurementsNotification, + activeTrackColor: appTheme.blueA400, + onChanged: (newValue) { + _updateNotificationSetting(cubit, {'weight_measurements_notification': newValue}); + }, + ), + ), + _divider(context), + _tile( + padding: const EdgeInsets.symmetric(horizontal: 16), + onTap: () {}, + title: 'Email уведомления', + subtitle: null, + trailing: CupertinoSwitch( + value: notificationSettings.isEmailNotificationsEnabled, + activeTrackColor: appTheme.blueA400, + onChanged: (newValue) { + _updateNotificationSetting(cubit, {'is_email_notifications_enabled': newValue}); + }, + ), + ), + _divider(context), + _tile( + padding: const EdgeInsets.symmetric(horizontal: 16), + onTap: () {}, + title: 'Push уведомления', + subtitle: null, + trailing: CupertinoSwitch( + value: notificationSettings.isPushNotificationsEnabled, + activeTrackColor: appTheme.blueA400, + onChanged: (newValue) { + _updateNotificationSetting(cubit, {'is_push_notifications_enabled': newValue}); + }, + ), + ), + _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), + ), + ), + _buildReminderTile(context, 'Первое напоминание', diaryReminderNotifications.firstReminder, 'first_reminder', cubit), + _buildReminderTile(context, 'Второе напоминание', diaryReminderNotifications.secondReminder, 'second_reminder', cubit), + _buildReminderTile(context, 'Третье напоминание', diaryReminderNotifications.thirdReminder, 'third_reminder', cubit), + ], + ); + } + + void _updateNotificationSetting(NotificationSettingsCubit cubit, Map data) { + cubit.updateNotificationSettings(data); + } + + void _updateReminderSetting(NotificationSettingsCubit cubit, String reminderType, Map data) { + final updateData = { + 'diary_reminder_notifications': {reminderType: data}, + }; + cubit.updateNotificationSettings(updateData); + } + + 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), + Text('(нажмите для изменения)', style: TextStyle(fontSize: 12, color: Colors.grey)), + ], + ) + : null, + trailing: Row( + mainAxisSize: MainAxisSize.min, + 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, + }); + }, + ), + ], + ), + ); + } + + void _showTimePicker(BuildContext context, ReminderSettings reminder, String reminderType, NotificationSettingsCubit cubit) { + int selectedHour = reminder.localTimeHours; + int selectedMinute = reminder.localTimeMins; + + showCupertinoModalPopup( + context: context, + builder: (BuildContext context) { + return Container( + height: 300, + padding: const EdgeInsets.only(top: 6.0), + margin: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), + color: CupertinoColors.systemBackground.resolveFrom(context), + child: SafeArea( + top: false, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + CupertinoButton(child: const Text('Отмена'), onPressed: () => Navigator.of(context).pop()), + CupertinoButton( + child: const Text('Готово'), + onPressed: () { + _updateReminderSetting(cubit, reminderType, { + 'enabled': reminder.enabled, + 'local_time_hours': selectedHour, + 'local_time_mins': selectedMinute, + }); + Navigator.of(context).pop(); + }, + ), + ], + ), + Expanded( + child: CupertinoDatePicker( + mode: CupertinoDatePickerMode.time, + use24hFormat: true, + initialDateTime: DateTime(2024, 1, 1, selectedHour, selectedMinute), + onDateTimeChanged: (DateTime newDateTime) { + selectedHour = newDateTime.hour; + selectedMinute = newDateTime.minute; + }, + ), + ), + ], + ), + ), + ); + }, + ); + } + + ListTile _tile({required String title, required Function()? onTap, Widget? trailing, EdgeInsets? padding, Widget? subtitle}) { + return ListTile( + contentPadding: padding, + onTap: onTap, + title: Text(title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500, height: 1)), + subtitle: subtitle, + trailing: trailing ?? const Icon(Icons.arrow_forward_ios_rounded, size: 12, color: Colors.black38), + ); + } + + Center _divider(BuildContext context) { + return Center( + child: Container( + height: 0.4, + width: MediaQuery.of(context).size.width - 32, + decoration: BoxDecoration(color: Colors.black26, borderRadius: BorderRadius.circular(16)), + ), + ); + } +} diff --git a/lib/features/menu/repositories/notification_settings_repository.dart b/lib/features/menu/repositories/notification_settings_repository.dart new file mode 100644 index 00000000..e0aeaa31 --- /dev/null +++ b/lib/features/menu/repositories/notification_settings_repository.dart @@ -0,0 +1,16 @@ +import 'package:eatvio_app/features/menu/models/notification_settings_model.dart'; +import 'package:eatvio_app/features/menu/services/notification_settings_service.dart'; + +class NotificationSettingsRepository { + final NotificationSettingsService _notificationSettingsService; + + NotificationSettingsRepository(this._notificationSettingsService); + + Future getNotificationSettings() async { + return await _notificationSettingsService.getNotificationSettings(); + } + + Future updateNotificationSettings(Map data) async { + return await _notificationSettingsService.updateNotificationSettings(data: data); + } +} diff --git a/lib/features/menu/services/notification_settings_service.dart b/lib/features/menu/services/notification_settings_service.dart new file mode 100644 index 00000000..f489267f --- /dev/null +++ b/lib/features/menu/services/notification_settings_service.dart @@ -0,0 +1,16 @@ +import 'package:dio/dio.dart'; +import 'package:eatvio_app/features/menu/models/notification_settings_model.dart'; +import 'package:retrofit/retrofit.dart'; + +part 'notification_settings_service.g.dart'; + +@RestApi() +abstract class NotificationSettingsService { + factory NotificationSettingsService(Dio dio, {String baseUrl}) = _NotificationSettingsService; + + @GET('/api/profile/notification_settings') + Future getNotificationSettings(); + + @POST('/api/profile/update_notification_settings') + Future updateNotificationSettings({@Body() required Map data}); +} diff --git a/lib/features/settings/presentation/pages/settings_screen.dart b/lib/features/settings/presentation/pages/settings_screen.dart index b7aa0d59..1c304f29 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,6 +147,7 @@ class _SettingsScreenState extends State { ); } + // ignore: unused_element Column _notifications(SettingsStateLoaded state) { return Column( crossAxisAlignment: CrossAxisAlignment.start, From dcf2805df7fe0852882875ae5ce011d93345c7c8 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Fri, 29 Aug 2025 14:26:28 +0300 Subject: [PATCH 131/134] fix: analyzer --- .../menu/presentation/pages/notifications_settings_screen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/features/menu/presentation/pages/notifications_settings_screen.dart b/lib/features/menu/presentation/pages/notifications_settings_screen.dart index 2c7f4e04..1c1065a3 100644 --- a/lib/features/menu/presentation/pages/notifications_settings_screen.dart +++ b/lib/features/menu/presentation/pages/notifications_settings_screen.dart @@ -165,7 +165,7 @@ class NotificationsSettingsContent extends StatelessWidget { style: TextStyle(fontSize: 14, color: appTheme.blueA400, fontWeight: FontWeight.w500), ), const SizedBox(width: 4), - Text('(нажмите для изменения)', style: TextStyle(fontSize: 12, color: Colors.grey)), + const Text('(нажмите для изменения)', style: TextStyle(fontSize: 12, color: Colors.grey)), ], ) : null, From 39fce4b0f9aa2558b89e40e2e8d01fa74418da33 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Fri, 29 Aug 2025 15:16:40 +0300 Subject: [PATCH 132/134] fix: mv env --- .gitea/workflows/test-flight-cd.yaml | 7 +++---- .gitea/workflows/test.yml | 7 +++---- env/.env | 0 lib/main.dart | 2 +- pubspec.yaml | 2 +- 5 files changed, 8 insertions(+), 10 deletions(-) delete mode 100644 env/.env diff --git a/.gitea/workflows/test-flight-cd.yaml b/.gitea/workflows/test-flight-cd.yaml index 4353f3be..c9649f0e 100644 --- a/.gitea/workflows/test-flight-cd.yaml +++ b/.gitea/workflows/test-flight-cd.yaml @@ -21,20 +21,19 @@ jobs: - name: Copy .env file run: | - mkdir -p env - cp env_repo/.env env/.env + cp env_repo/.env .env - name: Load environment variables from .env run: | set -a - source env/.env + source .env set +a # Export all variables to GITHUB_ENV while IFS= read -r line; do if [[ $line =~ ^[A-Za-z_][A-Za-z0-9_]*= ]]; then echo "$line" >> $GITHUB_ENV fi - done < env/.env + done < .env - name: Unlock login keychain run: | diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml index d209778b..f1f63162 100644 --- a/.gitea/workflows/test.yml +++ b/.gitea/workflows/test.yml @@ -26,20 +26,19 @@ jobs: - name: Copy .env file run: | - mkdir -p env - cp env_repo/.env env/.env + cp env_repo/.env .env - name: Load environment variables from .env run: | set -a - source env/.env + source .env set +a # Export all variables to GITHUB_ENV while IFS= read -r line; do if [[ $line =~ ^[A-Za-z_][A-Za-z0-9_]*= ]]; then echo "$line" >> $GITHUB_ENV fi - done < env/.env + done < .env - name: Create key.properties file run: | diff --git a/env/.env b/env/.env deleted file mode 100644 index e69de29b..00000000 diff --git a/lib/main.dart b/lib/main.dart index 468de90f..60293ed8 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -29,7 +29,7 @@ Future main() async { WidgetsBinding widgetsBinding = SentryWidgetsFlutterBinding.ensureInitialized(); FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); - await dotenv.load(fileName: "env/.env"); + await dotenv.load(fileName: ".env"); await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); await FirebaseMessaging.instance.requestPermission(); diff --git a/pubspec.yaml b/pubspec.yaml index 1505f232..ae850a35 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -90,7 +90,7 @@ dev_dependencies: flutter: uses-material-design: true assets: - - env/.env + - .env - assets/ - assets/images/ - assets/audio/ From b9dc5c013685ec91fb0c6b97554e643bcb960562 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Fri, 29 Aug 2025 12:16:55 +0000 Subject: [PATCH 133/134] =?UTF-8?q?=D0=A3=D0=B4=D0=B0=D0=BB=D0=B8=D1=82?= =?UTF-8?q?=D1=8C=20env/.env?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- env/.env | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 env/.env diff --git a/env/.env b/env/.env deleted file mode 100644 index e69de29b..00000000 From b665692779185307fae230704a1298c9f616b880 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Mon, 1 Sep 2025 13:03:43 +0300 Subject: [PATCH 134/134] fix: Issue-645 --- .../widgets/code_not_received_dialog.dart | 33 +++++++------------ 1 file changed, 11 insertions(+), 22 deletions(-) diff --git a/lib/features/registration/presentation/widgets/code_not_received_dialog.dart b/lib/features/registration/presentation/widgets/code_not_received_dialog.dart index c59d80d9..3baa5742 100644 --- a/lib/features/registration/presentation/widgets/code_not_received_dialog.dart +++ b/lib/features/registration/presentation/widgets/code_not_received_dialog.dart @@ -9,20 +9,13 @@ class CodeNotReceivedDialog extends StatelessWidget { final String currentEmail; final VerifyEmailCubit verifyEmailCubit; - const CodeNotReceivedDialog({ - super.key, - required this.currentEmail, - required this.verifyEmailCubit - }); + const CodeNotReceivedDialog({super.key, required this.currentEmail, required this.verifyEmailCubit}); void _showChangeEmailDialog(BuildContext context) { Navigator.of(context).pop(); // Закрываем текущий диалог showDialog( context: context, - builder: (context) => ChangeEmailDialog( - currentEmail: currentEmail, - verifyEmailCubit: verifyEmailCubit, - ), + builder: (context) => ChangeEmailDialog(currentEmail: currentEmail, verifyEmailCubit: verifyEmailCubit), ); } @@ -31,11 +24,7 @@ class CodeNotReceivedDialog extends StatelessWidget { return Dialog( backgroundColor: Colors.transparent, child: Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(16.0), - border: Border.all(color: Colors.grey[800]!, width: 2.0), - ), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(16.0)), padding: const EdgeInsets.all(24.0), child: Column( mainAxisSize: MainAxisSize.min, @@ -46,17 +35,17 @@ class CodeNotReceivedDialog extends StatelessWidget { text: TextSpan( style: TextStyles.textMMedium.copyWith(color: appTheme.gray600), children: [ - const TextSpan(text: 'Опечатка в email: Если адрес введён неправильно, код не придёт. '), + const TextSpan(text: 'Опечатка в email: Если адрес введён неправильно, код не придёт. ', style: TextStyles.textFS13FW400), TextSpan( text: 'Ввести email повторно.', - style: TextStyles.textMMedium.copyWith( - color: appTheme.blueA400, - decoration: TextDecoration.underline, - ), - recognizer: TapGestureRecognizer() - ..onTap = () => _showChangeEmailDialog(context), + style: TextStyles.textMMedium.copyWith(color: appTheme.blueA400, decoration: TextDecoration.underline), + recognizer: TapGestureRecognizer()..onTap = () => _showChangeEmailDialog(context), + ), + const TextSpan( + style: TextStyles.textFS13FW400, + text: + '\n\nПроверьте спам/промо/мусорную папку: Код может попасть туда из-за фильтров почтового провайдера\n\nПодождите 1-2 минуты: Иногда email задерживается из-за сетевых проблем.\n\nУбедитесь в стабильном соединении: Проверьте интернет и уведомления на устройстве.', ), - const TextSpan(text: '\n\nПроверьте спам/промо/мусорную папку: Код может попасть туда из-за фильтров почтового провайдера\n\nПодождите 1-2 минуты: Иногда email задерживается из-за сетевых проблем.\n\nУбедитесь в стабильном соединении: Проверьте интернет и уведомления на устройстве.'), ], ), ),