mirror of
https://github.com/GitJournal/GitJournal.git
synced 2025-08-24 01:08:09 +08:00

Instead of the legacy Provider.of. Less scope of bugs this way and the code is so much nicer to read.
402 lines
10 KiB
Dart
402 lines
10 KiB
Dart
/*
|
|
* SPDX-FileCopyrightText: 2019-2021 Vishesh Handa <me@vhanda.in>
|
|
*
|
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
*/
|
|
|
|
import 'dart:convert';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:gitjournal/l10n.dart';
|
|
import 'package:gitjournal/logger/logger.dart';
|
|
import 'package:gitjournal/settings/app_config.dart';
|
|
import 'package:gitjournal/utils/utils.dart';
|
|
import 'package:provider/provider.dart';
|
|
import 'package:time/time.dart';
|
|
|
|
class DebugScreen extends StatefulWidget {
|
|
static const routePath = '/settings/debug';
|
|
|
|
const DebugScreen({super.key});
|
|
|
|
@override
|
|
_DebugScreenState createState() => _DebugScreenState();
|
|
}
|
|
|
|
class _DebugScreenState extends State<DebugScreen> {
|
|
final ScrollController _controller = ScrollController();
|
|
|
|
late List<LogMessage> _logs;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
|
|
_logs = Log.fetchLogs().toList();
|
|
WidgetsBinding.instance.addPostFrameCallback((_) => _scrollToBottom());
|
|
}
|
|
|
|
void _scrollToTop() {
|
|
_controller.animateTo(
|
|
_controller.position.minScrollExtent,
|
|
duration: 10.milliseconds,
|
|
curve: Curves.easeOut,
|
|
);
|
|
}
|
|
|
|
void _scrollToBottom() {
|
|
_controller.animateTo(
|
|
_controller.position.maxScrollExtent,
|
|
duration: 10.milliseconds,
|
|
curve: Curves.easeOut,
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: Text(context.loc.settingsDebugTitle),
|
|
leading: IconButton(
|
|
icon: const Icon(Icons.arrow_back),
|
|
onPressed: () {
|
|
Navigator.of(context).pop();
|
|
},
|
|
),
|
|
actions: <Widget>[
|
|
IconButton(
|
|
icon: const Icon(Icons.copy),
|
|
onPressed: _copyToClipboard,
|
|
),
|
|
IconButton(
|
|
icon: const Icon(Icons.filter_list),
|
|
onPressed: _showFilterSelection,
|
|
),
|
|
IconButton(
|
|
icon: const Icon(Icons.arrow_upward),
|
|
onPressed: _scrollToTop,
|
|
),
|
|
IconButton(
|
|
icon: const Icon(Icons.arrow_downward),
|
|
onPressed: _scrollToBottom,
|
|
),
|
|
],
|
|
),
|
|
body: Scrollbar(
|
|
child: SingleChildScrollView(
|
|
scrollDirection: Axis.horizontal,
|
|
child: SizedBox(
|
|
width: 1000.0,
|
|
child: ListView(
|
|
controller: _controller,
|
|
padding: const EdgeInsets.all(16.0),
|
|
children: <Widget>[
|
|
..._fetchLogWidgets(),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
bool _shouldDisplay(LogMessage msg) {
|
|
var appConfig = context.watch<AppConfig>();
|
|
var filterLevel = appConfig.debugLogLevel;
|
|
|
|
if (filterLevel.isEmpty) {
|
|
return true;
|
|
}
|
|
|
|
if (filterLevel == 'v') {
|
|
return true;
|
|
}
|
|
if (filterLevel == 'd' && msg.l == 'v') {
|
|
return false;
|
|
}
|
|
if (filterLevel == 'i' && (msg.l == 'v' || msg.l == 'd')) {
|
|
return false;
|
|
}
|
|
if (filterLevel == 'w' && (msg.l == 'v' || msg.l == 'd' || msg.l == 'i')) {
|
|
return false;
|
|
}
|
|
if (filterLevel == 'e' &&
|
|
(msg.l == 'v' || msg.l == 'd' || msg.l == 'i' || msg.l == 'w')) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
Iterable<Widget> _fetchLogWidgets() sync* {
|
|
var prevDate = "";
|
|
for (var msg in _logs) {
|
|
if (!_shouldDisplay(msg)) {
|
|
continue;
|
|
}
|
|
|
|
var dt = DateTime.fromMillisecondsSinceEpoch(msg.t);
|
|
var date = dt.toIso8601String().substring(0, 10);
|
|
if (date != prevDate) {
|
|
yield _buildDateWidget(dt);
|
|
prevDate = date;
|
|
}
|
|
|
|
yield _buildLogWidget(msg);
|
|
}
|
|
}
|
|
|
|
Future<void> _copyToClipboard() async {
|
|
var messages = <String>[];
|
|
for (var logMsg in _logs) {
|
|
var msg = json.encode(logMsg.toMap());
|
|
messages.add(msg);
|
|
}
|
|
|
|
Clipboard.setData(ClipboardData(text: messages.join('\n')));
|
|
showSnackbar(context, context.loc.settingsDebugCopy);
|
|
}
|
|
|
|
Widget _buildLogWidget(LogMessage msg) {
|
|
var origTextStyle = Theme.of(context)
|
|
.textTheme
|
|
.bodyMedium!
|
|
.copyWith(fontFamily: "Roboto Mono");
|
|
|
|
var textStyle = origTextStyle.copyWith(color: _colorForLevel(msg.l));
|
|
|
|
var dt = DateTime.fromMillisecondsSinceEpoch(msg.t);
|
|
var timeStr = dt.toIso8601String().substring(11, 11 + 8);
|
|
var str = ' ${msg.msg}';
|
|
|
|
var props = <TextSpan>[];
|
|
msg.props?.forEach((key, value) {
|
|
var emptySpace = TextSpan(
|
|
text: '\n ',
|
|
style: textStyle.copyWith(fontWeight: FontWeight.bold));
|
|
props.add(emptySpace);
|
|
|
|
var keySpan = TextSpan(
|
|
text: '$key: ',
|
|
style: textStyle.copyWith(
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
);
|
|
var valueSpan = TextSpan(text: value.toString());
|
|
|
|
props.add(keySpan);
|
|
props.add(valueSpan);
|
|
});
|
|
|
|
var errorSpans = <TextSpan>[];
|
|
if (msg.ex != null) {
|
|
var emptySpace = TextSpan(
|
|
text: '\n ',
|
|
style: origTextStyle.copyWith(fontWeight: FontWeight.bold));
|
|
|
|
var exSpan = TextSpan(
|
|
text: '${msg.ex}',
|
|
style: textStyle.copyWith(
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
);
|
|
|
|
errorSpans.add(emptySpace);
|
|
errorSpans.add(exSpan);
|
|
}
|
|
|
|
if (msg.stack != null) {
|
|
var emptySpace = TextSpan(
|
|
text: '\n ',
|
|
style: textStyle.copyWith(fontWeight: FontWeight.bold));
|
|
|
|
var largestMemberLength = 0;
|
|
for (var entry in msg.stack!) {
|
|
var member = entry['member'];
|
|
if (member != null && member is String) {
|
|
if (member.length > largestMemberLength) {
|
|
largestMemberLength = member.length;
|
|
}
|
|
}
|
|
}
|
|
|
|
for (var entry in msg.stack!) {
|
|
var member = entry['member'];
|
|
if (member != null) {
|
|
var exSpan = TextSpan(
|
|
text: '$member'.padRight(largestMemberLength),
|
|
style: origTextStyle.copyWith(
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
);
|
|
|
|
errorSpans.add(emptySpace);
|
|
errorSpans.add(exSpan);
|
|
}
|
|
|
|
var location = entry['location'];
|
|
if (location != null && location is String) {
|
|
const prefix = 'package:';
|
|
if (location.startsWith(prefix)) {
|
|
location = location.substring(prefix.length);
|
|
}
|
|
var locSpan = TextSpan(
|
|
text: ' - $location',
|
|
style: origTextStyle,
|
|
);
|
|
|
|
// errorSpans.add(emptySpace);
|
|
errorSpans.add(locSpan);
|
|
}
|
|
}
|
|
}
|
|
|
|
return SelectableText.rich(
|
|
TextSpan(children: [
|
|
TextSpan(
|
|
text: timeStr,
|
|
style: textStyle.copyWith(fontWeight: FontWeight.bold)),
|
|
TextSpan(text: str),
|
|
...props,
|
|
...errorSpans,
|
|
], style: textStyle),
|
|
);
|
|
}
|
|
|
|
Color _colorForLevel(String l) {
|
|
var theme = Theme.of(context);
|
|
switch (l) {
|
|
case 'e':
|
|
return Colors.red;
|
|
}
|
|
return theme.brightness == Brightness.light ? Colors.black : Colors.white;
|
|
}
|
|
|
|
Widget _buildDateWidget(DateTime dt) {
|
|
var textStyle = Theme.of(context)
|
|
.textTheme
|
|
.titleLarge!
|
|
.copyWith(fontFamily: "Roboto Mono");
|
|
|
|
var text = dt.toIso8601String().substring(0, 10);
|
|
return Padding(
|
|
padding: const EdgeInsets.all(8.0),
|
|
child: SelectableText(
|
|
text,
|
|
style: textStyle,
|
|
textAlign: TextAlign.center,
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<void> _showFilterSelection() async {
|
|
var appConfig = context.watch<AppConfig>();
|
|
var filterLevel = appConfig.debugLogLevel;
|
|
|
|
var dialog = AlertDialog(
|
|
title: Text(context.loc.settingsDebugLevelsTitle),
|
|
content: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: <Widget>[
|
|
FilterListTile(
|
|
context.loc.settingsDebugLevelsError, 'e', filterLevel),
|
|
FilterListTile(
|
|
context.loc.settingsDebugLevelsWarning, 'w', filterLevel),
|
|
FilterListTile(context.loc.settingsDebugLevelsInfo, 'i', filterLevel),
|
|
FilterListTile(
|
|
context.loc.settingsDebugLevelsDebug, 'd', filterLevel),
|
|
FilterListTile(
|
|
context.loc.settingsDebugLevelsVerbose, 'v', filterLevel),
|
|
],
|
|
),
|
|
);
|
|
var l = await showDialog(context: context, builder: (context) => dialog);
|
|
if (l != null) {
|
|
appConfig.debugLogLevel = l;
|
|
appConfig.save();
|
|
}
|
|
}
|
|
}
|
|
|
|
class FilterListTile extends StatelessWidget {
|
|
final String publicLevel;
|
|
final String internalLevel;
|
|
final String currentLevel;
|
|
|
|
const FilterListTile(
|
|
this.publicLevel,
|
|
this.internalLevel,
|
|
this.currentLevel, {
|
|
super.key,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return ListTile(
|
|
leading: _getIcon(context),
|
|
title: Text(publicLevel),
|
|
onTap: () {
|
|
Navigator.pop(context, internalLevel);
|
|
},
|
|
selected: _isSelected(),
|
|
);
|
|
}
|
|
|
|
Icon _getIcon(BuildContext context) {
|
|
var theme = Theme.of(context);
|
|
var color = theme.textTheme.titleLarge!.color;
|
|
if (_isSelected()) {
|
|
switch (theme.brightness) {
|
|
case Brightness.light:
|
|
color = theme.primaryColor;
|
|
break;
|
|
case Brightness.dark:
|
|
color = theme.colorScheme.secondary;
|
|
break;
|
|
}
|
|
}
|
|
|
|
switch (internalLevel) {
|
|
case 'e':
|
|
return Icon(Icons.report, color: color);
|
|
case 'w':
|
|
return Icon(Icons.warning, color: color);
|
|
case 'i':
|
|
return Icon(Icons.info, color: color);
|
|
case 'd':
|
|
return Icon(Icons.bug_report, color: color);
|
|
case 'v':
|
|
return Icon(Icons.all_inclusive, color: color);
|
|
}
|
|
|
|
return Icon(Icons.all_inclusive, color: color);
|
|
}
|
|
|
|
bool _isSelected() {
|
|
if (currentLevel == 'v') {
|
|
return true;
|
|
}
|
|
if (currentLevel == 'd' && internalLevel == 'v') {
|
|
return false;
|
|
}
|
|
if (currentLevel == 'i' && (internalLevel == 'v' || internalLevel == 'd')) {
|
|
return false;
|
|
}
|
|
if (currentLevel == 'w' &&
|
|
(internalLevel == 'v' ||
|
|
internalLevel == 'd' ||
|
|
internalLevel == 'i')) {
|
|
return false;
|
|
}
|
|
if (currentLevel == 'e' &&
|
|
(internalLevel == 'v' ||
|
|
internalLevel == 'd' ||
|
|
internalLevel == 'i' ||
|
|
internalLevel == 'w')) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|