нахуевертил

This commit is contained in:
Jganenokk
2026-04-12 14:18:26 +07:00
parent 6f2dae7032
commit 24b7ff6aff
11 changed files with 213 additions and 62 deletions
+12 -4
View File
@@ -32,6 +32,7 @@ class Api {
final _stateController = StreamController<SessionState>.broadcast();
final _sessionExpiredController =
StreamController<SessionExpiredException>.broadcast();
final _handshakeSuccessController = StreamController<String>.broadcast();
Map<dynamic, dynamic>? _userAgent;
Map<dynamic, dynamic>? get userAgent => _userAgent;
@@ -44,6 +45,8 @@ class Api {
Stream<SessionState> get stateStream => _stateController.stream;
Stream<SessionExpiredException> get sessionExpiredStream =>
_sessionExpiredController.stream;
Stream<String> get handshakeSuccessStream =>
_handshakeSuccessController.stream;
SessionState get state => _sessionState;
StreamSubscription<Uint8List>? _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<void> reconnectAndLogin() async {
await connect();
if (_sessionState == SessionState.online && _onReconnectCallback != null) {
_onReconnectCallback!();
}
}
void Function()? _onReconnectCallback;
+3 -1
View File
@@ -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));
+4
View File
@@ -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();
@@ -199,7 +199,20 @@ class _ChatListScreenState extends State<ChatListScreen>
Future<void> _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<ChatListScreen>
_jumpFolderPageToSelection();
});
}
} else {
} catch (_) {
_syncFolderChatScrollControllersForCount(1);
if (mounted) {
setState(() {
@@ -253,6 +266,9 @@ class _ChatListScreenState extends State<ChatListScreen>
_foldersListKnown = null;
_isInitialLoading = false;
});
}
} finally {
if (mounted) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
_jumpFolderPageToSelection();
+70 -43
View File
@@ -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<SpoofScreen> {
_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<SpoofScreen> {
_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<SpoofScreen> {
if (!mounted) return;
final l10n = AppLocalizations.of(context)!;
final confirmed = await showDialog<bool>(
final confirmed = await showDialog<String>(
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<SpoofScreen> {
text: l10n.spoofDeviceTypeDescription,
),
const SizedBox(height: 12),
_buildChipSelector<String>(
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<SpoofScreen> {
);
}
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,
+7 -1
View File
@@ -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}",
+37 -1
View File
@@ -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:
+21 -1
View File
@@ -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';
+21 -1
View File
@@ -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 => 'Не';
+7 -1
View File
@@ -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}",
+13 -7
View File
@@ -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<KometApp> {
late Locale _locale;
bool _isLoggingOut = false;
late final ValueNotifier<bool> fpsOverlayEnabled =
ValueNotifier(widget.initialFpsOverlay);
late final ValueNotifier<bool> fpsOverlayEnabled = ValueNotifier(
widget.initialFpsOverlay,
);
@override
void initState() {
@@ -79,10 +81,14 @@ class KometAppState extends State<KometApp> {
_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<void> _tryAutoLogin() async {
final accountId = await TokenStorage.getActiveAccountId();
if (accountId == null) {
if (accountId == null || await TokenStorage.readToken(accountId) == null) {
_goToLogin();
return;
}