From 18a5df27a2be7d773870bb27acb09b91ac0ba09a Mon Sep 17 00:00:00 2001 From: Jganenokk Date: Sat, 4 Jul 2026 00:10:45 +0700 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=BD=D0=B0=D1=81=D1=82=D1=80=D0=BE?= =?UTF-8?q?=D0=B9=D0=BA=D0=B0=20=D1=83=D0=B2=D0=B5=D0=B4=D0=BE=D0=BC=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/backend/modules/account.dart | 20 + .../screens/profile/notifications_screen.dart | 455 +++++++++--------- 2 files changed, 248 insertions(+), 227 deletions(-) diff --git a/lib/backend/modules/account.dart b/lib/backend/modules/account.dart index 9aacd04..3d9a37f 100644 --- a/lib/backend/modules/account.dart +++ b/lib/backend/modules/account.dart @@ -46,6 +46,10 @@ class PrivacyConfig { final String stickersSuggest; final bool safeMode; final bool audioTranscriptionEnabled; + final String chatsPushNotification; + final String mCallPushNotification; + final String pushSound; + final String chatsPushSound; final String hash; const PrivacyConfig({ @@ -68,6 +72,10 @@ class PrivacyConfig { required this.stickersSuggest, required this.safeMode, required this.audioTranscriptionEnabled, + required this.chatsPushNotification, + required this.mCallPushNotification, + required this.pushSound, + required this.chatsPushSound, required this.hash, }); @@ -92,6 +100,10 @@ class PrivacyConfig { stickersSuggest: map['STICKERS_SUGGEST']?.toString() ?? 'ON', safeMode: map['SAFE_MODE'] ?? false, audioTranscriptionEnabled: map['AUDIO_TRANSCRIPTION_ENABLED'] ?? true, + chatsPushNotification: map['CHATS_PUSH_NOTIFICATION']?.toString() ?? 'ON', + mCallPushNotification: map['M_CALL_PUSH_NOTIFICATION']?.toString() ?? 'ON', + pushSound: map['PUSH_SOUND']?.toString() ?? 'oki.aiff', + chatsPushSound: map['CHATS_PUSH_SOUND']?.toString() ?? 'oki.aiff', hash: map['hash']?.toString() ?? '', ); } @@ -116,6 +128,10 @@ class PrivacyConfig { 'STICKERS_SUGGEST': stickersSuggest, 'SAFE_MODE': safeMode, 'AUDIO_TRANSCRIPTION_ENABLED': audioTranscriptionEnabled, + 'CHATS_PUSH_NOTIFICATION': chatsPushNotification, + 'M_CALL_PUSH_NOTIFICATION': mCallPushNotification, + 'PUSH_SOUND': pushSound, + 'CHATS_PUSH_SOUND': chatsPushSound, 'hash': hash, }); @@ -148,6 +164,10 @@ class PrivacyConfig { stickersSuggest: 'ON', safeMode: false, audioTranscriptionEnabled: true, + chatsPushNotification: 'ON', + mCallPushNotification: 'ON', + pushSound: 'oki.aiff', + chatsPushSound: 'oki.aiff', hash: '', ); } diff --git a/lib/frontend/screens/profile/notifications_screen.dart b/lib/frontend/screens/profile/notifications_screen.dart index 7060d00..a1fc326 100644 --- a/lib/frontend/screens/profile/notifications_screen.dart +++ b/lib/frontend/screens/profile/notifications_screen.dart @@ -2,11 +2,11 @@ import 'package:flutter/material.dart'; import 'package:material_symbols_icons/symbols.dart'; import '../../../core/utils/haptics.dart'; +import '../../../main.dart' show accountModule, isOnemeFlavor; import '../../widgets/connection_status.dart'; - +import '../../widgets/custom_notification.dart'; import '../../widgets/glossy_pill.dart'; import '../../widgets/section_header.dart'; -import '../../widgets/sheet_helpers.dart'; class NotificationsScreen extends StatefulWidget { const NotificationsScreen({super.key}); @@ -16,12 +16,58 @@ class NotificationsScreen extends StatefulWidget { } class _NotificationsScreenState extends State { - bool _fkmEnabled = false; - bool _personalChatsEnabled = true; - bool _groupsEnabled = true; - bool _channelsEnabled = true; + static const String _defaultSound = 'oki.aiff'; + + bool _loading = true; + bool _saving = false; + + bool _allNotifications = true; + bool _messagePreview = true; + bool _sound = true; + bool _callNotifications = true; + bool _newContacts = false; bool _hapticsEnabled = Haptics.enabled; - String _selectedSound = 'По умолчанию'; + + @override + void initState() { + super.initState(); + _load(); + } + + Future _load() async { + final config = await accountModule.getPrivacyConfig(); + if (!mounted) return; + setState(() { + _allNotifications = config.chatsPushNotification == 'ON'; + _messagePreview = config.pushDetails; + _sound = config.pushSound.isNotEmpty || config.chatsPushSound.isNotEmpty; + _callNotifications = config.mCallPushNotification == 'ON'; + _newContacts = config.pushNewContacts; + _loading = false; + }); + } + + Future _apply( + bool value, + Map settings, + ValueChanged assign, + ) async { + if (_saving) return; + setState(() { + assign(value); + _saving = true; + }); + try { + await accountModule.updatePrivacyConfig(settings); + } catch (e) { + if (mounted) { + setState(() => assign(!value)); + showCustomNotification(context, 'Не удалось сохранить: $e'); + } + } finally { + if (mounted) setState(() => _saving = false); + } + } Future _setHaptics(bool value) async { await Haptics.setEnabled(value); @@ -29,67 +75,13 @@ class _NotificationsScreenState extends State { if (mounted) setState(() => _hapticsEnabled = value); } - static const List _sounds = [ - 'По умолчанию', - 'Колокольчик', - 'Звон', - 'Капля', - 'Беззвучно', - ]; - - Future _pickSound() async { - final cs = Theme.of(context).colorScheme; - final picked = await showModalBottomSheet( - context: context, - backgroundColor: cs.surfaceContainerHigh, - shape: kSheetShape, - builder: (context) { - return SafeArea( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(20, 12, 20, 16), - child: Row( - children: [ - Text( - 'Звук уведомления', - style: TextStyle( - color: cs.onSurface, - fontSize: 18, - fontWeight: FontWeight.w600, - ), - ), - ], - ), - ), - for (final s in _sounds) - ListTile( - onTap: () => Navigator.of(context).pop(s), - leading: Icon( - s == _selectedSound - ? Symbols.radio_button_checked - : Symbols.radio_button_unchecked, - color: s == _selectedSound - ? cs.primary - : cs.onSurfaceVariant, - ), - title: Text( - s, - style: TextStyle(color: cs.onSurface, fontSize: 16), - ), - ), - ], - ), - ), - ); - }, + void _onFkmTap() { + showCustomNotification( + context, + isOnemeFlavor + ? 'А зачем? У тебя уже FCM.' + : 'Скачай лучше FCM-версию.', ); - if (picked != null && picked != _selectedSound) { - setState(() => _selectedSound = picked); - } } @override @@ -104,90 +96,132 @@ class _NotificationsScreenState extends State { ), body: SafeArea( top: false, - child: ListView( - physics: const BouncingScrollPhysics(), - padding: const EdgeInsets.fromLTRB(16, 12, 16, 120), - children: [ - const SectionHeader( - 'FKM', - padding: EdgeInsets.fromLTRB(8, 0, 8, 8), - fontSize: 14, - ), - _card(cs, [ - _toggleRow( - cs, - icon: Symbols.notifications_active, - label: 'Включить уведомления', - subtitle: - 'Для работы FKM уведомлений, приложению понадобится держать уведомление в шторке.', - value: _fkmEnabled, - onChanged: (v) => setState(() => _fkmEnabled = v), + child: _loading + ? const Center(child: CircularProgressIndicator()) + : ListView( + physics: const BouncingScrollPhysics(), + padding: const EdgeInsets.fromLTRB(16, 12, 16, 120), + children: [ + const SectionHeader( + 'FKM', + padding: EdgeInsets.fromLTRB(8, 0, 8, 8), + fontSize: 14, + ), + _card(cs, [ + _toggleRow( + cs, + icon: Symbols.notifications_active, + label: 'Включить уведомления', + subtitle: + 'Для работы FKM уведомлений, приложению понадобится держать уведомление в шторке.', + value: false, + onChanged: (_) => _onFkmTap(), + ), + ]), + const SizedBox(height: 20), + const SectionHeader( + 'Уведомления', + padding: EdgeInsets.fromLTRB(8, 0, 8, 8), + fontSize: 14, + ), + _card(cs, [ + _toggleRow( + cs, + icon: Symbols.notifications, + label: 'Все уведомления', + value: _allNotifications, + onChanged: (v) => _apply( + v, + {'CHATS_PUSH_NOTIFICATION': v ? 'ON' : 'OFF'}, + (b) => _allNotifications = b, + ), + ), + ]), + const SizedBox(height: 20), + const SectionHeader( + 'Все новые уведомления', + padding: EdgeInsets.fromLTRB(8, 0, 8, 8), + fontSize: 14, + ), + _card(cs, [ + _toggleRow( + cs, + icon: Symbols.chat, + label: 'Предпросмотр сообщений', + value: _messagePreview, + enabled: _allNotifications, + onChanged: (v) => _apply( + v, + {'PUSH_DETAILS': v}, + (b) => _messagePreview = b, + ), + ), + _divider(cs), + _toggleRow( + cs, + icon: Symbols.music_note, + label: 'Звук', + value: _sound, + enabled: _allNotifications, + onChanged: (v) => _apply( + v, + { + 'PUSH_SOUND': v ? _defaultSound : '', + 'CHATS_PUSH_SOUND': v ? _defaultSound : '', + }, + (b) => _sound = b, + ), + ), + ]), + const SizedBox(height: 20), + const SectionHeader( + 'Дополнительно', + padding: EdgeInsets.fromLTRB(8, 0, 8, 8), + fontSize: 14, + ), + _card(cs, [ + _toggleRow( + cs, + icon: Symbols.call, + label: 'Уведомления о звонках', + value: _callNotifications, + onChanged: (v) => _apply( + v, + {'M_CALL_PUSH_NOTIFICATION': v ? 'ON' : 'OFF'}, + (b) => _callNotifications = b, + ), + ), + _divider(cs), + _toggleRow( + cs, + icon: Symbols.person_add, + label: 'Уведомления от новых контактов', + value: _newContacts, + onChanged: (v) => _apply( + v, + {'PUSH_NEW_CONTACTS': v}, + (b) => _newContacts = b, + ), + ), + ]), + const SizedBox(height: 20), + const SectionHeader( + 'Тактильная отдача', + padding: EdgeInsets.fromLTRB(8, 0, 8, 8), + fontSize: 14, + ), + _card(cs, [ + _toggleRow( + cs, + icon: Symbols.vibration, + label: 'Тактильная отдача', + subtitle: 'Виброотклик при действиях в приложении', + value: _hapticsEnabled, + onChanged: _setHaptics, + ), + ]), + ], ), - ]), - const SizedBox(height: 20), - const SectionHeader( - 'Настройки уведомлений', - padding: EdgeInsets.fromLTRB(8, 0, 8, 8), - fontSize: 14, - ), - _card(cs, [ - _toggleRow( - cs, - icon: Symbols.person, - label: 'Уведомления от личных чатов', - value: _personalChatsEnabled, - onChanged: (v) => setState(() => _personalChatsEnabled = v), - ), - _divider(cs), - _toggleRow( - cs, - icon: Symbols.groups, - label: 'Уведомления от групп', - value: _groupsEnabled, - onChanged: (v) => setState(() => _groupsEnabled = v), - ), - _divider(cs), - _toggleRow( - cs, - icon: Symbols.campaign, - label: 'Уведомления от каналов', - value: _channelsEnabled, - onChanged: (v) => setState(() => _channelsEnabled = v), - ), - ]), - const SizedBox(height: 20), - const SectionHeader( - 'Звук', - padding: EdgeInsets.fromLTRB(8, 0, 8, 8), - fontSize: 14, - ), - _card(cs, [ - _tappableRow( - cs, - icon: Symbols.music_note, - label: 'Звук уведомления', - trailingText: _selectedSound, - onTap: _pickSound, - ), - ]), - const SizedBox(height: 20), - const SectionHeader( - 'Тактильная отдача', - padding: EdgeInsets.fromLTRB(8, 0, 8, 8), - fontSize: 14, - ), - _card(cs, [ - _toggleRow( - cs, - icon: Symbols.vibration, - label: 'Тактильная отдача', - subtitle: 'Виброотклик при действиях в приложении', - value: _hapticsEnabled, - onChanged: _setHaptics, - ), - ]), - ], - ), ), ); } @@ -219,87 +253,54 @@ class _NotificationsScreenState extends State { String? subtitle, required bool value, required ValueChanged onChanged, + bool enabled = true, }) { - return Material( - color: Colors.transparent, - child: InkWell( - onTap: () => onChanged(!value), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14), - child: Row( - children: [ - Icon(icon, color: cs.onSurfaceVariant, size: 22, weight: 400), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - label, - style: TextStyle( - color: cs.onSurface, - fontSize: 16, - fontWeight: FontWeight.w500, - ), - ), - if (subtitle != null) ...[ - const SizedBox(height: 2), - Text( - subtitle, - style: TextStyle( - color: cs.onSurfaceVariant, - fontSize: 13, - height: 1.3, + return AnimatedOpacity( + duration: const Duration(milliseconds: 200), + opacity: enabled ? 1 : 0.4, + child: IgnorePointer( + ignoring: !enabled, + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: () => onChanged(!value), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14), + child: Row( + children: [ + Icon(icon, color: cs.onSurfaceVariant, size: 22, weight: 400), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: TextStyle( + color: cs.onSurface, + fontSize: 16, + fontWeight: FontWeight.w500, + ), ), - ), - ], - ], - ), - ), - const SizedBox(width: 12), - Switch(value: value, onChanged: onChanged), - ], - ), - ), - ), - ); - } - - Widget _tappableRow( - ColorScheme cs, { - required IconData icon, - required String label, - required String trailingText, - required VoidCallback onTap, - }) { - return Material( - color: Colors.transparent, - child: InkWell( - onTap: onTap, - borderRadius: BorderRadius.circular(20), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 17), - child: Row( - children: [ - Icon(icon, color: cs.onSurfaceVariant, size: 22, weight: 400), - const SizedBox(width: 16), - Expanded( - child: Text( - label, - style: TextStyle( - color: cs.onSurface, - fontSize: 16, - fontWeight: FontWeight.w500, + if (subtitle != null) ...[ + const SizedBox(height: 2), + Text( + subtitle, + style: TextStyle( + color: cs.onSurfaceVariant, + fontSize: 13, + height: 1.3, + ), + ), + ], + ], + ), ), - ), + const SizedBox(width: 12), + Switch(value: value, onChanged: onChanged), + ], ), - Text( - trailingText, - style: TextStyle(color: cs.onSurfaceVariant, fontSize: 14), - ), - const SizedBox(width: 6), - Icon(Symbols.chevron_right, color: cs.outline, size: 20), - ], + ), ), ), ),