import 'package:eatvio_app/core/app_router.dart'; import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/widgets/bottom_sheet_handler_bar.dart'; import 'package:eatvio_app/core/widgets/bottom_sheet_with_blur.dart'; import 'package:eatvio_app/core/widgets/custom_outlined_button.dart'; import 'package:eatvio_app/core/widgets/data_error_widget.dart'; import 'package:eatvio_app/core/widgets/loader.dart'; import 'package:eatvio_app/core/widgets/show_error_dialog.dart'; import 'package:eatvio_app/features/auth/sdk/social_auth_facade.dart'; import 'package:eatvio_app/features/consultant/become_consultant/models/consultation_request_model.dart'; import 'package:eatvio_app/features/settings/entities/settings_entity.dart'; import 'package:eatvio_app/features/settings/presentation/bloc/settings_cubit/settings_cubit.dart'; import 'package:eatvio_app/features/settings/presentation/bloc/settings_cubit/settings_state.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:go_router/go_router.dart'; class SettingsScreen extends StatefulWidget { const SettingsScreen({super.key}); @override State createState() => _SettingsScreenState(); } class _SettingsScreenState extends State { final SettingsCubit settingsCubit = sl()..init(); late bool updatesNotificationsSwitchState; late bool fillDiaryNotificationsSwitchState; late bool bedTimeSwitchState; late bool moodSwitchState; void _showConsultationWarning() { showDialog( context: context, builder: (BuildContext context) { return AlertDialog( title: const Text('Предупреждение'), content: const Text('Во время консультации этот параметр нельзя отключить'), actions: [TextButton(onPressed: () => Navigator.of(context).pop(), child: const Text('Понятно'))], ); }, ); } @override Widget build(BuildContext context) { return BlocProvider( create: (BuildContext context) => settingsCubit, child: BlocConsumer( bloc: settingsCubit, listener: (BuildContext context, SettingsState state) { if (state is SettingsStateLoaded) { updatesNotificationsSwitchState = state.profileSettings.newUpdatesNotifications == 1 ? true : false; fillDiaryNotificationsSwitchState = state.profileSettings.diaryReminderNotifications == 1 ? true : false; bedTimeSwitchState = state.profileSettings.isBedtimeFillable; moodSwitchState = state.profileSettings.isMoodFillable; } }, builder: (BuildContext context, SettingsState state) { if (state is SettingsStateLoaded) { return Scaffold( appBar: AppBar(title: const Text('Настройки')), body: SafeArea( child: SingleChildScrollView( padding: const EdgeInsets.symmetric(vertical: 16), physics: const BouncingScrollPhysics(), child: Column( children: [ _oauthAccounts(state, settingsCubit), const SizedBox(height: 12), _additionalInfo(state), const SizedBox(height: 12), _notifications(state), const SizedBox(height: 12), _becomeConsultantButton(state), _deleteAccountButton(state, settingsCubit), ], ), ), ), ); } if (state is SettingsStateError) { return Scaffold( appBar: AppBar(title: const Text('Настройки')), body: DataErrorWidget(onReload: () => settingsCubit.init()), ); } return Scaffold( appBar: AppBar(title: const Text('Настройки')), body: const Loader(), ); }, ), ); } Column _additionalInfo(SettingsStateLoaded state) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Padding( padding: EdgeInsets.symmetric(horizontal: 16), child: Text( 'Дополнительаная информация', style: TextStyle(fontSize: 16, height: 1, color: Colors.grey, fontFamily: 'Mulish'), ), ), _tile( onTap: () => GoRouter.of(context).pushNamed( 'generalInfoScreen', extra: SettingsEntity(profileSettingsModel: state.profileSettings, profileDetailsModel: state.profileDetails, settingsCubit: settingsCubit), ), title: 'Общая информация', ), _divider(context), _tile( onTap: () => GoRouter.of(context).pushNamed( 'physicalFormScreen', extra: SettingsEntity(profileSettingsModel: state.profileSettings, profileDetailsModel: state.profileDetails, settingsCubit: settingsCubit), ), title: 'Физическая форма', ), _divider(context), _tile( onTap: () => GoRouter.of(context).pushNamed( 'pressureScreen', extra: SettingsEntity(profileSettingsModel: state.profileSettings, profileDetailsModel: state.profileDetails, settingsCubit: settingsCubit), ), title: 'Давление', ), _divider(context), _tile( onTap: () => GoRouter.of(context).pushNamed( 'otherInfoScreen', extra: SettingsEntity(profileSettingsModel: state.profileSettings, profileDetailsModel: state.profileDetails, settingsCubit: settingsCubit), ), title: 'Прочая информация', ), _divider(context), _tile( onTap: () => GoRouter.of(context).pushNamed( 'physicalActivityScreen', extra: SettingsEntity(profileSettingsModel: state.profileSettings, profileDetailsModel: state.profileDetails, settingsCubit: settingsCubit), ), title: 'Физическая нагрузка', ), _divider(context), _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), ), title: 'Сменить Email', ), ], ); } Column _notifications(SettingsStateLoaded state) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Padding( padding: EdgeInsets.symmetric(horizontal: 16), child: Text('При использовании дневника', style: TextStyle(fontSize: 16, height: 1, color: Colors.grey)), ), _tile( padding: const EdgeInsets.symmetric(horizontal: 16), onTap: () { if (state.hasActiveConsultation) { _showConsultationWarning(); return; } setState(() { moodSwitchState = !moodSwitchState; }); settingsCubit.updateProfileSettings(profileSettingsModel: state.profileSettings.copyWith(isMoodFillable: moodSwitchState)); }, title: 'Указывать "настроение" в заполнение дневника', trailing: CupertinoSwitch( value: state.hasActiveConsultation ? true : moodSwitchState, activeTrackColor: appTheme.blueA400, onChanged: (newValue) { if (state.hasActiveConsultation) { _showConsultationWarning(); return; } setState(() { moodSwitchState = newValue; }); settingsCubit.updateProfileSettings(profileSettingsModel: state.profileSettings.copyWith(isMoodFillable: moodSwitchState)); }, ), ), _divider(context), _tile( padding: const EdgeInsets.symmetric(horizontal: 16), onTap: () { if (state.hasActiveConsultation) { _showConsultationWarning(); return; } setState(() { bedTimeSwitchState = !bedTimeSwitchState; }); settingsCubit.updateProfileSettings(profileSettingsModel: state.profileSettings.copyWith(isBedtimeFillable: bedTimeSwitchState)); }, title: 'Указывать "отход ко сну" в заполнение дневника', trailing: CupertinoSwitch( value: state.hasActiveConsultation ? true : bedTimeSwitchState, activeTrackColor: appTheme.blueA400, onChanged: (newValue) { if (state.hasActiveConsultation) { _showConsultationWarning(); return; } setState(() { bedTimeSwitchState = newValue; }); settingsCubit.updateProfileSettings(profileSettingsModel: state.profileSettings.copyWith(isBedtimeFillable: bedTimeSwitchState)); }, ), ), ], ); } SingleChildRenderObjectWidget _becomeConsultantButton(SettingsStateLoaded state) { if (state.consultationRequestModel == null) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 16) + const EdgeInsets.only(bottom: 8), child: InkWell( onTap: () => GoRouter.of(context).pushNamed('becomeConsultantScreen'), borderRadius: BorderRadius.circular(20), child: Ink( height: 52, width: MediaQuery.of(context).size.width - 16, decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: appTheme.blueA400), child: const Center( child: Text('Стать консультантом', style: TextStyle(color: Colors.white, height: 1, fontSize: 18)), ), ), ), ); } else { if (state.consultationRequestModel!.educationModel.requestState == ModerationStatus.created) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 16) + const EdgeInsets.only(bottom: 8), child: Ink( height: 52, width: MediaQuery.of(context).size.width - 16, decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: const Color(0xFFEAEAEA)), child: const Center( child: Text('Данные на проверке', style: TextStyle(color: Colors.black, height: 1, fontSize: 18)), ), ), ); } else if (state.consultationRequestModel!.educationModel.requestState == ModerationStatus.canceled) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 16) + const EdgeInsets.only(bottom: 8), child: InkWell( onTap: () => GoRouter.of(context).pushNamed('stepperBecomeConsultantScreen', extra: state.profile.slug), borderRadius: BorderRadius.circular(20), child: Ink( height: 52, width: MediaQuery.of(context).size.width - 16, decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: const Color(0x33FFCC00)), child: const Center( child: Text('Заявка обработана', style: TextStyle(color: Colors.black, height: 1, fontSize: 18)), ), ), ), ); } } return const SizedBox.shrink(); } Padding _deleteAccountButton(SettingsStateLoaded state, SettingsCubit settingsCubit) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: InkWell( onTap: () async { showBottomSheetWithBlur( userRootNavigator: true, context: context, height: null, 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)), child: const Center( child: Text('Удалить аккаунт', style: TextStyle(color: Colors.black, height: 1, fontSize: 18)), ), ), ), ); } 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), ); } Center _divider(BuildContext context) { return Center( child: Container( height: 0.4, width: MediaQuery.of(context).size.width - 32, decoration: BoxDecoration(color: Colors.black26, borderRadius: BorderRadius.circular(16)), ), ); } Widget _oauthAccounts(SettingsStateLoaded state, SettingsCubit settingsCubit) { final isVkLinked = state.oauthAccounts?.vk != null; final isYandexLinked = state.oauthAccounts?.yandex != null; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Padding( padding: EdgeInsets.symmetric(horizontal: 16), child: Text( 'Привязанные аккаунты', style: TextStyle(fontSize: 16, height: 1, color: Colors.grey, fontFamily: 'Mulish'), ), ), const SizedBox(height: 12), Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ _OAuthAccountButton( onTap: () => _onVkPressed(state, settingsCubit), backgroundColor: const Color(0xFF2787F5), tooltip: 'VK ID', icon: SvgPicture.asset('assets/icons/vk_logo.svg', height: 24, width: 24), isLinked: isVkLinked, accountName: state.oauthAccounts?.vk?.name, ), const SizedBox(width: 12), _OAuthAccountButton( onTap: () => _onOkPressed(state, settingsCubit), backgroundColor: const Color(0xFFF58220), tooltip: 'Одноклассники', icon: SvgPicture.asset('assets/icons/ok_logo.svg', height: 24, width: 24), isLinked: isVkLinked, accountName: state.oauthAccounts?.vk?.name, ), const SizedBox(width: 12), _OAuthAccountButton( onTap: () => _onMailRuPressed(state, settingsCubit), backgroundColor: const Color(0xFF005FF9), tooltip: 'Mail.ru', icon: SvgPicture.asset('assets/icons/mail_logo.svg', height: 24, width: 24), isLinked: isVkLinked, accountName: state.oauthAccounts?.vk?.name, ), const SizedBox(width: 12), _OAuthAccountButton( onTap: () => _onYandexPressed(state, settingsCubit), backgroundColor: const Color(0xFFFFCC00), tooltip: 'Яндекс ID', icon: SvgPicture.asset('assets/icons/yandex_logo.svg', height: 24, width: 24), isLinked: isYandexLinked, accountName: state.oauthAccounts?.yandex?.name, ), ], ), ), const SizedBox(height: 12), ], ); } Future _onVkPressed(SettingsStateLoaded state, SettingsCubit settingsCubit) async { if (state.oauthAccounts?.vk != null) { // Отвязываем аккаунт if (!mounted) return; final shouldUnlink = await _showUnlinkConfirmationDialog(context, 'VK ID'); if (shouldUnlink == true) { try { await settingsCubit.unlinkOAuthAccount(provider: 'vk'); } catch (e) { if (mounted) { showErrorDialog(context, description: 'Не удалось отвязать аккаунт'); } } } return; } try { if (!mounted) return; final authResult = await SocialAuthFacade.signInWithVkAndFetchProfile(context: context); if (authResult == null) return; try { await settingsCubit.linkOAuthAccount( provider: 'vk', accessToken: authResult.accessToken, refreshToken: authResult.refreshToken ?? '', clientId: authResult.clientId, ); } catch (e) { if (mounted) { showErrorDialog(context, description: 'Не удалось привязать аккаунт'); } } } catch (e) { if (mounted) { showErrorDialog(context, description: 'Не удалось привязать аккаунт'); } } } Future _onOkPressed(SettingsStateLoaded state, SettingsCubit settingsCubit) async { if (state.oauthAccounts?.vk != null) { // Отвязываем аккаунт (OK использует тот же VK ID провайдер) if (!mounted) return; final shouldUnlink = await _showUnlinkConfirmationDialog(context, 'Одноклассники'); if (shouldUnlink == true) { try { await settingsCubit.unlinkOAuthAccount(provider: 'vk'); } catch (e) { if (mounted) { showErrorDialog(context, description: 'Не удалось отвязать аккаунт'); } } } return; } try { if (!mounted) return; final authResult = await SocialAuthFacade.signInWithOkAndFetchProfile(); if (authResult == null) return; try { await settingsCubit.linkOAuthAccount( provider: 'vk', accessToken: authResult.accessToken, refreshToken: authResult.refreshToken ?? '', clientId: authResult.clientId, ); } catch (e) { if (mounted) { showErrorDialog(context, description: 'Не удалось привязать аккаунт'); } } } catch (e) { if (mounted) { showErrorDialog(context, description: 'Не удалось привязать аккаунт'); } } } Future _onMailRuPressed(SettingsStateLoaded state, SettingsCubit settingsCubit) async { if (state.oauthAccounts?.vk != null) { // Отвязываем аккаунт (Mail.ru использует тот же VK ID провайдер) if (!mounted) return; final shouldUnlink = await _showUnlinkConfirmationDialog(context, 'Mail.ru'); if (shouldUnlink == true) { try { await settingsCubit.unlinkOAuthAccount(provider: 'vk'); } catch (e) { if (mounted) { showErrorDialog(context, description: 'Не удалось отвязать аккаунт'); } } } return; } try { if (!mounted) return; final authResult = await SocialAuthFacade.signInWithMailRuAndFetchProfile(); if (authResult == null) return; try { await settingsCubit.linkOAuthAccount( provider: 'vk', accessToken: authResult.accessToken, refreshToken: authResult.refreshToken ?? '', clientId: authResult.clientId, ); } catch (e) { if (mounted) { showErrorDialog(context, description: 'Не удалось привязать аккаунт'); } } } catch (e) { if (mounted) { showErrorDialog(context, description: 'Не удалось привязать аккаунт'); } } } Future _onYandexPressed(SettingsStateLoaded state, SettingsCubit settingsCubit) async { if (state.oauthAccounts?.yandex != null) { // Отвязываем аккаунт if (!mounted) return; final shouldUnlink = await _showUnlinkConfirmationDialog(context, 'Яндекс ID'); if (shouldUnlink == true) { try { await settingsCubit.unlinkOAuthAccount(provider: 'yandex'); } catch (e) { if (mounted) { showErrorDialog(context, description: 'Не удалось отвязать аккаунт'); } } } return; } try { if (!mounted) return; final authResult = await SocialAuthFacade.signInWithYandex(); if (authResult == null) { if (mounted) { showErrorDialog(context, description: 'Не удалось авторизоваться через Яндекс'); } return; } // Проверяем, что refreshToken не пустой для Яндекс if (authResult.refreshToken == null || authResult.refreshToken!.isEmpty) { if (mounted) { showErrorDialog(context, description: 'Не удалось получить токен обновления от Яндекс'); } return; } // Проверяем, что clientId не пустой для Яндекс if (authResult.clientId.isEmpty) { if (mounted) { showErrorDialog(context, description: 'Не удалось получить client ID от Яндекс'); } return; } try { await settingsCubit.linkOAuthAccount( provider: 'yandex', accessToken: authResult.accessToken, refreshToken: authResult.refreshToken!, clientId: authResult.clientId, ); } catch (e) { if (mounted) { showErrorDialog(context, description: 'Не удалось привязать аккаунт: ${e.toString()}'); } } } catch (e) { if (mounted) { showErrorDialog(context, description: 'Не удалось привязать аккаунт: ${e.toString()}'); } } } Future _showUnlinkConfirmationDialog(BuildContext context, String providerName) { return showDialog( context: context, builder: (context) => CupertinoAlertDialog( title: const Text('Отвязать аккаунт'), content: Text('Вы точно хотите отвязать аккаунт $providerName?'), actions: [ CupertinoDialogAction( onPressed: () { Navigator.of(context).pop(false); }, child: const Text('Отмена'), ), CupertinoDialogAction( onPressed: () { Navigator.of(context).pop(true); }, isDestructiveAction: true, child: const Text('Отвязать'), ), ], ), ); } } class _OAuthAccountButton extends StatelessWidget { const _OAuthAccountButton({ required this.onTap, required this.icon, required this.backgroundColor, required this.tooltip, required this.isLinked, this.accountName, }); final VoidCallback onTap; final Widget icon; final Color backgroundColor; final String tooltip; final bool isLinked; final String? accountName; @override Widget build(BuildContext context) { return Expanded( child: Opacity( opacity: isLinked ? 0.5 : 1.0, child: Tooltip( message: tooltip, child: InkWell( borderRadius: BorderRadius.circular(20), onTap: onTap, child: Ink( height: 48, decoration: BoxDecoration( color: Colors.transparent, borderRadius: BorderRadius.circular(20), border: Border.all(color: const Color(0xFFDBDBDB), width: 1), ), child: Center(child: icon), ), ), ), ), ); } } class DeleteAccountBottomSheet extends StatefulWidget { const DeleteAccountBottomSheet({super.key, required this.cubit}); final SettingsCubit cubit; @override State createState() => _DeleteAccountBottomSheet(); } class _DeleteAccountBottomSheet extends State { bool? isFailure; @override Widget build(BuildContext context) { final s = S.of(context); return Padding( padding: const EdgeInsets.symmetric(horizontal: 12.0), child: Column( mainAxisSize: MainAxisSize.min, children: [ const SizedBox(height: 8), const BottomSheetHandlerBar(), const SizedBox(height: 20), Text(s.accountDeleting, style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)), const SizedBox(height: 16), Text( s.youShureYouWantToDeleteAccount, style: TextStyles.textSMedium.copyWith(color: appTheme.black), textAlign: TextAlign.center, ), const SizedBox(height: 8), Text( s.itCantBeRecovered, style: TextStyles.textSMedium.copyWith(color: appTheme.black), textAlign: TextAlign.center, ), const SizedBox(height: 8), CustomOutlinedButton( text: s.delete, height: 52, buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.blueA400)), onPressed: () async { final result = await widget.cubit.deleteAccount(); if (result == true) { AppRouter.navigatorKey.currentContext?.go('/auth-screen'); return; } AppRouter.navigatorKey.currentContext?.pop(); if (!context.mounted) return; showErrorDialog(context, description: s.cannotDeleteAccount); }, ), const SizedBox(height: 40), ], ), ); } }