Add a very simple Debug screen

This will show all the debug messages, which include when a file is
being ignored and not loaded, and hopefully all the exceptions. So the
user should be able to better understand why a file has been ignored.

It's not an ideal solution, but it's a quick fix.

Fixes #122
This commit is contained in:
Vishesh Handa
2020-05-06 01:06:12 +02:00
parent 63e7d0e927
commit ea733aacb5
5 changed files with 137 additions and 14 deletions

View File

@ -18,3 +18,5 @@ settings:
analytics: Analytics analytics: Analytics
crashReports: Collect Anonymous Crash Reports crashReports: Collect Anonymous Crash Reports
usageStats: Collect Anonymous Usage Statistics usageStats: Collect Anonymous Usage Statistics
debug: Debug App
debugLog: Look under the hood

View File

@ -32,7 +32,7 @@ import 'setup/screens.dart';
class JournalApp extends StatelessWidget { class JournalApp extends StatelessWidget {
static Future main(SharedPreferences pref) async { static Future main(SharedPreferences pref) async {
Log.init(); await Log.init();
var appState = AppState(pref); var appState = AppState(pref);
appState.dumpToLog(); appState.dumpToLog();

View File

@ -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<DebugScreen> {
@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: <Widget>[
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;
}
}

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:gitjournal/core/notes_folder_fs.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/screens/settings_editors.dart';
import 'package:gitjournal/settings.dart'; import 'package:gitjournal/settings.dart';
import 'package:gitjournal/state_container.dart'; import 'package:gitjournal/state_container.dart';
@ -226,6 +227,16 @@ class SettingsListState extends State<SettingsList> {
}, },
), ),
VersionNumberTile(), VersionNumberTile(),
ListTile(
title: Text(tr('settings.debug')),
subtitle: Text(tr('settings.debugLog')),
onTap: () {
var route = MaterialPageRoute(
builder: (context) => DebugScreen(),
);
Navigator.of(context).push(route);
},
),
]); ]);
} }
} }

View File

@ -2,6 +2,7 @@ import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:fimber/fimber.dart'; import 'package:fimber/fimber.dart';
import 'package:meta/meta.dart';
import 'package:flutter/foundation.dart' as foundation; import 'package:flutter/foundation.dart' as foundation;
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
@ -10,7 +11,7 @@ class Log {
static String logFolderPath; static String logFolderPath;
static RandomAccessFile logFile; static RandomAccessFile logFile;
static void init() async { static Future<void> init() async {
if (foundation.kDebugMode) { if (foundation.kDebugMode) {
Fimber.plantTree(DebugTree.elapsed(useColors: true)); Fimber.plantTree(DebugTree.elapsed(useColors: true));
} else { } else {
@ -25,7 +26,7 @@ class Log {
// Ignore if it already exists // Ignore if it already exists
} }
setLogCapture(true); await setLogCapture(true);
} }
static void v(String msg, {dynamic ex, StackTrace stacktrace}) { static void v(String msg, {dynamic ex, StackTrace stacktrace}) {
@ -59,20 +60,25 @@ class Log {
dynamic ex, dynamic ex,
StackTrace stackTrace, StackTrace stackTrace,
) { ) {
if (logFile == null) return; if (logFile == null) {
var map = <String, dynamic>{ return;
't': DateTime.now().millisecondsSinceEpoch, }
'l': level,
'msg': msg.replaceAll('\n', ' '), var logMsg = LogMessage(
if (ex != null) 'ex': ex.toString().replaceAll('\n', ' '), t: DateTime.now().millisecondsSinceEpoch,
if (stackTrace != null) l: level,
'stack': stackTrace.toString().replaceAll('\n', ' '), msg: msg.replaceAll('\n', ' '),
}; ex: ex != null ? ex.toString().replaceAll('\n', ' ') : null,
var str = json.encode(map); stack: stackTrace != null
? stackTrace.toString().replaceAll('\n', ' ')
: null,
);
var str = json.encode(logMsg.toMap());
logFile.writeStringSync(str + '\n'); logFile.writeStringSync(str + '\n');
} }
static void setLogCapture(bool state) async { static Future<void> setLogCapture(bool state) async {
if (state) { if (state) {
var today = DateTime.now().toString().substring(0, 10); var today = DateTime.now().toString().substring(0, 10);
var logFilePath = p.join(logFolderPath, '$today.jsonl'); var logFilePath = p.join(logFolderPath, '$today.jsonl');
@ -85,4 +91,53 @@ class Log {
logFile = null; logFile = null;
} }
} }
static Iterable<LogMessage> fetchLogs() sync* {
var today = DateTime.now().toString().substring(0, 10);
for (var msg in fetchLogsForDate(today)) {
yield msg;
}
}
static Iterable<LogMessage> 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<String, dynamic> toMap() {
return <String, dynamic>{
't': t,
'l': l,
'msg': msg,
if (ex != null && ex.isNotEmpty) 'ex': ex,
if (stack != null && stack.isNotEmpty) 'stack': stack,
};
}
LogMessage.fromMap(Map<String, dynamic> map) {
t = map['t'];
l = map['l'];
msg = map['msg'];
ex = map['ex'];
stack = map['stack'];
}
} }