diff --git a/assets/images/undraw_detailed_analysis_re_tk6j.svg b/assets/images/undraw_detailed_analysis_re_tk6j.svg
new file mode 100644
index 000000000..c32758d7f
--- /dev/null
+++ b/assets/images/undraw_detailed_analysis_re_tk6j.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/lib/components/theming/font_fallbacks.dart b/lib/components/theming/font_fallbacks.dart
index b396e26fd..12bc34a59 100644
--- a/lib/components/theming/font_fallbacks.dart
+++ b/lib/components/theming/font_fallbacks.dart
@@ -36,9 +36,12 @@ extension FallbackTextStyle on TextStyle {
'Dekko',
];
- TextStyle withFallbacks() => copyWith(
+ TextStyle withFallbacks([
+ List fallbacks = sansSerifFallbacks,
+ ]) =>
+ copyWith(
fontFamilyFallback: fontFamilyFallback == null
- ? sansSerifFallbacks
- : [...fontFamilyFallback!, ...sansSerifFallbacks],
+ ? fallbacks
+ : [...fontFamilyFallback!, ...fallbacks],
);
}
diff --git a/lib/i18n/_missing_translations.yaml b/lib/i18n/_missing_translations.yaml
index 9a59d0da9..5e27d27cb 100644
--- a/lib/i18n/_missing_translations.yaml
+++ b/lib/i18n/_missing_translations.yaml
@@ -77,6 +77,8 @@ ar:
logs: Logs
viewLogs: View logs
debuggingInfo: Logs contain information useful for debugging and development
+ noLogs: "No logs here!"
+ logsAreTemporary: Logs are only kept until you close the app
login:
notYou(rich)(OUTDATED): "Not you? ${undoLogin(Choose another account)}."
status:
@@ -143,11 +145,15 @@ cs:
logs: Logs
viewLogs: View logs
debuggingInfo: Logs contain information useful for debugging and development
+ noLogs: "No logs here!"
+ logsAreTemporary: Logs are only kept until you close the app
de:
logs:
logs: Logs
viewLogs: View logs
debuggingInfo: Logs contain information useful for debugging and development
+ noLogs: "No logs here!"
+ logsAreTemporary: Logs are only kept until you close the app
es:
common:
done(OUTDATED): Done
@@ -180,6 +186,8 @@ es:
logs: Logs
viewLogs: View logs
debuggingInfo: Logs contain information useful for debugging and development
+ noLogs: "No logs here!"
+ logsAreTemporary: Logs are only kept until you close the app
login:
notYou(rich)(OUTDATED): "Not you? ${undoLogin(Choose another account)}."
status:
@@ -259,6 +267,8 @@ fa:
logs: Logs
viewLogs: View logs
debuggingInfo: Logs contain information useful for debugging and development
+ noLogs: "No logs here!"
+ logsAreTemporary: Logs are only kept until you close the app
login:
notYou(rich)(OUTDATED): "Not you? ${undoLogin(Choose another account)}."
status:
@@ -332,6 +342,8 @@ fr:
logs: Logs
viewLogs: View logs
debuggingInfo: Logs contain information useful for debugging and development
+ noLogs: "No logs here!"
+ logsAreTemporary: Logs are only kept until you close the app
login:
notYou(rich)(OUTDATED): "Not you? ${undoLogin(Choose another account)}."
status:
@@ -422,6 +434,8 @@ he:
logs: Logs
viewLogs: View logs
debuggingInfo: Logs contain information useful for debugging and development
+ noLogs: "No logs here!"
+ logsAreTemporary: Logs are only kept until you close the app
login:
notYou(rich)(OUTDATED): "Not you? ${undoLogin(Choose another account)}."
status:
@@ -582,6 +596,8 @@ hu:
logs: Logs
viewLogs: View logs
debuggingInfo: Logs contain information useful for debugging and development
+ noLogs: "No logs here!"
+ logsAreTemporary: Logs are only kept until you close the app
login:
notYou(rich)(OUTDATED): "Not you? ${undoLogin(Choose another account)}."
status:
@@ -705,6 +721,8 @@ it:
logs: Logs
viewLogs: View logs
debuggingInfo: Logs contain information useful for debugging and development
+ noLogs: "No logs here!"
+ logsAreTemporary: Logs are only kept until you close the app
ja:
common:
done(OUTDATED): Done
@@ -776,6 +794,8 @@ ja:
logs: Logs
viewLogs: View logs
debuggingInfo: Logs contain information useful for debugging and development
+ noLogs: "No logs here!"
+ logsAreTemporary: Logs are only kept until you close the app
login:
notYou(rich)(OUTDATED): "Not you? ${undoLogin(Choose another account)}."
status:
@@ -877,6 +897,8 @@ pt-BR:
logs: Logs
viewLogs: View logs
debuggingInfo: Logs contain information useful for debugging and development
+ noLogs: "No logs here!"
+ logsAreTemporary: Logs are only kept until you close the app
login:
notYou(rich)(OUTDATED): "Not you? ${undoLogin(Choose another account)}."
status:
@@ -940,18 +962,26 @@ ru:
logs: Logs
viewLogs: View logs
debuggingInfo: Logs contain information useful for debugging and development
+ noLogs: "No logs here!"
+ logsAreTemporary: Logs are only kept until you close the app
tr:
logs:
logs: Logs
viewLogs: View logs
debuggingInfo: Logs contain information useful for debugging and development
+ noLogs: "No logs here!"
+ logsAreTemporary: Logs are only kept until you close the app
zh-Hans-CN:
logs:
logs: Logs
viewLogs: View logs
debuggingInfo: Logs contain information useful for debugging and development
+ noLogs: "No logs here!"
+ logsAreTemporary: Logs are only kept until you close the app
zh-Hant-TW:
logs:
logs: Logs
viewLogs: View logs
debuggingInfo: Logs contain information useful for debugging and development
+ noLogs: "No logs here!"
+ logsAreTemporary: Logs are only kept until you close the app
diff --git a/lib/i18n/strings.g.dart b/lib/i18n/strings.g.dart
index a0b1cdd52..cce921142 100644
--- a/lib/i18n/strings.g.dart
+++ b/lib/i18n/strings.g.dart
@@ -233,6 +233,8 @@ class _StringsLogsEn {
String get logs => 'Logs';
String get viewLogs => 'View logs';
String get debuggingInfo => 'Logs contain information useful for debugging and development';
+ String get noLogs => 'No logs here!';
+ String get logsAreTemporary => 'Logs are only kept until you close the app';
}
// Path: login
diff --git a/lib/i18n/strings.i18n.yaml b/lib/i18n/strings.i18n.yaml
index 2635ecf3a..0d0fd9d64 100644
--- a/lib/i18n/strings.i18n.yaml
+++ b/lib/i18n/strings.i18n.yaml
@@ -149,6 +149,8 @@ logs:
logs: Logs
viewLogs: View logs
debuggingInfo: Logs contain information useful for debugging and development
+ noLogs: No logs here!
+ logsAreTemporary: Logs are only kept until you close the app
login:
title: Login
form:
diff --git a/lib/main_common.dart b/lib/main_common.dart
index 685d3d884..d04508d3b 100644
--- a/lib/main_common.dart
+++ b/lib/main_common.dart
@@ -50,17 +50,20 @@ Future main(
print('${record.level.name}: ${record.loggerName}: ${record.message}');
});
- final errorLogger = Logger('ErrorLogger');
- FlutterError.onError = (details) {
- errorLogger.severe(
- details.exceptionAsString(), details.exception, details.stack);
- FlutterError.presentError(details);
- };
- PlatformDispatcher.instance.onError = (error, stackTrace) {
- errorLogger.severe(error, stackTrace);
- // Returns false in debug mode so the error is printed to stderr
- return !kDebugMode;
- };
+ // For some reason, logging errors breaks hot reload while debugging.
+ if (!kDebugMode) {
+ final errorLogger = Logger('ErrorLogger');
+ FlutterError.onError = (details) {
+ errorLogger.severe(
+ details.exceptionAsString(), details.exception, details.stack);
+ FlutterError.presentError(details);
+ };
+ PlatformDispatcher.instance.onError = (error, stackTrace) {
+ errorLogger.severe(error, stackTrace);
+ // Returns false in debug mode so the error is printed to stderr
+ return !kDebugMode;
+ };
+ }
StrokeOptionsExtension.setDefaults();
Prefs.init();
diff --git a/lib/pages/logs.dart b/lib/pages/logs.dart
index 15e8a7b21..53052974e 100644
--- a/lib/pages/logs.dart
+++ b/lib/pages/logs.dart
@@ -1,5 +1,12 @@
+import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_svg/flutter_svg.dart';
+import 'package:google_fonts/google_fonts.dart';
import 'package:logging/logging.dart';
+import 'package:saber/components/theming/adaptive_icon.dart';
+import 'package:saber/components/theming/dynamic_material_app.dart';
+import 'package:saber/components/theming/font_fallbacks.dart';
import 'package:saber/i18n/strings.g.dart';
final logsHistory = _LogsHistory();
@@ -7,6 +14,22 @@ final logsHistory = _LogsHistory();
class _LogsHistory extends ChangeNotifier {
final _history = [];
+ /// A frozen copy of the history.
+ List? _frozenHistory;
+
+ /// The history of log records. Do not modify this list directly.
+ List get history => _frozenHistory ?? _history;
+
+ bool get isFrozen => _frozenHistory != null;
+
+ void freeze() {
+ _frozenHistory = List.unmodifiable(_history);
+ }
+
+ void unfreeze() {
+ _frozenHistory = null;
+ }
+
void add(LogRecord record) {
_history.add(record);
notifyListeners();
@@ -19,17 +42,115 @@ class LogsPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
- appBar: AppBar(
- title: Text(t.logs.logs),
- ),
body: AnimatedBuilder(
animation: logsHistory,
builder: (context, _) {
- return ListView.builder(
- itemCount: logsHistory._history.length,
- itemBuilder: (context, index) => _LogsItem(
- record: logsHistory._history[index],
- ),
+ final theme = Theme.of(context);
+ final colorScheme = theme.colorScheme;
+ final cupertino = theme.platform == TargetPlatform.iOS ||
+ theme.platform == TargetPlatform.macOS;
+ return CustomScrollView(
+ slivers: [
+ SliverAppBar(
+ collapsedHeight: kToolbarHeight,
+ expandedHeight: 200,
+ pinned: true,
+ scrolledUnderElevation: 1,
+ flexibleSpace: FlexibleSpaceBar(
+ title: Text(
+ t.logs.logs,
+ style: TextStyle(color: colorScheme.onSurface),
+ ),
+ centerTitle: cupertino,
+ ),
+ actions: [
+ if (logsHistory.isFrozen)
+ IconButton(
+ icon: AdaptiveIcon(
+ icon: Icons.play_arrow,
+ cupertinoIcon: CupertinoIcons.play_arrow,
+ ),
+ onPressed: logsHistory.unfreeze,
+ )
+ else
+ IconButton(
+ icon: AdaptiveIcon(
+ icon: Icons.pause,
+ cupertinoIcon: CupertinoIcons.pause,
+ ),
+ onPressed: logsHistory.freeze,
+ ),
+ IconButton(
+ icon: AdaptiveIcon(
+ icon: Icons.copy,
+ cupertinoIcon: CupertinoIcons.doc_on_clipboard,
+ ),
+ onPressed: logsHistory.history.isEmpty
+ ? null
+ : () {
+ final buffer = StringBuffer();
+ for (final record in logsHistory.history) {
+ buffer.write(record.level.name);
+ buffer.write(' at ');
+ buffer.writeln(record.time);
+ buffer.writeln(record.message);
+ if (record.error != null)
+ buffer.writeln(record.error);
+ if (record.stackTrace != null)
+ buffer.writeln(record.stackTrace);
+ buffer.writeln();
+ }
+ Clipboard.setData(
+ ClipboardData(text: buffer.toString()),
+ );
+ },
+ ),
+ ],
+ ),
+ if (logsHistory.history.isEmpty)
+ SliverFillRemaining(
+ child: Padding(
+ padding: const EdgeInsets.all(16),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ SvgPicture.asset(
+ 'assets/images/undraw_detailed_analysis_re_tk6j.svg',
+ width: 300,
+ height: 300 * 570 / 925.49161,
+ excludeFromSemantics: true,
+ ),
+ const SizedBox(height: 64),
+ Text(
+ t.logs.noLogs,
+ style: TextStyle(
+ color: colorScheme.onSurface,
+ fontSize: 24,
+ ),
+ ),
+ SizedBox(height: 8),
+ Text(
+ t.logs.logsAreTemporary,
+ style: TextStyle(color: colorScheme.onSurface),
+ ),
+ ],
+ ),
+ ),
+ )
+ else
+ SliverList.builder(
+ itemCount: logsHistory.history.length,
+ itemBuilder: (context, index) {
+ if (index < 0 || index >= logsHistory.history.length) {
+ return const SizedBox();
+ }
+ return _LogsItem(
+ record: logsHistory
+ .history[logsHistory.history.length - 1 - index],
+ );
+ },
+ ),
+ ],
);
},
),
@@ -51,6 +172,32 @@ class _LogsItem extends StatelessWidget {
children: [
_LogLevel(level: record.level),
Text(record.message),
+ if (record.stackTrace != null)
+ DecoratedBox(
+ decoration: BoxDecoration(
+ color: const Color(0xCC000000),
+ borderRadius: BorderRadius.circular(4),
+ ),
+ child: SingleChildScrollView(
+ scrollDirection: Axis.horizontal,
+ child: Text(
+ record.stackTrace.toString(),
+ style: GoogleFonts.firaMono(
+ fontSize: 11,
+ color: const Color(0xFFFFFFFF),
+ ).withFallbacks([
+ 'Fira Mono',
+ 'ui-monospace',
+ 'Cascadia Code',
+ 'Source Code Pro',
+ 'Menlo',
+ 'Consolas',
+ 'DejaVu Sans Mono',
+ 'monospace',
+ ]),
+ ),
+ ),
+ ),
],
),
);