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', + ]), + ), + ), + ), ], ), );