From 3e8c8c7e9f5acced311512e4ca4acf32589041e0 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Fri, 8 Aug 2025 12:16:20 +0300 Subject: [PATCH 1/7] 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 2/7] 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 3/7] 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 4/7] 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 5/7] 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 6/7] 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 7/7] 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: