diff --git a/assets/langs/en.yaml b/assets/langs/en.yaml index 42f78682..fa973864 100644 --- a/assets/langs/en.yaml +++ b/assets/langs/en.yaml @@ -18,3 +18,5 @@ settings: analytics: Analytics crashReports: Collect Anonymous Crash Reports usageStats: Collect Anonymous Usage Statistics + debug: Debug App + debugLog: Look under the hood diff --git a/lib/app.dart b/lib/app.dart index 6307429a..93cbeea1 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -32,7 +32,7 @@ import 'setup/screens.dart'; class JournalApp extends StatelessWidget { static Future main(SharedPreferences pref) async { - Log.init(); + await Log.init(); var appState = AppState(pref); appState.dumpToLog(); diff --git a/lib/screens/debug_screen.dart b/lib/screens/debug_screen.dart new file mode 100644 index 00000000..78fed4c1 --- /dev/null +++ b/lib/screens/debug_screen.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:gitjournal/utils/logger.dart'; + +class DebugScreen extends StatefulWidget { + @override + _DebugScreenState createState() => _DebugScreenState(); +} + +class _DebugScreenState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(tr('settings.debug')), + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ), + body: ListView( + children: [ + for (var msg in Log.fetchLogs()) _buildLogWidget(msg), + ], + ), + ); + } + + Widget _buildLogWidget(LogMessage msg) { + var textStyle = Theme.of(context).textTheme.subhead; + textStyle = textStyle.copyWith(color: _colorForLevel(msg.l)); + + var str = DateTime.fromMillisecondsSinceEpoch(msg.t).toIso8601String() + + ' ' + + msg.msg; + + if (msg.ex != null) { + str += ' ' + msg.ex; + } + if (msg.stack != null) { + str += ' ' + msg.stack; + } + return Text(str, style: textStyle); + } + + Color _colorForLevel(String l) { + switch (l) { + case 'e': + return Colors.red; + } + return Colors.black; + } +} diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index daf45ed4..b4eb29dd 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:gitjournal/core/notes_folder_fs.dart'; +import 'package:gitjournal/screens/debug_screen.dart'; import 'package:gitjournal/screens/settings_editors.dart'; import 'package:gitjournal/settings.dart'; import 'package:gitjournal/state_container.dart'; @@ -226,6 +227,16 @@ class SettingsListState extends State { }, ), VersionNumberTile(), + ListTile( + title: Text(tr('settings.debug')), + subtitle: Text(tr('settings.debugLog')), + onTap: () { + var route = MaterialPageRoute( + builder: (context) => DebugScreen(), + ); + Navigator.of(context).push(route); + }, + ), ]); } } diff --git a/lib/utils/logger.dart b/lib/utils/logger.dart index e0081bbf..d3625b3a 100644 --- a/lib/utils/logger.dart +++ b/lib/utils/logger.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:fimber/fimber.dart'; +import 'package:meta/meta.dart'; import 'package:flutter/foundation.dart' as foundation; import 'package:path_provider/path_provider.dart'; import 'package:path/path.dart' as p; @@ -10,7 +11,7 @@ class Log { static String logFolderPath; static RandomAccessFile logFile; - static void init() async { + static Future init() async { if (foundation.kDebugMode) { Fimber.plantTree(DebugTree.elapsed(useColors: true)); } else { @@ -25,7 +26,7 @@ class Log { // Ignore if it already exists } - setLogCapture(true); + await setLogCapture(true); } static void v(String msg, {dynamic ex, StackTrace stacktrace}) { @@ -59,20 +60,25 @@ class Log { dynamic ex, StackTrace stackTrace, ) { - if (logFile == null) return; - var map = { - 't': DateTime.now().millisecondsSinceEpoch, - 'l': level, - 'msg': msg.replaceAll('\n', ' '), - if (ex != null) 'ex': ex.toString().replaceAll('\n', ' '), - if (stackTrace != null) - 'stack': stackTrace.toString().replaceAll('\n', ' '), - }; - var str = json.encode(map); + if (logFile == null) { + return; + } + + var logMsg = LogMessage( + t: DateTime.now().millisecondsSinceEpoch, + l: level, + msg: msg.replaceAll('\n', ' '), + ex: ex != null ? ex.toString().replaceAll('\n', ' ') : null, + stack: stackTrace != null + ? stackTrace.toString().replaceAll('\n', ' ') + : null, + ); + + var str = json.encode(logMsg.toMap()); logFile.writeStringSync(str + '\n'); } - static void setLogCapture(bool state) async { + static Future setLogCapture(bool state) async { if (state) { var today = DateTime.now().toString().substring(0, 10); var logFilePath = p.join(logFolderPath, '$today.jsonl'); @@ -85,4 +91,53 @@ class Log { logFile = null; } } + + static Iterable fetchLogs() sync* { + var today = DateTime.now().toString().substring(0, 10); + for (var msg in fetchLogsForDate(today)) { + yield msg; + } + } + + static Iterable fetchLogsForDate(String date) sync* { + var file = File(p.join(logFolderPath, '$date.jsonl')); + var str = file.readAsStringSync(); + for (var line in LineSplitter.split(str)) { + yield LogMessage.fromMap(json.decode(line)); + } + } +} + +class LogMessage { + int t; + String l; + String msg; + String ex; + String stack; + + LogMessage({ + @required this.t, + @required this.l, + @required this.msg, + this.ex, + this.stack, + }); + + Map toMap() { + return { + 't': t, + 'l': l, + 'msg': msg, + if (ex != null && ex.isNotEmpty) 'ex': ex, + if (stack != null && stack.isNotEmpty) 'stack': stack, + }; + } + + LogMessage.fromMap(Map map) { + t = map['t']; + l = map['l']; + msg = map['msg']; + ex = map['ex']; + stack = map['stack']; + } }