надеюсь будет работать сукi

This commit is contained in:
torvalds
2026-07-05 23:43:15 +07:00
parent 1fd4a10814
commit 496b7a40ab
10 changed files with 419 additions and 117 deletions
+162 -70
View File
@@ -70,10 +70,16 @@ class Api {
StreamSubscription<SocketState>? _socketStateSubscription;
Timer? _pingTimer;
Timer? _reconnectTimer;
Timer? _connectWatchdog;
int _connectGen = 0;
int _reconnectAttempts = 0;
bool _autoReconnect = false;
int _sessionEpoch = 0;
static const Duration _connectWatchdogTimeout = Duration(seconds: 75);
static const Duration _shouldArmTimeout = Duration(seconds: 5);
static const Duration _endpointTimeout = Duration(seconds: 5);
int get sessionEpoch => _sessionEpoch;
/// Залипает на время сессии: VPN-путь не сработал — идём мимо туннеля.
@@ -83,87 +89,168 @@ class Api {
/// Подключается к серверу, шлёт хэндшейк, запускает пинг.
Future<void> connect() async {
if (_sessionState != SessionState.disconnected) return;
// Ставим автоматический реконнект и статус подключения
_autoReconnect = true;
_setSessionState(SessionState.connecting);
_dataSubscription = _connection.dataStream.listen(_onDataReceived);
_socketStateSubscription = _connection.stateStream.listen((socketState) {
if (socketState == SocketState.disconnected &&
_sessionState != SessionState.disconnected) {
_onDisconnected();
}
});
final bypassArmed = await VpnBypassService.instance.shouldArm();
if (!bypassArmed) _bypassActive = false;
final useBypass = _bypassActive && bypassArmed;
// Попытку через VPN ограничиваем по времени, чтобы быстро понять,
// что туннель не пропускает, и переключиться на обход.
final attemptTimeout = bypassArmed && !useBypass
? const Duration(seconds: 8)
: null;
try {
final endpoint = await ServerConfig.loadEndpoint();
await _connection.connect(
endpoint.host,
endpoint.port,
bypassVpn: useBypass,
timeout: attemptTimeout,
);
} catch (e) {
await _handleConnectFailure(
e,
phase: 'Не удалось подключиться',
bypassArmed: bypassArmed,
useBypass: useBypass,
bypassWhy: 'подключение не удалось',
);
if (_sessionState != SessionState.disconnected) {
logger.i('connect пропущен: состояние ${_sessionState.name}');
return;
}
_setSessionState(SessionState.connected);
_reconnectAttempts = 0;
_autoReconnect = true;
final gen = ++_connectGen;
_setSessionState(SessionState.connecting);
logger.i('connect: старт (поколение $gen)');
_armConnectWatchdog(gen);
try {
final response = await sendHandshake();
if (response.isOk) {
_callsSeed = response.payload['callsSeed'] as int?;
_registrationCountries = _parseRegistrationCountries(response.payload);
_sessionState = SessionState.online;
_sessionEpoch++;
_startPinging();
logger.i('Сессия онлайн, хэндшейк ок');
if (_onReconnectCallback != null) {
try {
await _onReconnectCallback!();
} catch (e) {
logger.w('Авто-логин при хэндшейке не удался: $e');
}
_dataSubscription = _connection.dataStream.listen(_onDataReceived);
_socketStateSubscription = _connection.stateStream.listen((socketState) {
if (socketState == SocketState.disconnected &&
_sessionState != SessionState.disconnected) {
_onDisconnected();
}
if (_sessionState == SessionState.online) {
_stateController.add(SessionState.online);
_handshakeSuccessController.add(
response.payload['device_name'] as String? ?? 'Unknown',
});
bool bypassArmed;
try {
bypassArmed = await VpnBypassService.instance.shouldArm().timeout(
_shouldArmTimeout,
);
} catch (e) {
logger.w('connect: shouldArm завис/упал ($e) — без обхода VPN');
bypassArmed = false;
}
if (gen != _connectGen) return;
if (!bypassArmed) _bypassActive = false;
final useBypass = _bypassActive && bypassArmed;
final attemptTimeout = bypassArmed && !useBypass
? const Duration(seconds: 8)
: null;
({String host, int port}) endpoint;
try {
endpoint = await ServerConfig.loadEndpoint().timeout(_endpointTimeout);
} catch (e) {
logger.w('connect: loadEndpoint завис/упал ($e) — дефолтный endpoint');
endpoint = (
host: ServerConfig.defaultHost,
port: ServerConfig.defaultPort,
);
}
if (gen != _connectGen) return;
logger.i(
'connect: endpoint ${endpoint.host}:${endpoint.port}, bypass=$useBypass',
);
try {
await _connection.connect(
endpoint.host,
endpoint.port,
bypassVpn: useBypass,
timeout: attemptTimeout,
);
} catch (e) {
if (gen != _connectGen) return;
await _handleConnectFailure(
e,
phase: 'Не удалось подключиться',
bypassArmed: bypassArmed,
useBypass: useBypass,
bypassWhy: 'подключение не удалось',
);
return;
}
if (gen != _connectGen) return;
_setSessionState(SessionState.connected);
_reconnectAttempts = 0;
try {
logger.i('connect: сокет готов, отправляю хэндшейк');
final response = await sendHandshake();
if (gen != _connectGen) return;
if (response.isOk) {
_callsSeed = response.payload['callsSeed'] as int?;
_registrationCountries = _parseRegistrationCountries(
response.payload,
);
_sessionState = SessionState.online;
_sessionEpoch++;
_cancelConnectWatchdog();
_startPinging();
logger.i('Сессия онлайн, хэндшейк ок');
if (_onReconnectCallback != null) {
try {
await _onReconnectCallback!();
} catch (e) {
logger.w('Авто-логин при хэндшейке не удался: $e');
}
}
if (_sessionState == SessionState.online) {
_stateController.add(SessionState.online);
_handshakeSuccessController.add(
response.payload['device_name'] as String? ?? 'Unknown',
);
}
} else {
logger.e('Хэндшейк отклонён: ${response.payload}');
await _handleConnectFailure(
StateError('хэндшейк отклонён сервером'),
phase: 'Хэндшейк отклонён',
bypassArmed: bypassArmed,
useBypass: useBypass,
bypassWhy: 'хэндшейк отклонён',
disconnectSocket: true,
);
}
} else {
logger.e('Хэндшейк отклонён: ${response.payload}');
} catch (e) {
if (gen != _connectGen) return;
await _handleConnectFailure(
e,
phase: 'Ошибка хэндшейка',
bypassArmed: bypassArmed,
useBypass: useBypass,
bypassWhy: 'хэндшейк не прошёл',
disconnectSocket: true,
);
}
} catch (e) {
await _handleConnectFailure(
e,
phase: 'Ошибка хэндшейка',
bypassArmed: bypassArmed,
useBypass: useBypass,
bypassWhy: 'хэндшейк не прошёл',
disconnectSocket: true,
);
} catch (e, st) {
logger.e('connect: непредвиденная ошибка: $e\n$st');
if (gen == _connectGen) await _resetStuckConnect(gen);
}
}
void _armConnectWatchdog(int gen) {
_connectWatchdog?.cancel();
_connectWatchdog = Timer(_connectWatchdogTimeout, () {
if (gen != _connectGen) return;
if (_sessionState == SessionState.online ||
_sessionState == SessionState.disconnected) {
return;
}
logger.e(
'connect: watchdog ${_connectWatchdogTimeout.inSeconds}с — застряли в '
'${_sessionState.name}, принудительный сброс',
);
unawaited(_resetStuckConnect(gen));
});
}
void _cancelConnectWatchdog() {
_connectWatchdog?.cancel();
_connectWatchdog = null;
}
Future<void> _resetStuckConnect(int gen) async {
if (gen != _connectGen) return;
_connectGen++;
_cancelConnectWatchdog();
_cleanup();
try {
await _connection.disconnect();
} catch (_) {}
_setSessionState(SessionState.disconnected);
if (_autoReconnect) _scheduleReconnect();
}
Future<void> _handleConnectFailure(
Object error, {
required String phase,
@@ -173,6 +260,7 @@ class Api {
bool disconnectSocket = false,
}) async {
logger.e('$phase: $error');
_cancelConnectWatchdog();
if (_sessionState != SessionState.disconnected) {
_cleanup();
if (disconnectSocket) await _connection.disconnect();
@@ -193,6 +281,7 @@ class Api {
Future<void> disconnect() async {
_autoReconnect = false;
_bypassActive = false;
_connectGen++;
_reconnectTimer?.cancel();
_cleanup();
await _connection.disconnect();
@@ -441,6 +530,7 @@ class Api {
}
void _onDisconnected() {
_connectGen++;
_cleanup();
_setSessionState(SessionState.disconnected);
if (_autoReconnect) _scheduleReconnect();
@@ -463,6 +553,7 @@ class Api {
}
Future<void> _forceReconnect() async {
_connectGen++;
_cleanup();
await _connection.disconnect();
_reconnectAttempts = 0;
@@ -472,6 +563,7 @@ class Api {
}
void _cleanup() {
_cancelConnectWatchdog();
_pingTimer?.cancel();
_dataSubscription?.cancel();
_socketStateSubscription?.cancel();
+6 -7
View File
@@ -269,16 +269,15 @@ class AccountModule {
final dataMap = data.cast<dynamic, dynamic>();
if (resolvedAccountId == null) {
final profileMap = dataMap['profile'];
if (profileMap is Map) {
final contact = profileMap['contact'];
if (contact is Map) {
resolvedAccountId = contact['id'] as int?;
}
}
resolvedAccountId = extractAccountId(dataMap);
if (resolvedAccountId == null) {
logger.e(
'login: accountId не найден в ответе; '
'${describeResponseShape(dataMap)}',
);
throw Exception('login: не удалось определить accountId из ответа');
}
logger.i('login: accountId=$resolvedAccountId определён из ответа');
await TokenStorage.saveToken(authToken, resolvedAccountId);
await TokenStorage.setActiveAccount(resolvedAccountId);
await SpoofingService.commitPendingSpoof(resolvedAccountId);
@@ -2,6 +2,70 @@ import 'dart:convert';
import '../../../core/storage/app_database.dart';
int? _coerceAccountId(dynamic value) {
if (value is int) return value;
if (value is double) return value.toInt();
if (value is String) return int.tryParse(value.trim());
return null;
}
int? extractAccountId(dynamic response) {
if (response is! Map) return null;
final profile = response['profile'];
if (profile is Map) {
final contact = profile['contact'];
if (contact is Map) {
final cid =
_coerceAccountId(contact['id']) ??
_coerceAccountId(contact['contactId']) ??
_coerceAccountId(contact['accountId']);
if (cid != null) return cid;
}
final pid =
_coerceAccountId(profile['id']) ??
_coerceAccountId(profile['accountId']);
if (pid != null) return pid;
}
final contact = response['contact'];
if (contact is Map) {
final cid =
_coerceAccountId(contact['id']) ??
_coerceAccountId(contact['contactId']);
if (cid != null) return cid;
}
final account = response['account'];
if (account is Map) {
final aid =
_coerceAccountId(account['id']) ??
_coerceAccountId(account['accountId']);
if (aid != null) return aid;
}
return _coerceAccountId(response['accountId']) ??
_coerceAccountId(response['account_id']);
}
String describeResponseShape(dynamic response) {
if (response is! Map) return 'не-Map (${response.runtimeType})';
final sb = StringBuffer('keys=${response.keys.toList()}');
final profile = response['profile'];
if (profile is Map) {
sb.write(' profile.keys=${profile.keys.toList()}');
final contact = profile['contact'];
if (contact is Map) {
sb.write(' contact.keys=${contact.keys.toList()}');
} else {
sb.write(' profile.contact=${contact.runtimeType}');
}
} else {
sb.write(' profile=${profile.runtimeType}');
}
return sb.toString();
}
class PrivacyConfig {
final String searchByPhone;
final String incomingCall;
@@ -295,13 +359,7 @@ class VerifyCodeResult {
String? get challengeHint => passwordChallenge?['hint'] as String?;
int? get accountId {
final profileData = payload['profile'];
if (profileData is! Map) return null;
final contact = profileData['contact'];
if (contact is! Map) return null;
return contact['id'] as int?;
}
int? get accountId => extractAccountId(payload);
String? _nestedToken(String key) {
final attrs = payload['tokenAttrs'];
+37 -8
View File
@@ -1,6 +1,9 @@
import 'dart:async';
import 'package:shared_preferences/shared_preferences.dart';
import '../storage/token_storage.dart';
import '../utils/logger.dart';
enum ProxyType { none, socks5, httpConnect }
@@ -35,26 +38,52 @@ abstract class ProxyConfig {
static const String _prefUsername = 'proxy_username';
static const String _prefPassword = 'proxy_password';
static const Duration _secureReadTimeout = Duration(seconds: 5);
static Future<String?> _readSecureSafe(String key) async {
try {
return await TokenStorage.readSecure(key).timeout(_secureReadTimeout);
} catch (e) {
logger.w('ProxyConfig: чтение secure "$key" не удалось/зависло: $e');
return null;
}
}
static Future<void> _migrateLegacySecure(String key, String value) async {
try {
await TokenStorage.writeSecure(key, value).timeout(_secureReadTimeout);
final prefs = await SharedPreferences.getInstance();
await prefs.remove(key);
} catch (e) {
logger.w('ProxyConfig: миграция legacy "$key" не удалась: $e');
}
}
static Future<ProxySettings> load() async {
final prefs = await SharedPreferences.getInstance();
final typeIndex = prefs.getInt(_prefType) ?? 0;
final host = prefs.getString(_prefHost) ?? '';
final port = prefs.getInt(_prefPort) ?? 1080;
var username = await TokenStorage.readSecure(_prefUsername);
var password = await TokenStorage.readSecure(_prefPassword);
if (ProxyType.values[typeIndex.clamp(0, ProxyType.values.length - 1)] ==
ProxyType.none ||
host.isEmpty) {
return ProxySettings(
type: ProxyType.values[typeIndex.clamp(0, ProxyType.values.length - 1)],
host: host,
port: port,
);
}
var username = await _readSecureSafe(_prefUsername);
var password = await _readSecureSafe(_prefPassword);
final legacyUsername = prefs.getString(_prefUsername);
final legacyPassword = prefs.getString(_prefPassword);
if (username == null && legacyUsername != null) {
username = legacyUsername;
await TokenStorage.writeSecure(_prefUsername, legacyUsername);
unawaited(_migrateLegacySecure(_prefUsername, legacyUsername));
}
if (password == null && legacyPassword != null) {
password = legacyPassword;
await TokenStorage.writeSecure(_prefPassword, legacyPassword);
}
if (legacyUsername != null || legacyPassword != null) {
await prefs.remove(_prefUsername);
await prefs.remove(_prefPassword);
unawaited(_migrateLegacySecure(_prefPassword, legacyPassword));
}
return ProxySettings(
type: ProxyType.values[typeIndex.clamp(0, ProxyType.values.length - 1)],
+38 -10
View File
@@ -15,6 +15,8 @@ enum SocketState { disconnected, connecting, connected }
/// Отдаёт сырые байты через [dataStream], сборкой пакетов занимается [PacketReceiver].
class Connection {
static const Duration _defaultConnectTimeout = Duration(seconds: 15);
static const Duration _proxyLoadTimeout = Duration(seconds: 8);
static const Duration _vpnCallTimeout = Duration(seconds: 5);
SecureSocket? _socket;
StreamSubscription<Uint8List>? _subscription;
@@ -40,20 +42,41 @@ class Connection {
bool bypassVpn = false,
Duration? timeout,
}) async {
if (_state != SocketState.disconnected) return;
if (_state != SocketState.disconnected) {
logger.w('Connection.connect пропущен: state=$_state (уже $_state)');
return;
}
_setState(SocketState.connecting);
try {
final proxySettings = await ProxyConfig.load();
// Решение «обходить VPN или нет» принимает вызывающий (Api):
// первая попытка идёт через VPN, при её провале — мимо туннеля.
if (bypassVpn) {
await VpnBypassService.instance.bind();
} else {
await VpnBypassService.instance.restoreDefault();
logger.i('Connection: загрузка прокси-конфига');
ProxySettings proxySettings;
try {
proxySettings = await ProxyConfig.load().timeout(_proxyLoadTimeout);
} catch (e) {
logger.w('Connection: ProxyConfig.load завис/упал ($e) — без прокси');
proxySettings = const ProxySettings();
}
logger.i(
'Connection: VPN ${bypassVpn ? 'bind (обход)' : 'restoreDefault'}',
);
try {
if (bypassVpn) {
await VpnBypassService.instance.bind().timeout(_vpnCallTimeout);
} else {
await VpnBypassService.instance
.restoreDefault()
.timeout(_vpnCallTimeout);
}
} catch (e) {
logger.w('Connection: VPN-вызов завис/упал ($e) — продолжаю');
}
logger.i(
'Connection: открываю сокет $host:$port '
'(прокси: ${proxySettings.isEnabled ? proxySettings.type.name : 'нет'})',
);
final socket = await _openSecureSocket(
host,
port,
@@ -109,7 +132,9 @@ class Connection {
socket = await connector.connect(host, port).timeout(connectTimeout);
logger.i('Подключено через прокси ${proxySettings.type.name}');
} else {
logger.i('Connection: TCP connect $host:$port (лимит ${connectTimeout.inSeconds}с)');
socket = await Socket.connect(host, port, timeout: connectTimeout);
logger.i('Connection: TCP установлен, начинаю TLS');
}
final allowInsecure = await TlsConfig.isInsecureAllowed();
if (allowInsecure) {
@@ -121,8 +146,11 @@ class Connection {
? SecureSocket.secure(socket, host: host, onBadCertificate: (_) => true)
: SecureSocket.secure(socket, host: host);
try {
return await secured.timeout(connectTimeout);
final result = await secured.timeout(connectTimeout);
logger.i('Connection: TLS-handshake завершён');
return result;
} on TimeoutException {
logger.w('Connection: TLS-handshake таймаут ${connectTimeout.inSeconds}с');
socket.destroy();
rethrow;
}
+58 -12
View File
@@ -57,11 +57,15 @@ class _SessionData {
final DateTime startedAt;
final List<_LogEntry> entries;
final bool truncated;
final List<String> logLines;
final bool logsTruncated;
_SessionData({
required this.startedAt,
required this.entries,
this.truncated = false,
this.logLines = const [],
this.logsTruncated = false,
});
static _SessionData fromJson(Map data) {
@@ -72,37 +76,43 @@ class _SessionData {
if (e is Map) entries.add(_LogEntry.fromJson(e));
}
}
final rawLogs = data['logs'];
final logLines = <String>[];
if (rawLogs is List) {
for (final l in rawLogs) {
logLines.add(l.toString());
}
}
return _SessionData(
startedAt:
DateTime.tryParse(data['startedAt']?.toString() ?? '') ??
DateTime.fromMillisecondsSinceEpoch(0),
entries: entries,
truncated: data['truncated'] == true,
logLines: logLines,
logsTruncated: data['logsTruncated'] == true,
);
}
}
/// Персистентный лог запросов для кнопки «Отладочный лог».
///
/// Хранит ВСЕ запросы (с payload) за последние [_maxSessions] заходов в
/// приложение: каждый запуск — отдельная сессия-файл, старые ротируются.
/// Пинги идут мимо [Api.sendRequest], поэтому в лог не попадают.
///
/// Payload сохраняется уже отредактированным — токен скрыт целиком, от номера
/// остаются первые 3 символа, остальное видно. Поэтому секреты не лежат на диске.
class DebugSessionLog {
DebugSessionLog._();
static final DebugSessionLog instance = DebugSessionLog._();
static const int _maxSessions = 3;
static const int _maxEntriesPerSession = 2000;
static const int _maxLogLinesPerSession = 5000;
static const Duration _flushDebounce = Duration(seconds: 3);
static final RegExp _ansiEscape = RegExp(r'\x1B\[[0-9;]*m');
Directory? _dir;
File? _currentFile;
DateTime? _currentStart;
final List<_LogEntry> _entries = [];
final List<String> _logLines = [];
bool _truncated = false;
bool _logsTruncated = false;
bool _initialized = false;
bool _dirty = false;
Timer? _flushTimer;
@@ -120,12 +130,23 @@ class DebugSessionLog {
_currentFile = File(
'${dir.path}/session_${_currentStart!.millisecondsSinceEpoch}.json',
);
if (_dirty) _scheduleFlush();
} catch (_) {
_dir = null;
_currentFile = null;
}
}
void recordLogLine(String line) {
final clean = line.replaceAll(_ansiEscape, '');
_logLines.add(clean);
if (_logLines.length > _maxLogLinesPerSession) {
_logLines.removeRange(0, _logLines.length - _maxLogLinesPerSession);
_logsTruncated = true;
}
_scheduleFlush();
}
void recordRequest(int opcode, int seq, dynamic payload) {
_entries.add(
_LogEntry(
@@ -189,6 +210,8 @@ class DebugSessionLog {
'startedAt': _currentStart?.toIso8601String(),
'truncated': _truncated,
'entries': _entries.map((e) => e.toJson()).toList(),
'logsTruncated': _logsTruncated,
'logs': List.of(_logLines),
});
await file.writeAsString(data);
} catch (_) {}
@@ -249,6 +272,8 @@ class DebugSessionLog {
startedAt: _currentStart ?? DateTime.now(),
entries: List.of(_entries),
truncated: _truncated,
logLines: List.of(_logLines),
logsTruncated: _logsTruncated,
),
);
sessions.sort((a, b) => a.startedAt.compareTo(b.startedAt));
@@ -256,7 +281,8 @@ class DebugSessionLog {
? sessions.sublist(sessions.length - _maxSessions)
: sessions;
final totalEntries = lastN.fold<int>(0, (sum, s) => sum + s.entries.length);
if (totalEntries == 0) return null;
final totalLogs = lastN.fold<int>(0, (sum, s) => sum + s.logLines.length);
if (totalEntries == 0 && totalLogs == 0) return null;
final buffer = StringBuffer();
buffer.writeln('Komet — отладочный лог');
@@ -264,6 +290,7 @@ class DebugSessionLog {
buffer.writeln('Экспортирован: ${DateTime.now().toIso8601String()}');
buffer.writeln('Заходов в приложение: ${lastN.length}');
buffer.writeln('Всего запросов: $totalEntries');
buffer.writeln('Всего строк лога: $totalLogs');
buffer.writeln('Скрыто: токен полностью, номер кроме первых 3 символов');
buffer.writeln();
@@ -275,12 +302,31 @@ class DebugSessionLog {
);
buffer.writeln(
'запросов: ${session.entries.length}'
'${session.truncated ? ' (обрезано до $_maxEntriesPerSession)' : ''}',
'${session.truncated ? ' (обрезано до $_maxEntriesPerSession)' : ''}'
' · строк лога: ${session.logLines.length}'
'${session.logsTruncated ? ' (обрезано до $_maxLogLinesPerSession)' : ''}',
);
buffer.writeln('==================================================');
buffer.writeln();
for (var i = 0; i < session.entries.length; i++) {
_writeEntry(buffer, i + 1, session.entries[i]);
buffer.writeln('----- ЛОГИ ПРИЛОЖЕНИЯ -----');
if (session.logLines.isEmpty) {
buffer.writeln('(пусто)');
} else {
for (final line in session.logLines) {
buffer.writeln(line);
}
}
buffer.writeln();
buffer.writeln('----- ЗАПРОСЫ -----');
if (session.entries.isEmpty) {
buffer.writeln('(пусто)');
buffer.writeln();
} else {
for (var i = 0; i < session.entries.length; i++) {
_writeEntry(buffer, i + 1, session.entries[i]);
}
}
}
return buffer.toString();
+12 -1
View File
@@ -3,6 +3,8 @@ import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:logger/logger.dart';
import 'debug_session_log.dart';
Level _minimumLogLevel() {
const raw = String.fromEnvironment('KOMET_LOG_LEVEL', defaultValue: '');
switch (raw.toLowerCase()) {
@@ -41,9 +43,18 @@ final logger = Logger(
filter: _logFilter(),
level: _minimumLogLevel(),
printer: KometLogPrinter(),
output: ConsoleOutput(),
output: MultiOutput([ConsoleOutput(), DebugSessionLogOutput()]),
);
class DebugSessionLogOutput extends LogOutput {
@override
void output(OutputEvent event) {
for (final line in event.lines) {
DebugSessionLog.instance.recordLogLine(line);
}
}
}
int _importanceSortKey(Level level) {
final v = level.value;
if (v >= 5999) {
@@ -49,7 +49,7 @@ class DebugQuickActionsSection extends StatelessWidget {
),
const SizedBox(height: 2),
Text(
'Все запросы за последние 3 захода в приложение',
'Все логи и запросы за последние 3 захода в приложение',
style: TextStyle(
color: cs.onSurfaceVariant,
fontSize: 13,
@@ -73,7 +73,7 @@ class _DebugMenuScreenState extends State<DebugMenuScreen> {
endpoint: TrafficMonitor.instance.activeEndpoint,
);
if (content == null) {
if (mounted) showCustomNotification(context, 'Нет запросов для лога');
if (mounted) showCustomNotification(context, 'Лог пуст');
return;
}
final bytes = Uint8List.fromList(utf8.encode(content));
+39
View File
@@ -105,8 +105,47 @@ Future<Locale> _loadInitialLocale() async {
return const Locale('ru');
}
void _installLogCapture() {
final previousDebugPrint = debugPrint;
debugPrint = (String? message, {int? wrapWidth}) {
if (message != null) {
final t = DateTime.now();
final stamp =
'${t.hour.toString().padLeft(2, '0')}:${t.minute.toString().padLeft(2, '0')}:${t.second.toString().padLeft(2, '0')}.${t.millisecond.toString().padLeft(3, '0')}';
DebugSessionLog.instance.recordLogLine(' |$stamp P $message');
}
previousDebugPrint(message, wrapWidth: wrapWidth);
};
final previousFlutterOnError = FlutterError.onError;
FlutterError.onError = (FlutterErrorDetails details) {
DebugSessionLog.instance.recordLogLine(
' | FlutterError: ${details.exceptionAsString()}',
);
if (details.stack != null) {
DebugSessionLog.instance.recordLogLine(details.stack.toString());
}
if (previousFlutterOnError != null) {
previousFlutterOnError(details);
} else {
FlutterError.presentError(details);
}
};
final previousPlatformOnError = ui.PlatformDispatcher.instance.onError;
ui.PlatformDispatcher.instance.onError = (Object error, StackTrace stack) {
DebugSessionLog.instance.recordLogLine(' | Uncaught: $error');
DebugSessionLog.instance.recordLogLine(stack.toString());
if (previousPlatformOnError != null) {
return previousPlatformOnError(error, stack);
}
return false;
};
}
void main() async {
WidgetsFlutterBinding.ensureInitialized();
_installLogCapture();
VideoPlayerMediaKit.ensureInitialized(
windows: true,
linux: true,