From 24b7ff6affbf0416dcd16edb94e7d063ff80ceef Mon Sep 17 00:00:00 2001 From: Jganenokk Date: Sun, 12 Apr 2026 14:18:26 +0700 Subject: [PATCH] =?UTF-8?q?=D0=BD=D0=B0=D1=85=D1=83=D0=B5=D0=B2=D0=B5?= =?UTF-8?q?=D1=80=D1=82=D0=B8=D0=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/backend/api.dart | 16 ++- lib/backend/modules/account.dart | 4 +- lib/core/protocol/packet.dart | 4 + .../screens/chats/chat_list_screen.dart | 20 +++- .../screens/profile/spoof_screen.dart | 113 +++++++++++------- lib/l10n/app_en.arb | 8 +- lib/l10n/app_localizations.dart | 38 +++++- lib/l10n/app_localizations_en.dart | 22 +++- lib/l10n/app_localizations_ru.dart | 22 +++- lib/l10n/app_ru.arb | 8 +- lib/main.dart | 20 ++-- 11 files changed, 213 insertions(+), 62 deletions(-) diff --git a/lib/backend/api.dart b/lib/backend/api.dart index 0ff3ace..2c50950 100644 --- a/lib/backend/api.dart +++ b/lib/backend/api.dart @@ -32,6 +32,7 @@ class Api { final _stateController = StreamController.broadcast(); final _sessionExpiredController = StreamController.broadcast(); + final _handshakeSuccessController = StreamController.broadcast(); Map? _userAgent; Map? get userAgent => _userAgent; @@ -44,6 +45,8 @@ class Api { Stream get stateStream => _stateController.stream; Stream get sessionExpiredStream => _sessionExpiredController.stream; + Stream get handshakeSuccessStream => + _handshakeSuccessController.stream; SessionState get state => _sessionState; StreamSubscription? _dataSubscription; @@ -93,6 +96,12 @@ class Api { _setSessionState(SessionState.online); _startPinging(); logger.i('Сессия онлайн, хэндшейк ок'); + _handshakeSuccessController.add( + response.payload['device_name'] as String? ?? 'Unknown', + ); + if (_onReconnectCallback != null) { + _onReconnectCallback!(); + } } else { logger.e('Хэндшейк отклонён: ${response.payload}'); } @@ -254,7 +263,8 @@ class Api { await for (final packet in _receiver.feed(data)) { if (packet.isError && packet.payload is Map && - packet.payload['message'] == 'FAIL_LOGIN_TOKEN') { + (packet.payload['message'] == 'FAIL_LOGIN_TOKEN' || + packet.payload['message'] == 'FAIL_WRONG_PASSWORD')) { _sessionExpiredController.add( SessionExpiredException(messageFromErrorPayload(packet.payload)), ); @@ -277,13 +287,11 @@ class Api { _socketStateSubscription = null; _receiver.reset(); _dispatcher.clearPending(); + _handshakeSuccessController.add('disconnected'); } Future reconnectAndLogin() async { await connect(); - if (_sessionState == SessionState.online && _onReconnectCallback != null) { - _onReconnectCallback!(); - } } void Function()? _onReconnectCallback; diff --git a/lib/backend/modules/account.dart b/lib/backend/modules/account.dart index d86108f..1f57233 100644 --- a/lib/backend/modules/account.dart +++ b/lib/backend/modules/account.dart @@ -1022,7 +1022,9 @@ class AccountModule { void _checkPacketError(Packet packet, String method) { if (packet.isError) { final payload = packet.payload; - if (payload is Map && payload['message'] == 'FAIL_LOGIN_TOKEN') { + if (payload is Map && + (payload['message'] == 'FAIL_LOGIN_TOKEN' || + payload['message'] == 'FAIL_WRONG_PASSWORD')) { throw SessionExpiredException(messageFromErrorPayload(payload)); } throw PacketError(messageFromErrorPayload(payload)); diff --git a/lib/core/protocol/packet.dart b/lib/core/protocol/packet.dart index 7a0a320..7b6fb38 100644 --- a/lib/core/protocol/packet.dart +++ b/lib/core/protocol/packet.dart @@ -65,6 +65,10 @@ class SessionExpiredException extends PacketError { String messageFromErrorPayload(dynamic payload) { if (payload is Map) { + final msg = payload['message']; + if (msg == 'FAIL_WRONG_PASSWORD' || msg == 'FAIL_LOGIN_TOKEN') { + return 'Ваш токен был отклонён сервером, хм... Попробуйте войти ещё раз.'; + } for (final key in ['localizedMessage', 'message', 'title']) { final v = payload[key]; if (v is String && v.trim().isNotEmpty) return v.trim(); diff --git a/lib/frontend/screens/chats/chat_list_screen.dart b/lib/frontend/screens/chats/chat_list_screen.dart index 1f1bf57..2f2bf70 100644 --- a/lib/frontend/screens/chats/chat_list_screen.dart +++ b/lib/frontend/screens/chats/chat_list_screen.dart @@ -199,7 +199,20 @@ class _ChatListScreenState extends State Future _reloadChatsAndFolders() async { final p = await AppDatabase.loadActiveProfile(); - if (p != null) { + if (p == null) { + _syncFolderChatScrollControllersForCount(1); + if (mounted) { + setState(() { + _folders = []; + _selectedFolderId = null; + _foldersListKnown = null; + _isInitialLoading = false; + }); + } + return; + } + + try { final chats = await ChatsModule.getChats(p.id); var folders = await FoldersModule.loadFolders(p.id); final foldersKnown = await FoldersModule.hasReceivedFoldersList(p.id); @@ -244,7 +257,7 @@ class _ChatListScreenState extends State _jumpFolderPageToSelection(); }); } - } else { + } catch (_) { _syncFolderChatScrollControllersForCount(1); if (mounted) { setState(() { @@ -253,6 +266,9 @@ class _ChatListScreenState extends State _foldersListKnown = null; _isInitialLoading = false; }); + } + } finally { + if (mounted) { WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; _jumpFolderPageToSelection(); diff --git a/lib/frontend/screens/profile/spoof_screen.dart b/lib/frontend/screens/profile/spoof_screen.dart index ff16a0c..1a1f724 100644 --- a/lib/frontend/screens/profile/spoof_screen.dart +++ b/lib/frontend/screens/profile/spoof_screen.dart @@ -9,8 +9,10 @@ import 'package:shared_preferences/shared_preferences.dart'; import '../../../core/config/device_presets.dart'; import '../../../core/storage/spoofing_service.dart'; +import '../../../core/storage/token_storage.dart'; import '../../../l10n/app_localizations.dart'; import '../../../main.dart'; +import '../auth/login_screen.dart'; enum SpoofingMethod { partial, full } @@ -127,19 +129,10 @@ class _SpoofScreenState extends State { _deviceNameController.text = '${androidInfo.manufacturer} ${androidInfo.model}'; _osVersionController.text = 'Android ${androidInfo.version.release}'; - _selectedDeviceType = 'ANDROID'; _selectedArch = androidInfo.supportedAbis.isNotEmpty ? androidInfo.supportedAbis.first : 'arm64-v8a'; _buildNumberController.text = '$_hardcodedBuildNumber'; - } else if (Platform.isIOS) { - final iosInfo = await deviceInfo.iosInfo; - _deviceNameController.text = iosInfo.name; - _osVersionController.text = - '${iosInfo.systemName} ${iosInfo.systemVersion}'; - _selectedDeviceType = 'IOS'; - _selectedArch = 'arm64'; - _buildNumberController.text = '$_hardcodedBuildNumber'; } else { await _applyGeneratedData(); } @@ -168,15 +161,7 @@ class _SpoofScreenState extends State { _appVersionController.text = _hardcodedVersion; _deviceIdController.text = _generateDeviceId(); - _selectedDeviceType = preset.deviceType; - - if (preset.deviceType == 'ANDROID') { - _selectedArch = 'arm64-v8a'; - } else if (preset.deviceType == 'IOS') { - _selectedArch = 'arm64'; - } else { - _selectedArch = 'x86_64'; - } + _selectedArch = 'arm64-v8a'; _buildNumberController.text = '$_hardcodedBuildNumber'; if (_selectedMethod == SpoofingMethod.full) { @@ -247,32 +232,76 @@ class _SpoofScreenState extends State { if (!mounted) return; final l10n = AppLocalizations.of(context)!; - final confirmed = await showDialog( + final confirmed = await showDialog( context: context, builder: (context) => AlertDialog( title: Text(l10n.spoofDialogApplyTitle), - content: Text(l10n.spoofDialogApplyContent), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(l10n.spoofDialogApplyContent), + const SizedBox(height: 12), + Text( + l10n.spoofDialogApplyWarning, + style: TextStyle( + color: Theme.of(context).colorScheme.error, + fontWeight: FontWeight.w500, + ), + ), + ], + ), actions: [ TextButton( - onPressed: () => Navigator.of(context).pop(false), + onPressed: () => Navigator.of(context).pop('cancel'), child: Text(l10n.spoofDialogApplyDeny), ), + TextButton( + onPressed: () => Navigator.of(context).pop('relogin'), + child: Text(l10n.spoofDialogReloginConfirm), + ), FilledButton( - onPressed: () => Navigator.of(context).pop(true), + onPressed: () => Navigator.of(context).pop('apply'), child: Text(l10n.spoofDialogApplyConfirm), ), ], ), ); - if (confirmed != true || !mounted) return; + if (!mounted || confirmed == null) return; + + if (confirmed == 'relogin') { + final prefs = await SharedPreferences.getInstance(); + await _saveAllData(prefs); + await api.disconnect(); + final accountId = await TokenStorage.getActiveAccountId(); + if (accountId != null) { + await TokenStorage.deleteToken(accountId); + } + await prefs.setBool('spoofing_enabled', true); + await api.connect(); + if (mounted) { + final navState = KometApp.navigatorKey.currentState; + if (navState != null) { + await navState.pushAndRemoveUntil( + MaterialPageRoute(builder: (_) => const LoginScreen()), + (route) => false, + ); + } + } + return; + } + + if (confirmed != 'apply') return; await _saveAllData(prefs); try { await api.disconnect(); await api.connect(); - if (mounted) Navigator.of(context).pop(); + if (mounted && Navigator.of(context).canPop()) { + Navigator.of(context).pop(); + } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( @@ -452,29 +481,18 @@ class _SpoofScreenState extends State { text: l10n.spoofDeviceTypeDescription, ), const SizedBox(height: 12), - _buildChipSelector( - options: const [ - _ChipOption('ANDROID', 'ANDROID', Icons.android_outlined), - _ChipOption('IOS', 'iOS', Icons.phone_iphone_outlined), - _ChipOption( - 'DESKTOP', + Row( + children: [ + _buildDisabledChip('ANDROID', Icons.android_outlined, theme), + const SizedBox(width: 8), + _buildDisabledChip('iOS', Icons.phone_iphone_outlined, theme), + const SizedBox(width: 8), + _buildDisabledChip( 'Desktop', Icons.desktop_windows_outlined, + theme, ), ], - selected: _selectedDeviceType, - onSelected: (value) { - setState(() { - _selectedDeviceType = value; - if (value == 'ANDROID') { - _selectedArch = 'arm64-v8a'; - } else if (value == 'IOS') { - _selectedArch = 'arm64'; - } else { - _selectedArch = 'x86_64'; - } - }); - }, ), ], ), @@ -482,6 +500,15 @@ class _SpoofScreenState extends State { ); } + Widget _buildDisabledChip(String label, IconData icon, ThemeData theme) { + return Chip( + label: Text(label), + avatar: Icon(icon, size: 18, color: theme.colorScheme.onSurfaceVariant), + backgroundColor: theme.colorScheme.surfaceContainerHighest, + side: BorderSide(color: theme.colorScheme.outlineVariant), + ); + } + Widget _buildDescriptionTile({ required IconData icon, required Color color, diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index c047aaa..806d0c5 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -64,7 +64,7 @@ "spoofMethodPartialDescription": "Recommended method. Random data is used, but your real timezone and locale are kept for plausibility.", "spoofMethodFullDescription": "All data including timezone and locale is generated randomly. Use this method at your own risk!", "spoofDeviceTypeTitle": "Device type", - "spoofDeviceTypeDescription": "Choose a device type for preset generation. Tapping \"Generate\" will only use presets of the selected type.", + "spoofDeviceTypeDescription": "This field is not changeable, to avoid token association issues", "spoofDeviceTypeLabel": "Device type", "spoofMainSectionTitle": "Main data", "spoofFieldDeviceName": "Device name", @@ -88,6 +88,12 @@ "spoofDialogYes": "Yes", "spoofDialogApplyTitle": "Apply settings?", "spoofDialogApplyContent": "Need to reconnect the app, ok?", + "spoofDialogApplyWarning": "Your spoof will change immediately. But due to MAX specifics, you must re-login to the account for it to become visible", + "spoofDialogReloginTitle": "Done!", + "spoofDialogReloginContent": "Due to MAX specifics, your spoof is changed, but changes will be visible only after re-login.", + "spoofDialogReloginWarning": "Re-login now?", + "spoofDialogReloginDeny": "Later", + "spoofDialogReloginConfirm": "Re-login now", "spoofDialogApplyDeny": "No", "spoofDialogApplyConfirm": "Ok!", "spoofErrorApplyFailed": "Failed to apply settings: {error}", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 15fbdb0..5bd5013 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -431,7 +431,7 @@ abstract class AppLocalizations { /// No description provided for @spoofDeviceTypeDescription. /// /// In en, this message translates to: - /// **'Choose a device type for preset generation. Tapping \"Generate\" will only use presets of the selected type.'** + /// **'This field is not changeable, to avoid token association issues'** String get spoofDeviceTypeDescription; /// No description provided for @spoofDeviceTypeLabel. @@ -572,6 +572,42 @@ abstract class AppLocalizations { /// **'Need to reconnect the app, ok?'** String get spoofDialogApplyContent; + /// No description provided for @spoofDialogApplyWarning. + /// + /// In en, this message translates to: + /// **'Your spoof will change immediately. But due to MAX specifics, you must re-login to the account for it to become visible'** + String get spoofDialogApplyWarning; + + /// No description provided for @spoofDialogReloginTitle. + /// + /// In en, this message translates to: + /// **'Done!'** + String get spoofDialogReloginTitle; + + /// No description provided for @spoofDialogReloginContent. + /// + /// In en, this message translates to: + /// **'Due to MAX specifics, your spoof is changed, but changes will be visible only after re-login.'** + String get spoofDialogReloginContent; + + /// No description provided for @spoofDialogReloginWarning. + /// + /// In en, this message translates to: + /// **'Re-login now?'** + String get spoofDialogReloginWarning; + + /// No description provided for @spoofDialogReloginDeny. + /// + /// In en, this message translates to: + /// **'Later'** + String get spoofDialogReloginDeny; + + /// No description provided for @spoofDialogReloginConfirm. + /// + /// In en, this message translates to: + /// **'Re-login now'** + String get spoofDialogReloginConfirm; + /// No description provided for @spoofDialogApplyDeny. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 4422b8f..bfce511 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -183,7 +183,7 @@ class AppLocalizationsEn extends AppLocalizations { @override String get spoofDeviceTypeDescription => - 'Choose a device type for preset generation. Tapping \"Generate\" will only use presets of the selected type.'; + 'This field is not changeable, to avoid token association issues'; @override String get spoofDeviceTypeLabel => 'Device type'; @@ -256,6 +256,26 @@ class AppLocalizationsEn extends AppLocalizations { @override String get spoofDialogApplyContent => 'Need to reconnect the app, ok?'; + @override + String get spoofDialogApplyWarning => + 'Your spoof will change immediately. But due to MAX specifics, you must re-login to the account for it to become visible'; + + @override + String get spoofDialogReloginTitle => 'Done!'; + + @override + String get spoofDialogReloginContent => + 'Due to MAX specifics, your spoof is changed, but changes will be visible only after re-login.'; + + @override + String get spoofDialogReloginWarning => 'Re-login now?'; + + @override + String get spoofDialogReloginDeny => 'Later'; + + @override + String get spoofDialogReloginConfirm => 'Re-login now'; + @override String get spoofDialogApplyDeny => 'No'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 6648921..7a1c8b4 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -185,7 +185,7 @@ class AppLocalizationsRu extends AppLocalizations { @override String get spoofDeviceTypeDescription => - 'Выберите тип устройства для генерации пресетов. При нажатии \"Сгенерировать\" будут использоваться только пресеты выбранного типа.'; + 'Данное поле не изменяемое, во избежании проблем с ассоциацией токена'; @override String get spoofDeviceTypeLabel => 'Тип устройства'; @@ -258,6 +258,26 @@ class AppLocalizationsRu extends AppLocalizations { @override String get spoofDialogApplyContent => 'Нужно перезайти в приложение, ок?'; + @override + String get spoofDialogApplyWarning => + 'Ваш спуф изменится сразу. Но из-за особенностей МАХ, для того что-бы это стало заметно, вы должны перелогиниться в аккаунт'; + + @override + String get spoofDialogReloginTitle => 'Готово!'; + + @override + String get spoofDialogReloginContent => + 'Из-за особенности МАХ, ваш спуф изменён, но видны изменения будут только при перезаходе в аккаунт.'; + + @override + String get spoofDialogReloginWarning => 'Перезайти сейчас?'; + + @override + String get spoofDialogReloginDeny => 'Позже'; + + @override + String get spoofDialogReloginConfirm => 'Перелогиниться сейчас'; + @override String get spoofDialogApplyDeny => 'Не'; diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 2efbabb..0f2378c 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -64,7 +64,7 @@ "spoofMethodPartialDescription": "Рекомендуемый метод. Используются случайные данные, но ваш реальный часовой пояс и локаль для большей правдоподобности.", "spoofMethodFullDescription": "Все данные, включая часовой пояс и локаль, генерируются случайно. Использование этого метода на ваш страх и риск!", "spoofDeviceTypeTitle": "Тип устройства", - "spoofDeviceTypeDescription": "Выберите тип устройства для генерации пресетов. При нажатии \"Сгенерировать\" будут использоваться только пресеты выбранного типа.", + "spoofDeviceTypeDescription": "Данное поле не изменяемое, во избежании проблем с ассоциацией токена", "spoofDeviceTypeLabel": "Тип устройства", "spoofMainSectionTitle": "Основные данные", "spoofFieldDeviceName": "Имя устройства", @@ -88,6 +88,12 @@ "spoofDialogYes": "Да", "spoofDialogApplyTitle": "Применить настройки?", "spoofDialogApplyContent": "Нужно перезайти в приложение, ок?", + "spoofDialogApplyWarning": "Ваш спуф изменится сразу. Но из-за особенностей МАХ, для того что-бы это стало заметно, вы должны перелогиниться в аккаунт", + "spoofDialogReloginTitle": "Готово!", + "spoofDialogReloginContent": "Из-за особенности МАХ, ваш спуф изменён, но видны изменения будут только при перезаходе в аккаунт.", + "spoofDialogReloginWarning": "Перезайти сейчас?", + "spoofDialogReloginDeny": "Позже", + "spoofDialogReloginConfirm": "Перелогиниться сейчас", "spoofDialogApplyDeny": "Не", "spoofDialogApplyConfirm": "Ок!", "spoofErrorApplyFailed": "Ошибка при применении настроек: {error}", diff --git a/lib/main.dart b/lib/main.dart index 82ae9b6..0ce4998 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -36,6 +36,7 @@ void main() async { await AppDatabase.init(); await api.connect(); final initialLocale = await _loadInitialLocale(); + final prefs = await SharedPreferences.getInstance(); final initialFpsOverlay = prefs.getBool('dev_fps_overlay') ?? false; runApp( @@ -70,8 +71,9 @@ class KometAppState extends State { late Locale _locale; bool _isLoggingOut = false; - late final ValueNotifier fpsOverlayEnabled = - ValueNotifier(widget.initialFpsOverlay); + late final ValueNotifier fpsOverlayEnabled = ValueNotifier( + widget.initialFpsOverlay, + ); @override void initState() { @@ -79,10 +81,14 @@ class KometAppState extends State { _locale = widget.initialLocale; api.setReconnectCallback(() async { - final accountId = await TokenStorage.getActiveAccountId(); - if (accountId != null) { - await accountModule.login(accountId: accountId); - } + try { + final accountId = await TokenStorage.getActiveAccountId(); + if (accountId != null && + await TokenStorage.readToken(accountId) != null) { + final token = await TokenStorage.readToken(accountId); + await accountModule.login(accountId: accountId, token: token); + } + } catch (_) {} }); api.sessionExpiredStream.listen((SessionExpiredException e) async { @@ -250,7 +256,7 @@ class _StartupScreenState extends State<_StartupScreen> { Future _tryAutoLogin() async { final accountId = await TokenStorage.getActiveAccountId(); - if (accountId == null) { + if (accountId == null || await TokenStorage.readToken(accountId) == null) { _goToLogin(); return; }