From 527981b92c8fd48d143ebe732c8d6eef899ce9e8 Mon Sep 17 00:00:00 2001 From: PartyDonut Date: Thu, 2 Jan 2025 11:33:33 +0100 Subject: [PATCH 1/3] feature: Added persistent crash logs --- lib/main.dart | 2 +- lib/models/error_log_model.dart | 90 ++++++++++++++++++ lib/providers/crash_log_provider.dart | 101 +++++++++------------ lib/screens/crash_screen/crash_screen.dart | 1 + lib/screens/shared/detail_scaffold.dart | 7 +- 5 files changed, 141 insertions(+), 60 deletions(-) create mode 100644 lib/models/error_log_model.dart diff --git a/lib/main.dart b/lib/main.dart index c0daebb..66e46f4 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -50,8 +50,8 @@ Future> loadConfig() async { } void main() async { - final crashProvider = CrashLogNotifier(); WidgetsFlutterBinding.ensureInitialized(); + final crashProvider = CrashLogNotifier(); if (kIsWeb) { html.document.onContextMenu.listen((event) => event.preventDefault()); diff --git a/lib/models/error_log_model.dart b/lib/models/error_log_model.dart new file mode 100644 index 0000000..7c334e0 --- /dev/null +++ b/lib/models/error_log_model.dart @@ -0,0 +1,90 @@ +import 'package:flutter/material.dart'; + +import 'package:collection/collection.dart'; +import 'package:logging/logging.dart'; + +enum ErrorType { + severe, + warning, + shout, +} + +class ErrorLogModel { + final ErrorType type; + final String message; + final DateTime time; + final StackTrace? stackTrace; + + const ErrorLogModel({ + required this.type, + required this.message, + required this.time, + required this.stackTrace, + }); + + factory ErrorLogModel.fromLogRecord(LogRecord record) { + late ErrorType type; + if (record.level == Level.WARNING) { + type = ErrorType.warning; + } else if (record.level == Level.SHOUT) { + type = ErrorType.shout; + } else { + type = ErrorType.severe; + } + + return ErrorLogModel( + type: type, + message: record.message, + time: record.time, + stackTrace: record.stackTrace, + ); + } + + String get label { + var join = _label; + if (join.length > 250) { + return "${join.substring(0, 250)}... \n \nTruncated copy log to see more"; + } else { + return join; + } + } + + String get _label { + return [ + type.name.toUpperCase(), + " | ", + message, + ].join(); + } + + String get content => [ + time.toIso8601String(), + "\n", + "\n", + stackTrace, + ].whereNotNull().join(); + + String get clipBoard => [_label, content].toString(); + + Color get color => switch (type) { + ErrorType.severe => Colors.redAccent, + ErrorType.warning => Colors.orange, + ErrorType.shout => Colors.yellowAccent, + }; + + Map toJson() => { + 'time': time.toIso8601String(), + 'level': type.name, + 'message': message, + 'stackTrace': stackTrace?.toString(), + }; + + static ErrorLogModel fromJson(Map json) { + return ErrorLogModel( + type: ErrorType.values.firstWhereOrNull((level) => level.name == json['level']) ?? ErrorType.warning, + message: json['message'], + stackTrace: json['stackTrace'] != null ? StackTrace.fromString(json['stackTrace']) : null, + time: DateTime.parse(json['time']), + ); + } +} diff --git a/lib/providers/crash_log_provider.dart b/lib/providers/crash_log_provider.dart index f5e917e..a67b009 100644 --- a/lib/providers/crash_log_provider.dart +++ b/lib/providers/crash_log_provider.dart @@ -1,75 +1,32 @@ +import 'dart:convert'; +import 'dart:io'; + import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:collection/collection.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:logging/logging.dart'; +import 'package:path_provider/path_provider.dart'; -enum ErrorType { - severe, - warning, - shout, -} - -class ErrorViewModel { - final LogRecord rec; - - const ErrorViewModel({required this.rec}); - - ErrorType get type { - if (rec.level == Level.WARNING) { - return ErrorType.warning; - } - if (rec.level == Level.SHOUT) { - return ErrorType.shout; - } - return ErrorType.severe; - } - - String get label { - var join = [ - type.name.toUpperCase(), - " | ", - rec.message, - ].join(); - if (join.length > 250) { - return "${join.substring(0, 80)}... \n \nTruncated copy log to see more"; - } else { - return join; - } - } - - String get content => [ - rec.time.toIso8601String(), - "\n", - "\n", - rec.stackTrace, - ].whereNotNull().join(); - - String get clipBoard => [label, content].toString(); - - Color get color => switch (type) { - ErrorType.severe => Colors.redAccent, - ErrorType.warning => Colors.orange, - ErrorType.shout => Colors.yellowAccent, - }; -} +import 'package:fladder/models/error_log_model.dart'; -final crashLogProvider = StateNotifierProvider>((ref) => CrashLogNotifier()); +final crashLogProvider = StateNotifierProvider>((ref) => CrashLogNotifier()); -class CrashLogNotifier extends StateNotifier> { +class CrashLogNotifier extends StateNotifier> { CrashLogNotifier() : super([]) { init(); } late final Logger logger; final maxLength = 100; + String? logFilePath; - void init() { + Future init() async { logger = Logger.root; logger.level = Level.ALL; logger.onRecord.listen(logPrint); + FlutterError.onError = (FlutterErrorDetails details) => logFile(details); + PlatformDispatcher.instance.onError = (error, stack) { logFile(FlutterErrorDetails( exception: error, @@ -78,10 +35,40 @@ class CrashLogNotifier extends StateNotifier> { )); return false; }; + + if (!kIsWeb) { + await _initializeLogFile(); + await _loadLogsFromFile(); + } + } + + Future _initializeLogFile() async { + final directory = await getApplicationCacheDirectory(); + logFilePath = '${directory.path}/crash_logs.json'; + } + + Future _loadLogsFromFile() async { + if (logFilePath == null) return; + final file = File(logFilePath!); + if (await file.exists()) { + final content = await file.readAsString(); + final List jsonData = jsonDecode(content); + state = jsonData.map((json) => ErrorLogModel.fromJson(json)).toList(); + } + } + + Future _saveLogsToFile() async { + if (logFilePath == null) return; + final file = File(logFilePath!); + final jsonData = state.map((log) => log.toJson()).toList(); + await file.writeAsString(jsonEncode(jsonData)); } void clearLogs() { state = []; + if (!kIsWeb) { + _saveLogsToFile(); + } } void logPrint(LogRecord rec) { @@ -89,16 +76,18 @@ class CrashLogNotifier extends StateNotifier> { print('${rec.level.name}: ${rec.time}: ${rec.message}'); } if (rec.level > Level.INFO) { - state = [ErrorViewModel(rec: rec), ...state]; + state = [ErrorLogModel.fromLogRecord(rec), ...state]; if (state.length >= maxLength) { state = state.sublist(0, maxLength); } + if (!kIsWeb) { + _saveLogsToFile(); + } } } void logFile(FlutterErrorDetails details) { logger.severe('Flutter error: ${details.exception}', details.exception, details.stack); - if (details.stack != null && kDebugMode) { print('${details.stack}'); } diff --git a/lib/screens/crash_screen/crash_screen.dart b/lib/screens/crash_screen/crash_screen.dart index a6749b5..88cfcfa 100644 --- a/lib/screens/crash_screen/crash_screen.dart +++ b/lib/screens/crash_screen/crash_screen.dart @@ -4,6 +4,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:fladder/models/error_log_model.dart'; import 'package:fladder/providers/crash_log_provider.dart'; import 'package:fladder/screens/shared/fladder_snackbar.dart'; import 'package:fladder/util/list_padding.dart'; diff --git a/lib/screens/shared/detail_scaffold.dart b/lib/screens/shared/detail_scaffold.dart index 32477c2..fed3e1b 100644 --- a/lib/screens/shared/detail_scaffold.dart +++ b/lib/screens/shared/detail_scaffold.dart @@ -161,9 +161,10 @@ class _DetailScaffoldState extends ConsumerState { ), Padding( padding: EdgeInsets.only( - bottom: 0, - left: MediaQuery.of(context).padding.left, - top: MediaQuery.of(context).padding.top + 50), + bottom: 0, + left: MediaQuery.of(context).padding.left, + top: MediaQuery.of(context).padding.top, + ), child: ConstrainedBox( constraints: BoxConstraints( minHeight: MediaQuery.sizeOf(context).height, From 7d956d1eb3177d385e5eccd54884e7cab4e7a60a Mon Sep 17 00:00:00 2001 From: PartyDonut Date: Thu, 2 Jan 2025 11:43:43 +0100 Subject: [PATCH 2/3] Changed the max limit to 50 --- lib/providers/crash_log_provider.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/providers/crash_log_provider.dart b/lib/providers/crash_log_provider.dart index a67b009..650cc6e 100644 --- a/lib/providers/crash_log_provider.dart +++ b/lib/providers/crash_log_provider.dart @@ -17,7 +17,7 @@ class CrashLogNotifier extends StateNotifier> { } late final Logger logger; - final maxLength = 100; + final maxLength = 50; String? logFilePath; Future init() async { From 29cac5978c0c78b94c7b5a7315647e0ca6919468 Mon Sep 17 00:00:00 2001 From: PartyDonut Date: Thu, 2 Jan 2025 11:52:51 +0100 Subject: [PATCH 3/3] Removed unused Future --- lib/providers/crash_log_provider.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/providers/crash_log_provider.dart b/lib/providers/crash_log_provider.dart index 650cc6e..35cf7a2 100644 --- a/lib/providers/crash_log_provider.dart +++ b/lib/providers/crash_log_provider.dart @@ -20,7 +20,7 @@ class CrashLogNotifier extends StateNotifier> { final maxLength = 50; String? logFilePath; - Future init() async { + void init() async { logger = Logger.root; logger.level = Level.ALL; logger.onRecord.listen(logPrint);