mirror of
https://github.com/gabehf/Fladder.git
synced 2026-03-08 23:18:16 -07:00
feature: Save logging to cache directory (#196)
Co-authored-by: PartyDonut <PartyDonut@users.noreply.github.com>
This commit is contained in:
parent
ae4707d3a6
commit
1a42be4be0
5 changed files with 143 additions and 62 deletions
|
|
@ -50,8 +50,8 @@ Future<Map<String, dynamic>> loadConfig() async {
|
||||||
}
|
}
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
final crashProvider = CrashLogNotifier();
|
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
final crashProvider = CrashLogNotifier();
|
||||||
|
|
||||||
if (kIsWeb) {
|
if (kIsWeb) {
|
||||||
html.document.onContextMenu.listen((event) => event.preventDefault());
|
html.document.onContextMenu.listen((event) => event.preventDefault());
|
||||||
|
|
|
||||||
90
lib/models/error_log_model.dart
Normal file
90
lib/models/error_log_model.dart
Normal file
|
|
@ -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<String, dynamic> toJson() => {
|
||||||
|
'time': time.toIso8601String(),
|
||||||
|
'level': type.name,
|
||||||
|
'message': message,
|
||||||
|
'stackTrace': stackTrace?.toString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
static ErrorLogModel fromJson(Map<String, dynamic> 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']),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,75 +1,32 @@
|
||||||
import 'package:flutter/foundation.dart';
|
import 'dart:convert';
|
||||||
import 'package:flutter/material.dart';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
|
||||||
enum ErrorType {
|
import 'package:fladder/models/error_log_model.dart';
|
||||||
severe,
|
|
||||||
warning,
|
|
||||||
shout,
|
|
||||||
}
|
|
||||||
|
|
||||||
class ErrorViewModel {
|
final crashLogProvider = StateNotifierProvider<CrashLogNotifier, List<ErrorLogModel>>((ref) => CrashLogNotifier());
|
||||||
final LogRecord rec;
|
|
||||||
|
|
||||||
const ErrorViewModel({required this.rec});
|
class CrashLogNotifier extends StateNotifier<List<ErrorLogModel>> {
|
||||||
|
|
||||||
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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
final crashLogProvider = StateNotifierProvider<CrashLogNotifier, List<ErrorViewModel>>((ref) => CrashLogNotifier());
|
|
||||||
|
|
||||||
class CrashLogNotifier extends StateNotifier<List<ErrorViewModel>> {
|
|
||||||
CrashLogNotifier() : super([]) {
|
CrashLogNotifier() : super([]) {
|
||||||
init();
|
init();
|
||||||
}
|
}
|
||||||
|
|
||||||
late final Logger logger;
|
late final Logger logger;
|
||||||
final maxLength = 100;
|
final maxLength = 50;
|
||||||
|
String? logFilePath;
|
||||||
|
|
||||||
void init() {
|
void init() async {
|
||||||
logger = Logger.root;
|
logger = Logger.root;
|
||||||
logger.level = Level.ALL;
|
logger.level = Level.ALL;
|
||||||
logger.onRecord.listen(logPrint);
|
logger.onRecord.listen(logPrint);
|
||||||
|
|
||||||
FlutterError.onError = (FlutterErrorDetails details) => logFile(details);
|
FlutterError.onError = (FlutterErrorDetails details) => logFile(details);
|
||||||
|
|
||||||
PlatformDispatcher.instance.onError = (error, stack) {
|
PlatformDispatcher.instance.onError = (error, stack) {
|
||||||
logFile(FlutterErrorDetails(
|
logFile(FlutterErrorDetails(
|
||||||
exception: error,
|
exception: error,
|
||||||
|
|
@ -78,10 +35,40 @@ class CrashLogNotifier extends StateNotifier<List<ErrorViewModel>> {
|
||||||
));
|
));
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!kIsWeb) {
|
||||||
|
await _initializeLogFile();
|
||||||
|
await _loadLogsFromFile();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _initializeLogFile() async {
|
||||||
|
final directory = await getApplicationCacheDirectory();
|
||||||
|
logFilePath = '${directory.path}/crash_logs.json';
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadLogsFromFile() async {
|
||||||
|
if (logFilePath == null) return;
|
||||||
|
final file = File(logFilePath!);
|
||||||
|
if (await file.exists()) {
|
||||||
|
final content = await file.readAsString();
|
||||||
|
final List<dynamic> jsonData = jsonDecode(content);
|
||||||
|
state = jsonData.map((json) => ErrorLogModel.fromJson(json)).toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _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() {
|
void clearLogs() {
|
||||||
state = [];
|
state = [];
|
||||||
|
if (!kIsWeb) {
|
||||||
|
_saveLogsToFile();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void logPrint(LogRecord rec) {
|
void logPrint(LogRecord rec) {
|
||||||
|
|
@ -89,16 +76,18 @@ class CrashLogNotifier extends StateNotifier<List<ErrorViewModel>> {
|
||||||
print('${rec.level.name}: ${rec.time}: ${rec.message}');
|
print('${rec.level.name}: ${rec.time}: ${rec.message}');
|
||||||
}
|
}
|
||||||
if (rec.level > Level.INFO) {
|
if (rec.level > Level.INFO) {
|
||||||
state = [ErrorViewModel(rec: rec), ...state];
|
state = [ErrorLogModel.fromLogRecord(rec), ...state];
|
||||||
if (state.length >= maxLength) {
|
if (state.length >= maxLength) {
|
||||||
state = state.sublist(0, maxLength);
|
state = state.sublist(0, maxLength);
|
||||||
}
|
}
|
||||||
|
if (!kIsWeb) {
|
||||||
|
_saveLogsToFile();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void logFile(FlutterErrorDetails details) {
|
void logFile(FlutterErrorDetails details) {
|
||||||
logger.severe('Flutter error: ${details.exception}', details.exception, details.stack);
|
logger.severe('Flutter error: ${details.exception}', details.exception, details.stack);
|
||||||
|
|
||||||
if (details.stack != null && kDebugMode) {
|
if (details.stack != null && kDebugMode) {
|
||||||
print('${details.stack}');
|
print('${details.stack}');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import 'package:flutter/services.dart';
|
||||||
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.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/providers/crash_log_provider.dart';
|
||||||
import 'package:fladder/screens/shared/fladder_snackbar.dart';
|
import 'package:fladder/screens/shared/fladder_snackbar.dart';
|
||||||
import 'package:fladder/util/list_padding.dart';
|
import 'package:fladder/util/list_padding.dart';
|
||||||
|
|
|
||||||
|
|
@ -161,9 +161,10 @@ class _DetailScaffoldState extends ConsumerState<DetailScaffold> {
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.only(
|
padding: EdgeInsets.only(
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
left: MediaQuery.of(context).padding.left,
|
left: MediaQuery.of(context).padding.left,
|
||||||
top: MediaQuery.of(context).padding.top + 50),
|
top: MediaQuery.of(context).padding.top,
|
||||||
|
),
|
||||||
child: ConstrainedBox(
|
child: ConstrainedBox(
|
||||||
constraints: BoxConstraints(
|
constraints: BoxConstraints(
|
||||||
minHeight: MediaQuery.sizeOf(context).height,
|
minHeight: MediaQuery.sizeOf(context).height,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue