feat(debug): диплинк komet.pw/export-logs и zip-экспорт логов за 24 часа
This commit is contained in:
@@ -64,6 +64,15 @@
|
|||||||
<data android:scheme="http" android:host="max.ru"/>
|
<data android:scheme="http" android:host="max.ru"/>
|
||||||
<data android:scheme="http" android:host="www.max.ru"/>
|
<data android:scheme="http" android:host="www.max.ru"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
<intent-filter android:autoVerify="true">
|
||||||
|
<action android:name="android.intent.action.VIEW"/>
|
||||||
|
<category android:name="android.intent.category.DEFAULT"/>
|
||||||
|
<category android:name="android.intent.category.BROWSABLE"/>
|
||||||
|
<data android:scheme="https" android:host="komet.pw"/>
|
||||||
|
<data android:scheme="https" android:host="www.komet.pw"/>
|
||||||
|
<data android:path="/export-logs"/>
|
||||||
|
<data android:path="/export-logs/"/>
|
||||||
|
</intent-filter>
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW"/>
|
<action android:name="android.intent.action.VIEW"/>
|
||||||
<category android:name="android.intent.category.DEFAULT"/>
|
<category android:name="android.intent.category.DEFAULT"/>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'dart:async';
|
|||||||
import 'package:app_links/app_links.dart';
|
import 'package:app_links/app_links.dart';
|
||||||
|
|
||||||
import '../../backend/api.dart';
|
import '../../backend/api.dart';
|
||||||
|
import '../../frontend/debug/log_export.dart';
|
||||||
import '../../frontend/widgets/max_link_handler.dart';
|
import '../../frontend/widgets/max_link_handler.dart';
|
||||||
import '../../main.dart';
|
import '../../main.dart';
|
||||||
import 'desktop_url_scheme.dart';
|
import 'desktop_url_scheme.dart';
|
||||||
@@ -16,6 +17,8 @@ class DeepLinkService {
|
|||||||
StreamSubscription<Uri>? _sub;
|
StreamSubscription<Uri>? _sub;
|
||||||
StreamSubscription<SessionState>? _stateSub;
|
StreamSubscription<SessionState>? _stateSub;
|
||||||
String? _pending;
|
String? _pending;
|
||||||
|
bool _pendingLogExport = false;
|
||||||
|
Timer? _logExportRetry;
|
||||||
bool _ready = false;
|
bool _ready = false;
|
||||||
bool _started = false;
|
bool _started = false;
|
||||||
|
|
||||||
@@ -42,6 +45,11 @@ class DeepLinkService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _onUri(Uri uri) {
|
void _onUri(Uri uri) {
|
||||||
|
if (_isLogExportLink(uri)) {
|
||||||
|
_pendingLogExport = true;
|
||||||
|
_flushPending();
|
||||||
|
return;
|
||||||
|
}
|
||||||
final url = _normalize(uri);
|
final url = _normalize(uri);
|
||||||
if (url == null) return;
|
if (url == null) return;
|
||||||
_pending = url;
|
_pending = url;
|
||||||
@@ -49,16 +57,46 @@ class DeepLinkService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _flushPending() {
|
void _flushPending() {
|
||||||
final pending = _pending;
|
|
||||||
if (pending == null || !_ready) return;
|
|
||||||
if (api.state != SessionState.online) return;
|
|
||||||
final context = KometApp.navigatorKey.currentContext;
|
final context = KometApp.navigatorKey.currentContext;
|
||||||
if (context == null) return;
|
|
||||||
|
|
||||||
|
if (_pendingLogExport) {
|
||||||
|
if (context == null) {
|
||||||
|
_logExportRetry ??= Timer(const Duration(milliseconds: 300), () {
|
||||||
|
_logExportRetry = null;
|
||||||
|
_flushPending();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
_pendingLogExport = false;
|
||||||
|
exportDebugLog(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_ready || context == null) return;
|
||||||
|
final pending = _pending;
|
||||||
|
if (pending == null || api.state != SessionState.online) return;
|
||||||
_pending = null;
|
_pending = null;
|
||||||
tryHandleMaxLink(context, pending);
|
tryHandleMaxLink(context, pending);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool _isLogExportLink(Uri uri) {
|
||||||
|
final scheme = uri.scheme.toLowerCase();
|
||||||
|
final host = uri.host.toLowerCase();
|
||||||
|
final segments = <String>[
|
||||||
|
if (scheme == 'komet' && host.isNotEmpty) host,
|
||||||
|
...uri.pathSegments,
|
||||||
|
].where((s) => s.isNotEmpty).toList();
|
||||||
|
|
||||||
|
if (scheme == 'komet') {
|
||||||
|
return segments.length == 1 && segments.first == 'export-logs';
|
||||||
|
}
|
||||||
|
if (scheme == 'https' || scheme == 'http') {
|
||||||
|
return (host == 'komet.pw' || host == 'www.komet.pw') &&
|
||||||
|
segments.length == 1 &&
|
||||||
|
segments.first == 'export-logs';
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
String? _normalize(Uri uri) {
|
String? _normalize(Uri uri) {
|
||||||
final scheme = uri.scheme.toLowerCase();
|
final scheme = uri.scheme.toLowerCase();
|
||||||
|
|
||||||
@@ -82,6 +120,8 @@ class DeepLinkService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
_logExportRetry?.cancel();
|
||||||
|
_logExportRetry = null;
|
||||||
_sub?.cancel();
|
_sub?.cancel();
|
||||||
_sub = null;
|
_sub = null;
|
||||||
_stateSub?.cancel();
|
_stateSub?.cancel();
|
||||||
|
|||||||
@@ -5,8 +5,16 @@ import 'dart:io';
|
|||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
|
||||||
import '../protocol/opcode_map.dart';
|
import '../protocol/opcode_map.dart';
|
||||||
|
import 'format.dart';
|
||||||
import 'log_redact.dart';
|
import 'log_redact.dart';
|
||||||
|
|
||||||
|
class DebugExportFile {
|
||||||
|
final String name;
|
||||||
|
final String content;
|
||||||
|
|
||||||
|
DebugExportFile(this.name, this.content);
|
||||||
|
}
|
||||||
|
|
||||||
class _LogEntry {
|
class _LogEntry {
|
||||||
final int opcode;
|
final int opcode;
|
||||||
final int seq;
|
final int seq;
|
||||||
@@ -99,7 +107,8 @@ class DebugSessionLog {
|
|||||||
DebugSessionLog._();
|
DebugSessionLog._();
|
||||||
static final DebugSessionLog instance = DebugSessionLog._();
|
static final DebugSessionLog instance = DebugSessionLog._();
|
||||||
|
|
||||||
static const int _maxSessions = 3;
|
static const Duration _retention = Duration(hours: 24);
|
||||||
|
static const int _maxStoredSessions = 30;
|
||||||
static const int _maxEntriesPerSession = 2000;
|
static const int _maxEntriesPerSession = 2000;
|
||||||
static const int _maxLogLinesPerSession = 5000;
|
static const int _maxLogLinesPerSession = 5000;
|
||||||
static const Duration _flushDebounce = Duration(seconds: 3);
|
static const Duration _flushDebounce = Duration(seconds: 3);
|
||||||
@@ -225,9 +234,17 @@ class DebugSessionLog {
|
|||||||
|
|
||||||
Future<void> _rotate() async {
|
Future<void> _rotate() async {
|
||||||
final files = await _sessionFiles();
|
final files = await _sessionFiles();
|
||||||
const keep = _maxSessions - 1;
|
final cutoff = DateTime.now().subtract(_retention).millisecondsSinceEpoch;
|
||||||
if (files.length <= keep) return;
|
final stale = <File>[];
|
||||||
for (final file in files.take(files.length - keep)) {
|
final fresh = <File>[];
|
||||||
|
for (final file in files) {
|
||||||
|
(_startMillis(file) < cutoff ? stale : fresh).add(file);
|
||||||
|
}
|
||||||
|
const keep = _maxStoredSessions - 1;
|
||||||
|
if (fresh.length > keep) {
|
||||||
|
stale.addAll(fresh.take(fresh.length - keep));
|
||||||
|
}
|
||||||
|
for (final file in stale) {
|
||||||
try {
|
try {
|
||||||
await file.delete();
|
await file.delete();
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
@@ -255,7 +272,8 @@ class DebugSessionLog {
|
|||||||
return int.tryParse(digits) ?? 0;
|
return int.tryParse(digits) ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String?> buildExport({String? endpoint}) async {
|
Future<List<DebugExportFile>?> buildExportFiles({String? endpoint}) async {
|
||||||
|
final cutoff = DateTime.now().subtract(_retention);
|
||||||
final sessions = <_SessionData>[];
|
final sessions = <_SessionData>[];
|
||||||
final dir = _dir;
|
final dir = _dir;
|
||||||
if (dir != null) {
|
if (dir != null) {
|
||||||
@@ -263,7 +281,10 @@ class DebugSessionLog {
|
|||||||
if (_currentFile != null && file.path == _currentFile!.path) continue;
|
if (_currentFile != null && file.path == _currentFile!.path) continue;
|
||||||
try {
|
try {
|
||||||
final decoded = jsonDecode(await file.readAsString());
|
final decoded = jsonDecode(await file.readAsString());
|
||||||
if (decoded is Map) sessions.add(_SessionData.fromJson(decoded));
|
if (decoded is Map) {
|
||||||
|
final session = _SessionData.fromJson(decoded);
|
||||||
|
if (!session.startedAt.isBefore(cutoff)) sessions.add(session);
|
||||||
|
}
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -277,56 +298,66 @@ class DebugSessionLog {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
sessions.sort((a, b) => a.startedAt.compareTo(b.startedAt));
|
sessions.sort((a, b) => a.startedAt.compareTo(b.startedAt));
|
||||||
final lastN = sessions.length > _maxSessions
|
final totalEntries = sessions.fold<int>(
|
||||||
? sessions.sublist(sessions.length - _maxSessions)
|
0,
|
||||||
: sessions;
|
(sum, s) => sum + s.entries.length,
|
||||||
final totalEntries = lastN.fold<int>(0, (sum, s) => sum + s.entries.length);
|
);
|
||||||
final totalLogs = lastN.fold<int>(0, (sum, s) => sum + s.logLines.length);
|
final totalLogs = sessions.fold<int>(
|
||||||
|
0,
|
||||||
|
(sum, s) => sum + s.logLines.length,
|
||||||
|
);
|
||||||
if (totalEntries == 0 && totalLogs == 0) return null;
|
if (totalEntries == 0 && totalLogs == 0) return null;
|
||||||
|
|
||||||
|
final info = StringBuffer();
|
||||||
|
info.writeln('Komet — отладочный лог');
|
||||||
|
if (endpoint != null) info.writeln('Сервер: $endpoint');
|
||||||
|
info.writeln('Экспортирован: ${DateTime.now().toIso8601String()}');
|
||||||
|
info.writeln('Период: последние ${_retention.inHours} часа');
|
||||||
|
info.writeln('Заходов в приложение: ${sessions.length}');
|
||||||
|
info.writeln('Всего запросов: $totalEntries');
|
||||||
|
info.writeln('Всего строк лога: $totalLogs');
|
||||||
|
info.writeln('Скрыто: токен полностью, номер кроме первых 3 символов');
|
||||||
|
|
||||||
|
final files = <DebugExportFile>[DebugExportFile('info.txt', '$info')];
|
||||||
|
for (var s = 0; s < sessions.length; s++) {
|
||||||
|
final session = sessions[s];
|
||||||
|
final name =
|
||||||
|
'session_${(s + 1).toString().padLeft(2, '0')}_'
|
||||||
|
'${formatFileStamp(session.startedAt)}.txt';
|
||||||
|
files.add(DebugExportFile(name, _buildSessionText(s + 1, session)));
|
||||||
|
}
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _buildSessionText(int index, _SessionData session) {
|
||||||
final buffer = StringBuffer();
|
final buffer = StringBuffer();
|
||||||
buffer.writeln('Komet — отладочный лог');
|
buffer.writeln('==================================================');
|
||||||
if (endpoint != null) buffer.writeln('Сервер: $endpoint');
|
buffer.writeln('ЗАХОД #$index — ${session.startedAt.toIso8601String()}');
|
||||||
buffer.writeln('Экспортирован: ${DateTime.now().toIso8601String()}');
|
buffer.writeln(
|
||||||
buffer.writeln('Заходов в приложение: ${lastN.length}');
|
'запросов: ${session.entries.length}'
|
||||||
buffer.writeln('Всего запросов: $totalEntries');
|
'${session.truncated ? ' (обрезано до $_maxEntriesPerSession)' : ''}'
|
||||||
buffer.writeln('Всего строк лога: $totalLogs');
|
' · строк лога: ${session.logLines.length}'
|
||||||
buffer.writeln('Скрыто: токен полностью, номер кроме первых 3 символов');
|
'${session.logsTruncated ? ' (обрезано до $_maxLogLinesPerSession)' : ''}',
|
||||||
|
);
|
||||||
|
buffer.writeln('==================================================');
|
||||||
buffer.writeln();
|
buffer.writeln();
|
||||||
|
|
||||||
for (var s = 0; s < lastN.length; s++) {
|
buffer.writeln('----- ЛОГИ ПРИЛОЖЕНИЯ -----');
|
||||||
final session = lastN[s];
|
if (session.logLines.isEmpty) {
|
||||||
buffer.writeln('==================================================');
|
buffer.writeln('(пусто)');
|
||||||
buffer.writeln(
|
} else {
|
||||||
'ЗАХОД #${s + 1} — ${session.startedAt.toIso8601String()}',
|
for (final line in session.logLines) {
|
||||||
);
|
buffer.writeln(line);
|
||||||
buffer.writeln(
|
|
||||||
'запросов: ${session.entries.length}'
|
|
||||||
'${session.truncated ? ' (обрезано до $_maxEntriesPerSession)' : ''}'
|
|
||||||
' · строк лога: ${session.logLines.length}'
|
|
||||||
'${session.logsTruncated ? ' (обрезано до $_maxLogLinesPerSession)' : ''}',
|
|
||||||
);
|
|
||||||
buffer.writeln('==================================================');
|
|
||||||
buffer.writeln();
|
|
||||||
|
|
||||||
buffer.writeln('----- ЛОГИ ПРИЛОЖЕНИЯ -----');
|
|
||||||
if (session.logLines.isEmpty) {
|
|
||||||
buffer.writeln('(пусто)');
|
|
||||||
} else {
|
|
||||||
for (final line in session.logLines) {
|
|
||||||
buffer.writeln(line);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
buffer.writeln();
|
}
|
||||||
|
buffer.writeln();
|
||||||
|
|
||||||
buffer.writeln('----- ЗАПРОСЫ -----');
|
buffer.writeln('----- ЗАПРОСЫ -----');
|
||||||
if (session.entries.isEmpty) {
|
if (session.entries.isEmpty) {
|
||||||
buffer.writeln('(пусто)');
|
buffer.writeln('(пусто)');
|
||||||
buffer.writeln();
|
} else {
|
||||||
} else {
|
for (var i = 0; i < session.entries.length; i++) {
|
||||||
for (var i = 0; i < session.entries.length; i++) {
|
_writeEntry(buffer, i + 1, session.entries[i]);
|
||||||
_writeEntry(buffer, i + 1, session.entries[i]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return buffer.toString();
|
return buffer.toString();
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:archive/archive.dart';
|
||||||
|
import 'package:file_picker/file_picker.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
import '../../core/transport/traffic_monitor.dart';
|
||||||
|
import '../../core/utils/debug_session_log.dart';
|
||||||
|
import '../../core/utils/format.dart';
|
||||||
|
import '../widgets/custom_notification.dart';
|
||||||
|
|
||||||
|
Future<void> exportDebugLog(BuildContext context) async {
|
||||||
|
final exportFiles = await DebugSessionLog.instance.buildExportFiles(
|
||||||
|
endpoint: TrafficMonitor.instance.activeEndpoint,
|
||||||
|
);
|
||||||
|
if (exportFiles == null) {
|
||||||
|
if (context.mounted) showCustomNotification(context, 'Лог пуст');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final archive = Archive();
|
||||||
|
for (final file in exportFiles) {
|
||||||
|
final data = utf8.encode(file.content);
|
||||||
|
archive.addFile(ArchiveFile(file.name, data.length, data));
|
||||||
|
}
|
||||||
|
final bytes = ZipEncoder().encodeBytes(archive);
|
||||||
|
final fileName = 'komet_debug_${formatFileStamp(DateTime.now())}.zip';
|
||||||
|
final isMobile = Platform.isAndroid || Platform.isIOS;
|
||||||
|
try {
|
||||||
|
final path = await FilePicker.platform.saveFile(
|
||||||
|
dialogTitle: 'Сохранить отладочный лог',
|
||||||
|
fileName: fileName,
|
||||||
|
type: FileType.any,
|
||||||
|
bytes: isMobile ? bytes : null,
|
||||||
|
);
|
||||||
|
if (path == null) return;
|
||||||
|
if (!isMobile) {
|
||||||
|
await File(path).writeAsBytes(bytes);
|
||||||
|
}
|
||||||
|
if (context.mounted) {
|
||||||
|
showCustomNotification(context, 'Лог сохранён: $path');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (context.mounted) {
|
||||||
|
showCustomNotification(context, 'Не удалось сохранить лог: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -49,7 +49,7 @@ class DebugQuickActionsSection extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 2),
|
const SizedBox(height: 2),
|
||||||
Text(
|
Text(
|
||||||
'Все логи и запросы за последние 3 захода в приложение',
|
'Zip-архив: логи и запросы за последние 24 часа, каждый заход отдельным файлом',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: cs.onSurfaceVariant,
|
color: cs.onSurfaceVariant,
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
|
|||||||
@@ -1,8 +1,3 @@
|
|||||||
import 'dart:convert';
|
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:file_picker/file_picker.dart';
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import '../../../backend/modules/chats.dart';
|
import '../../../backend/modules/chats.dart';
|
||||||
@@ -10,8 +5,6 @@ import '../../../core/calls/call_controller.dart';
|
|||||||
import '../../../core/config/app_media_cache.dart';
|
import '../../../core/config/app_media_cache.dart';
|
||||||
import '../../../core/protocol/opcode_map.dart';
|
import '../../../core/protocol/opcode_map.dart';
|
||||||
import '../../../core/protocol/packet.dart';
|
import '../../../core/protocol/packet.dart';
|
||||||
import '../../../core/transport/traffic_monitor.dart';
|
|
||||||
import '../../../core/utils/debug_session_log.dart';
|
|
||||||
import '../../../core/utils/format.dart';
|
import '../../../core/utils/format.dart';
|
||||||
import '../../../core/utils/logger.dart';
|
import '../../../core/utils/logger.dart';
|
||||||
import '../../../core/utils/media_cache.dart';
|
import '../../../core/utils/media_cache.dart';
|
||||||
@@ -20,6 +13,7 @@ import '../../debug/cache_section.dart';
|
|||||||
import '../../debug/feature_toggles_section.dart';
|
import '../../debug/feature_toggles_section.dart';
|
||||||
import '../../debug/header_section.dart';
|
import '../../debug/header_section.dart';
|
||||||
import '../../debug/id_search_section.dart';
|
import '../../debug/id_search_section.dart';
|
||||||
|
import '../../debug/log_export.dart';
|
||||||
import '../../debug/network_section.dart';
|
import '../../debug/network_section.dart';
|
||||||
import '../../debug/previews_section.dart';
|
import '../../debug/previews_section.dart';
|
||||||
import '../../debug/quick_actions_section.dart';
|
import '../../debug/quick_actions_section.dart';
|
||||||
@@ -68,38 +62,6 @@ class _DebugMenuScreenState extends State<DebugMenuScreen> {
|
|||||||
if (mounted) setState(() => _cacheSize = size);
|
if (mounted) setState(() => _cacheSize = size);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _exportDebugLog() async {
|
|
||||||
final content = await DebugSessionLog.instance.buildExport(
|
|
||||||
endpoint: TrafficMonitor.instance.activeEndpoint,
|
|
||||||
);
|
|
||||||
if (content == null) {
|
|
||||||
if (mounted) showCustomNotification(context, 'Лог пуст');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final bytes = Uint8List.fromList(utf8.encode(content));
|
|
||||||
final fileName = 'komet_debug_${formatFileStamp(DateTime.now())}.txt';
|
|
||||||
final isMobile = Platform.isAndroid || Platform.isIOS;
|
|
||||||
try {
|
|
||||||
final path = await FilePicker.platform.saveFile(
|
|
||||||
dialogTitle: 'Сохранить отладочный лог',
|
|
||||||
fileName: fileName,
|
|
||||||
type: FileType.any,
|
|
||||||
bytes: isMobile ? bytes : null,
|
|
||||||
);
|
|
||||||
if (path == null) return;
|
|
||||||
if (!isMobile) {
|
|
||||||
await File(path).writeAsBytes(bytes);
|
|
||||||
}
|
|
||||||
if (mounted) {
|
|
||||||
showCustomNotification(context, 'Лог сохранён: $path');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
if (mounted) {
|
|
||||||
showCustomNotification(context, 'Не удалось сохранить лог: $e');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _clearCache() async {
|
Future<void> _clearCache() async {
|
||||||
if (_clearingCache) return;
|
if (_clearingCache) return;
|
||||||
setState(() => _clearingCache = true);
|
setState(() => _clearingCache = true);
|
||||||
@@ -255,7 +217,9 @@ class _DebugMenuScreenState extends State<DebugMenuScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: DebugQuickActionsSection(onExportLog: _exportDebugLog),
|
child: DebugQuickActionsSection(
|
||||||
|
onExportLog: () => exportDebugLog(context),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
SliverToBoxAdapter(child: DebugNetworkSection(appState: appState)),
|
SliverToBoxAdapter(child: DebugNetworkSection(appState: appState)),
|
||||||
const SliverToBoxAdapter(child: DebugFeatureTogglesSection()),
|
const SliverToBoxAdapter(child: DebugFeatureTogglesSection()),
|
||||||
|
|||||||
+1
-1
@@ -50,7 +50,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.4"
|
version: "1.0.4"
|
||||||
archive:
|
archive:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: archive
|
name: archive
|
||||||
sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff
|
sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ dependencies:
|
|||||||
flutter_timezone: ^5.0.1
|
flutter_timezone: ^5.0.1
|
||||||
timezone: ^0.11.0
|
timezone: ^0.11.0
|
||||||
file_picker: ^8.0.0
|
file_picker: ^8.0.0
|
||||||
|
archive: ^4.0.9
|
||||||
geolocator: ^13.0.0
|
geolocator: ^13.0.0
|
||||||
photo_manager: ^3.0.0
|
photo_manager: ^3.0.0
|
||||||
image: ^4.3.0
|
image: ^4.3.0
|
||||||
|
|||||||
Reference in New Issue
Block a user