From 574206dc219d46bc94668aa07a1ed818f3f65adf Mon Sep 17 00:00:00 2001 From: klockky Date: Sun, 28 Jun 2026 14:03:29 +0300 Subject: [PATCH] =?UTF-8?q?feat(devtools):=20=D0=BC=D0=BE=D0=BD=D0=B8?= =?UTF-8?q?=D1=82=D0=BE=D1=80=20=D1=81=D0=BE=D0=BA=D0=B5=D1=82-=D1=82?= =?UTF-8?q?=D1=80=D0=B0=D1=84=D0=B8=D0=BA=D0=B0=20=D0=B2=20=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D1=8E=20=D1=80=D0=B0=D0=B7=D1=80=D0=B0=D0=B1=D0=BE=D1=82?= =?UTF-8?q?=D1=87=D0=B8=D0=BA=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/backend/api.dart | 2 + lib/core/transport/connection.dart | 13 + lib/core/transport/sender.dart | 2 + lib/core/transport/traffic_monitor.dart | 236 +++++++++ .../screens/profile/debug_menu_screen.dart | 66 +++ .../profile/traffic_monitor_screen.dart | 483 ++++++++++++++++++ lib/main.dart | 3 + pubspec.lock | 24 + pubspec.yaml | 1 + 9 files changed, 830 insertions(+) create mode 100644 lib/core/transport/traffic_monitor.dart create mode 100644 lib/frontend/screens/profile/traffic_monitor_screen.dart diff --git a/lib/backend/api.dart b/lib/backend/api.dart index 7e115ef..247329c 100644 --- a/lib/backend/api.dart +++ b/lib/backend/api.dart @@ -13,6 +13,7 @@ import '../core/transport/connection.dart'; import '../core/transport/dispatcher.dart'; import '../core/transport/receiver.dart'; import '../core/transport/sender.dart'; +import '../core/transport/traffic_monitor.dart'; import '../core/transport/vpn_bypass.dart'; import '../core/utils/logger.dart'; @@ -342,6 +343,7 @@ class Api { logger.e('PacketReceiver: ошибка распаковки: $e'); continue; } + TrafficMonitor.instance.recordIncoming(packet, raw.length); if (packet.isError && packet.payload is Map && (packet.payload['message'] == 'FAIL_LOGIN_TOKEN' || diff --git a/lib/core/transport/connection.dart b/lib/core/transport/connection.dart index 3b38410..ac7ee8e 100644 --- a/lib/core/transport/connection.dart +++ b/lib/core/transport/connection.dart @@ -6,6 +6,7 @@ import '../config/proxy_config.dart'; import '../utils/logger.dart'; import 'proxy_connector.dart'; import 'tls_config.dart'; +import 'traffic_monitor.dart'; import 'vpn_bypass.dart'; enum SocketState { disconnected, connecting, connected } @@ -64,6 +65,17 @@ class Connection { _setState(SocketState.connected); logger.i('Подключено к $host:$port'); + final route = proxySettings.isEnabled + ? 'через прокси ${proxySettings.type.name}' + : bypassVpn + ? 'напрямую (обход VPN)' + : 'прямое соединение'; + TrafficMonitor.instance.recordEvent( + 'Подключено', + detail: '$host:$port · TLS · $route', + endpoint: '$host:$port', + ); + _subscription = _socket!.listen( (data) { if (!_dataController.isClosed) _dataController.add(data); @@ -130,6 +142,7 @@ class Connection { _socket = null; if (socket != null) { + TrafficMonitor.instance.recordEvent('Соединение закрыто'); try { await socket.close(); } catch (e) { diff --git a/lib/core/transport/sender.dart b/lib/core/transport/sender.dart index f762c9b..758fab0 100644 --- a/lib/core/transport/sender.dart +++ b/lib/core/transport/sender.dart @@ -2,6 +2,7 @@ import '../protocol/packet.dart'; import '../utils/log_redact.dart'; import '../utils/logger.dart'; import 'connection.dart'; +import 'traffic_monitor.dart'; class PacketSender { int _seq = 0; @@ -17,6 +18,7 @@ class PacketSender { final seq = _nextSeq(); final data = packPacket(opcode, payload, seq: seq); connection.write(data); + TrafficMonitor.instance.recordOutgoing(opcode, payload, seq, data.length); logger.i( '=> {ver: 10, cmd: 0, seq: $seq, opcode: $opcode, payload: ${payloadForLog(payload)}}', ); diff --git a/lib/core/transport/traffic_monitor.dart b/lib/core/transport/traffic_monitor.dart new file mode 100644 index 0000000..4835eab --- /dev/null +++ b/lib/core/transport/traffic_monitor.dart @@ -0,0 +1,236 @@ +import 'dart:convert'; + +import 'package:flutter/foundation.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import '../protocol/opcode_map.dart'; +import '../protocol/packet.dart'; + +enum TrafficDirection { outgoing, incoming, event } + +class TrafficEntry { + final TrafficDirection direction; + final DateTime time; + final String label; + final int? opcode; + final int? seq; + final int? cmd; + final dynamic payload; + final int? byteSize; + final String? detail; + + TrafficEntry({ + required this.direction, + required this.time, + required this.label, + this.opcode, + this.seq, + this.cmd, + this.payload, + this.byteSize, + this.detail, + }); + + String get prettyPayload => prettyJson(payload); +} + +String prettyJson(dynamic value) { + if (value == null) return 'null'; + try { + return const JsonEncoder.withIndent(' ').convert(_sanitize(value)); + } catch (_) { + return value.toString(); + } +} + +const _redacted = '***'; + +const _sensitiveExportFields = { + 'token', + 'accesstoken', + 'refreshtoken', + 'authtoken', + 'password', + 'secret', + 'phone', + 'phonenumber', + 'email', + 'msisdn', + 'otp', + 'smscode', + 'verifycode', + 'pin', + 'qrlink', + 'webappdata', + 'deviceid', + 'instanceid', + 'mt_instanceid', + 'text', + 'caption', +}; + +bool _isSensitiveExportKey(Object? key) { + if (key is! String) return false; + return _sensitiveExportFields.contains(key.toLowerCase()); +} + +dynamic _redactForExport(dynamic value) { + if (value is Map) { + final out = {}; + value.forEach((k, v) { + out[k] = _isSensitiveExportKey(k) ? _redacted : _redactForExport(v); + }); + return out; + } + if (value is List) return value.map(_redactForExport).toList(); + return value; +} + +dynamic _sanitize(dynamic value) { + if (value is Map) { + final out = {}; + value.forEach((k, v) => out[k.toString()] = _sanitize(v)); + return out; + } + if (value is Uint8List) return ''; + if (value is List) return value.map(_sanitize).toList(); + if (value is num || value is bool || value is String) return value; + return value.toString(); +} + +/// Перехватчик сокет-трафика для меню разработчика. +/// +/// Захват включается только пока открыт экран монитора ([enabled]), +/// поэтому в обычной работе хуки в sender/dispatcher/connection почти +/// бесплатны (один ранний выход по флагу). +class TrafficMonitor extends ChangeNotifier { + TrafficMonitor._(); + static final TrafficMonitor instance = TrafficMonitor._(); + + static const int _maxEntries = 1000; + static const String _prefKey = 'dev_traffic_capture'; + static const bool _defaultEnabled = false; + + final List _entries = []; + String? _activeEndpoint; + + final ValueNotifier captureEnabled = ValueNotifier(_defaultEnabled); + + bool get enabled => captureEnabled.value; + + List get entries => List.unmodifiable(_entries); + String? get activeEndpoint => _activeEndpoint; + + Future load() async { + final prefs = await SharedPreferences.getInstance(); + captureEnabled.value = prefs.getBool(_prefKey) ?? _defaultEnabled; + } + + Future setEnabled(bool value) async { + if (captureEnabled.value != value) { + captureEnabled.value = value; + notifyListeners(); + } + final prefs = await SharedPreferences.getInstance(); + await prefs.setBool(_prefKey, value); + } + + void clear() { + _entries.clear(); + notifyListeners(); + } + + /// Сериализует захваченный трафик для экспорта. + /// Чувствительные поля payload (токены, телефоны, коды и т.п.) + /// маскируются через [redactForLog] — файлом можно делиться. + String buildExport({String? appVersion}) { + final data = { + 'tool': 'Komet traffic monitor', + 'appVersion': ?appVersion, + 'exportedAt': DateTime.now().toIso8601String(), + 'endpoint': _activeEndpoint, + 'entryCount': _entries.length, + 'sensitiveDataRedacted': true, + 'entries': _entries.map(_entryToJson).toList(), + }; + return const JsonEncoder.withIndent(' ').convert(data); + } + + Map _entryToJson(TrafficEntry e) { + return { + 'time': e.time.toIso8601String(), + 'direction': e.direction.name, + 'label': e.label, + if (e.opcode != null) 'opcode': e.opcode, + if (e.seq != null) 'seq': e.seq, + if (e.cmd != null) 'cmd': e.cmd, + if (e.byteSize != null) 'bytes': e.byteSize, + if (e.detail != null) 'detail': e.detail, + if (e.payload != null) 'payload': _sanitize(_redactForExport(e.payload)), + }; + } + + void recordOutgoing(int opcode, dynamic payload, int seq, int byteSize) { + if (!enabled) return; + _add( + TrafficEntry( + direction: TrafficDirection.outgoing, + time: DateTime.now(), + label: Opcode.name(opcode), + opcode: opcode, + seq: seq, + cmd: CmdType.request, + payload: payload, + byteSize: byteSize, + ), + ); + } + + void recordIncoming(Packet packet, int byteSize) { + if (!enabled) return; + _add( + TrafficEntry( + direction: TrafficDirection.incoming, + time: DateTime.now(), + label: Opcode.name(packet.opcode), + opcode: packet.opcode, + seq: packet.seq, + cmd: packet.cmd, + payload: packet.payload, + byteSize: byteSize, + ), + ); + } + + void recordEvent(String label, {String? detail, String? endpoint}) { + if (endpoint != null) _activeEndpoint = endpoint; + if (!enabled) return; + _add( + TrafficEntry( + direction: TrafficDirection.event, + time: DateTime.now(), + label: label, + detail: detail, + ), + ); + } + + void _add(TrafficEntry entry) { + _entries.add(entry); + if (_entries.length > _maxEntries) { + _entries.removeRange(0, _entries.length - _maxEntries); + } + _scheduleNotify(); + } + + bool _notifyScheduled = false; + + void _scheduleNotify() { + if (_notifyScheduled) return; + _notifyScheduled = true; + Future.microtask(() { + _notifyScheduled = false; + notifyListeners(); + }); + } +} diff --git a/lib/frontend/screens/profile/debug_menu_screen.dart b/lib/frontend/screens/profile/debug_menu_screen.dart index 5573f6c..245d220 100644 --- a/lib/frontend/screens/profile/debug_menu_screen.dart +++ b/lib/frontend/screens/profile/debug_menu_screen.dart @@ -26,6 +26,7 @@ import '../calls/call_screen.dart'; import '../../../core/calls/call_controller.dart'; import '../../widgets/connection_status.dart'; import '../digital_id/digital_id_web_screen.dart'; +import 'traffic_monitor_screen.dart'; class DebugMenuScreen extends StatefulWidget { const DebugMenuScreen({super.key}); @@ -496,6 +497,71 @@ class _DebugMenuScreenState extends State { ), ), ), + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 0), + child: Material( + color: cs.surfaceContainerHigh, + borderRadius: BorderRadius.circular(20), + child: InkWell( + borderRadius: BorderRadius.circular(20), + onTap: () => Navigator.push( + context, + MaterialPageRoute( + builder: (_) => const TrafficMonitorScreen(), + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 17, + ), + child: Row( + children: [ + Icon( + Symbols.lan, + color: cs.onSurfaceVariant, + size: 22, + weight: 400, + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Монитор трафика', + style: TextStyle( + color: cs.onSurface, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 2), + Text( + 'Реалтайм: домены, опкоды и payload внутри ' + 'сокет-соединения', + style: TextStyle( + color: cs.onSurfaceVariant, + fontSize: 13, + ), + ), + ], + ), + ), + Icon( + Symbols.chevron_right, + color: cs.onSurfaceVariant, + size: 22, + weight: 400, + ), + ], + ), + ), + ), + ), + ), + ), SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.fromLTRB(16, 12, 16, 0), diff --git a/lib/frontend/screens/profile/traffic_monitor_screen.dart b/lib/frontend/screens/profile/traffic_monitor_screen.dart new file mode 100644 index 0000000..e748c8a --- /dev/null +++ b/lib/frontend/screens/profile/traffic_monitor_screen.dart @@ -0,0 +1,483 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; +import 'package:material_symbols_icons/symbols.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:share_plus/share_plus.dart'; + +import '../../../core/protocol/packet.dart'; +import '../../../core/transport/traffic_monitor.dart'; +import '../../../core/utils/format.dart'; +import '../../widgets/custom_notification.dart'; + +class TrafficMonitorScreen extends StatefulWidget { + const TrafficMonitorScreen({super.key}); + + @override + State createState() => _TrafficMonitorScreenState(); +} + +class _TrafficMonitorScreenState extends State { + final _monitor = TrafficMonitor.instance; + final _scrollController = ScrollController(); + final Set _expanded = Set.identity(); + bool _stickToBottom = true; + + @override + void initState() { + super.initState(); + _scrollController.addListener(_onScroll); + } + + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + + void _onScroll() { + if (!_scrollController.hasClients) return; + final pos = _scrollController.position; + _stickToBottom = pos.pixels >= pos.maxScrollExtent - 80; + } + + void _scrollToBottom() { + if (!_scrollController.hasClients) return; + final pos = _scrollController.position; + if (pos.userScrollDirection != ScrollDirection.idle) return; + if (pos.pixels >= pos.maxScrollExtent) return; + _scrollController.jumpTo(pos.maxScrollExtent); + } + + void _clear() { + _expanded.clear(); + _monitor.clear(); + } + + Future _share() async { + if (_monitor.entries.isEmpty) return; + final box = context.findRenderObject() as RenderBox?; + try { + final json = _monitor.buildExport(); + final dir = await getTemporaryDirectory(); + final stamp = _fileStamp(DateTime.now()); + final file = File('${dir.path}/komet_traffic_$stamp.json'); + await file.writeAsString(json); + await Share.shareXFiles( + [XFile(file.path, mimeType: 'application/json')], + subject: 'Komet traffic capture', + sharePositionOrigin: box == null + ? null + : box.localToGlobal(Offset.zero) & box.size, + ); + } catch (e) { + if (mounted) { + showCustomNotification(context, 'Не удалось поделиться: $e'); + } + } + } + + String _fileStamp(DateTime t) { + String two(int n) => n.toString().padLeft(2, '0'); + return '${t.year}${two(t.month)}${two(t.day)}_' + '${two(t.hour)}${two(t.minute)}${two(t.second)}'; + } + + @override + Widget build(BuildContext context) { + final cs = Theme.of(context).colorScheme; + + return Scaffold( + backgroundColor: cs.surface, + body: SafeArea( + bottom: false, + child: Column( + children: [ + _topBar(cs), + _controlBar(cs), + Expanded( + child: AnimatedBuilder( + animation: _monitor, + builder: (context, _) { + final entries = _monitor.entries; + if (_stickToBottom && _expanded.isEmpty) { + WidgetsBinding.instance.addPostFrameCallback( + (_) => _scrollToBottom(), + ); + } + if (entries.isEmpty) return _emptyState(cs); + return ListView.builder( + controller: _scrollController, + physics: const BouncingScrollPhysics(), + padding: const EdgeInsets.fromLTRB(12, 8, 12, 24), + itemCount: entries.length, + itemBuilder: (context, i) { + final entry = entries[i]; + return _TrafficRow( + key: ValueKey(entry), + entry: entry, + expanded: _expanded.contains(entry), + onToggle: () => setState(() { + if (!_expanded.remove(entry)) _expanded.add(entry); + }), + ); + }, + ); + }, + ), + ), + ], + ), + ), + ); + } + + Widget _topBar(ColorScheme cs) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 4), + child: Row( + children: [ + IconButton( + icon: Icon( + Symbols.arrow_back, + color: cs.onSurface, + size: 24, + weight: 400, + ), + onPressed: () => Navigator.pop(context), + ), + const SizedBox(width: 4), + Expanded( + child: Text( + 'Монитор трафика', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: cs.onSurface, + fontSize: 20, + fontWeight: FontWeight.w700, + fontFamily: 'Outfit', + ), + ), + ), + AnimatedBuilder( + animation: _monitor, + builder: (context, _) { + final empty = _monitor.entries.isEmpty; + final activeColor = empty + ? cs.onSurfaceVariant.withValues(alpha: 0.4) + : cs.onSurface; + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + tooltip: 'Поделиться (без чувствительных данных)', + icon: Icon( + Symbols.ios_share, + color: activeColor, + size: 22, + weight: 400, + ), + onPressed: empty ? null : _share, + ), + IconButton( + tooltip: 'Очистить', + icon: Icon( + Symbols.delete_sweep, + color: activeColor, + size: 24, + weight: 400, + ), + onPressed: empty ? null : _clear, + ), + ], + ); + }, + ), + ], + ), + ); + } + + Widget _controlBar(ColorScheme cs) { + return Container( + margin: const EdgeInsets.fromLTRB(12, 4, 12, 8), + padding: const EdgeInsets.fromLTRB(16, 10, 12, 10), + decoration: BoxDecoration( + color: cs.surfaceContainerHigh, + borderRadius: BorderRadius.circular(16), + ), + child: AnimatedBuilder( + animation: _monitor, + builder: (context, _) { + final on = _monitor.enabled; + final endpoint = _monitor.activeEndpoint; + return Row( + children: [ + Container( + width: 10, + height: 10, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: on ? const Color(0xFF34C759) : cs.outline, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + endpoint ?? 'Нет соединения', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: cs.onSurface, + fontSize: 14, + fontWeight: FontWeight.w600, + fontFamily: 'monospace', + ), + ), + const SizedBox(height: 1), + Text( + on + ? 'Захват включён · ${_monitor.entries.length}' + : 'Захват остановлен · ${_monitor.entries.length}', + style: TextStyle( + color: cs.onSurfaceVariant, + fontSize: 12, + ), + ), + ], + ), + ), + Switch( + value: on, + onChanged: (v) => _monitor.setEnabled(v), + ), + ], + ); + }, + ), + ); + } + + Widget _emptyState(ColorScheme cs) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Symbols.cell_tower, size: 48, color: cs.onSurfaceVariant), + const SizedBox(height: 12), + Text( + _monitor.enabled ? 'Ожидание трафика…' : 'Захват выключен', + style: TextStyle(color: cs.onSurfaceVariant, fontSize: 14), + ), + ], + ), + ); + } +} + +class _TrafficRow extends StatelessWidget { + final TrafficEntry entry; + final bool expanded; + final VoidCallback onToggle; + + const _TrafficRow({ + super.key, + required this.entry, + required this.expanded, + required this.onToggle, + }); + + @override + Widget build(BuildContext context) { + final cs = Theme.of(context).colorScheme; + final e = entry; + final accent = _accentColor(cs, e.direction); + final hasPayload = e.payload != null; + + return Container( + margin: const EdgeInsets.only(bottom: 6), + decoration: BoxDecoration( + color: cs.surfaceContainerHigh, + borderRadius: BorderRadius.circular(12), + border: Border(left: BorderSide(color: accent, width: 3)), + ), + child: Material( + type: MaterialType.transparency, + child: InkWell( + borderRadius: BorderRadius.circular(12), + onTap: hasPayload ? onToggle : null, + child: Padding( + padding: const EdgeInsets.fromLTRB(12, 9, 12, 9), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + _formatTime(e.time), + style: TextStyle( + color: cs.outline, + fontSize: 11, + fontFamily: 'monospace', + ), + ), + const SizedBox(width: 8), + Icon(_directionIcon(e.direction), color: accent, size: 15), + const SizedBox(width: 6), + Expanded( + child: Text( + e.label, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: cs.onSurface, + fontSize: 14, + fontWeight: FontWeight.w600, + fontFamily: 'monospace', + ), + ), + ), + if (e.byteSize != null) ...[ + const SizedBox(width: 8), + Text( + formatBytes(e.byteSize!), + style: TextStyle( + color: cs.onSurfaceVariant, + fontSize: 11, + fontFamily: 'monospace', + ), + ), + ], + if (hasPayload) + Icon( + expanded ? Symbols.expand_less : Symbols.expand_more, + color: cs.onSurfaceVariant, + size: 18, + ), + ], + ), + if (_meta(e).isNotEmpty) ...[ + const SizedBox(height: 3), + Padding( + padding: const EdgeInsets.only(left: 49), + child: Text( + _meta(e), + style: TextStyle( + color: cs.onSurfaceVariant, + fontSize: 11, + fontFamily: 'monospace', + ), + ), + ), + ], + if (expanded && hasPayload) ...[ + const SizedBox(height: 8), + _payloadBox(context, cs, e), + ], + ], + ), + ), + ), + ), + ); + } + + String _meta(TrafficEntry e) { + if (e.detail != null) return e.detail!; + final parts = []; + if (e.opcode != null) parts.add('op ${e.opcode}'); + if (e.seq != null) parts.add('seq ${e.seq}'); + if (e.cmd != null) parts.add(_cmdLabel(e.cmd!, e.direction)); + return parts.join(' · '); + } + + Widget _payloadBox(BuildContext context, ColorScheme cs, TrafficEntry e) { + final text = e.prettyPayload; + return Container( + width: double.infinity, + decoration: BoxDecoration( + color: cs.surfaceContainerHighest, + borderRadius: BorderRadius.circular(10), + ), + padding: const EdgeInsets.fromLTRB(12, 10, 6, 10), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: SelectableText( + text, + style: TextStyle( + color: cs.onSurface, + fontSize: 12, + fontFamily: 'monospace', + height: 1.35, + ), + ), + ), + IconButton( + tooltip: 'Скопировать', + visualDensity: VisualDensity.compact, + icon: Icon( + Symbols.content_copy, + size: 16, + color: cs.onSurfaceVariant, + ), + onPressed: () async { + await Clipboard.setData(ClipboardData(text: text)); + if (context.mounted) { + showCustomNotification(context, 'Payload скопирован'); + } + }, + ), + ], + ), + ); + } + + Color _accentColor(ColorScheme cs, TrafficDirection d) { + switch (d) { + case TrafficDirection.outgoing: + return cs.primary; + case TrafficDirection.incoming: + return cs.tertiary; + case TrafficDirection.event: + return cs.onSurfaceVariant; + } + } + + IconData _directionIcon(TrafficDirection d) { + switch (d) { + case TrafficDirection.outgoing: + return Symbols.north_east; + case TrafficDirection.incoming: + return Symbols.south_west; + case TrafficDirection.event: + return Symbols.lan; + } + } + + String _cmdLabel(int cmd, TrafficDirection direction) { + switch (cmd) { + case CmdType.ok: + return 'OK'; + case CmdType.notFound: + return 'NOT_FOUND'; + case CmdType.error: + return 'ERROR'; + default: + return direction == TrafficDirection.incoming ? 'PUSH' : 'REQ'; + } + } + + String _formatTime(DateTime t) { + String two(int n) => n.toString().padLeft(2, '0'); + final ms = t.millisecond.toString().padLeft(3, '0'); + return '${two(t.hour)}:${two(t.minute)}:${two(t.second)}.$ms'; + } +} diff --git a/lib/main.dart b/lib/main.dart index 641cda4..839f640 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -52,6 +52,7 @@ import 'frontend/screens/calls/call_screen.dart'; import 'core/push/push_service.dart'; import 'core/storage/app_database.dart'; import 'core/transport/tls_config.dart'; +import 'core/transport/traffic_monitor.dart'; import 'core/transport/vpn_bypass.dart'; import 'core/storage/token_storage.dart'; import 'core/utils/haptics.dart'; @@ -128,6 +129,7 @@ void main() async { final cacheLimitFuture = AppMediaCacheLimit.load(); final digitalIdNativeFuture = AppDigitalIdNative.load(); final showExtraInfoFuture = AppShowExtraInfo.load(); + final trafficCaptureFuture = TrafficMonitor.instance.load(); final packageInfo = await packageInfoFuture; isOnemeFlavor = packageInfo.packageName == 'ru.oneme.app'; @@ -173,6 +175,7 @@ void main() async { AppMediaCacheLimit.current.value = await cacheLimitFuture; AppDigitalIdNative.current.value = await digitalIdNativeFuture; AppShowExtraInfo.current.value = await showExtraInfoFuture; + await trafficCaptureFuture; runApp( KometApp( initialLocale: initialLocale, diff --git a/pubspec.lock b/pubspec.lock index d29aaa7..c2c814e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -885,6 +885,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.18.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" mobile_scanner: dependency: "direct main" description: @@ -1205,6 +1213,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.4" + share_plus: + dependency: "direct main" + description: + name: share_plus + sha256: fce43200aa03ea87b91ce4c3ac79f0cecd52e2a7a56c7a4185023c271fbfa6da + url: "https://pub.dev" + source: hosted + version: "10.1.4" + share_plus_platform_interface: + dependency: transitive + description: + name: share_plus_platform_interface + sha256: cc012a23fc2d479854e6c80150696c4a5f5bb62cb89af4de1c505cf78d0a5d0b + url: "https://pub.dev" + source: hosted + version: "5.0.2" shared_preferences: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 03a36e6..d6a818b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -60,6 +60,7 @@ dependencies: mobile_scanner: ^7.2.0 cached_network_image: ^3.4.1 path_provider: ^2.1.4 + share_plus: ^10.1.4 open_filex: ^4.5.0 url_launcher: ^6.3.1 app_links: ^7.0.0