diff --git a/app/lib/features/geckoview/features/pwa/presentation/dialogs/pwa_install_dialog.dart b/app/lib/features/geckoview/features/pwa/presentation/dialogs/pwa_install_dialog.dart new file mode 100644 index 00000000..1006f1ba --- /dev/null +++ b/app/lib/features/geckoview/features/pwa/presentation/dialogs/pwa_install_dialog.dart @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2024-2026 Fabian Freund. + * + * This file is part of WebLibre + * (see https://weblibre.eu). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import 'package:flutter/material.dart'; + +/// Shows a dialog asking the user to confirm adding a PWA to the home screen. +/// +/// Returns true if the user confirms, false if cancelled. +Future showPwaInstallConfirmDialog(BuildContext context, String name) { + return showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Add to Home Screen'), + content: Text('Add "$name" to your home screen?'), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: const Text('Cancel'), + ), + FilledButton( + onPressed: () => Navigator.of(context).pop(true), + child: const Text('Add'), + ), + ], + ), + ); +} diff --git a/app/lib/features/geckoview/features/pwa/presentation/widgets/pwa_install_button.dart b/app/lib/features/geckoview/features/pwa/presentation/widgets/pwa_install_button.dart index e5bb33f6..13e96d58 100644 --- a/app/lib/features/geckoview/features/pwa/presentation/widgets/pwa_install_button.dart +++ b/app/lib/features/geckoview/features/pwa/presentation/widgets/pwa_install_button.dart @@ -22,29 +22,14 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:weblibre/core/logger.dart'; import 'package:weblibre/features/geckoview/features/pwa/domain/providers.dart'; +import 'package:weblibre/features/geckoview/features/pwa/presentation/dialogs/pwa_install_dialog.dart'; import 'package:weblibre/utils/ui_helper.dart'; Future showPwaInstallDialog(BuildContext context, WidgetRef ref) async { final manifest = ref.read(currentTabManifestProvider); final name = manifest?.name ?? manifest?.shortName ?? 'this web app'; - final confirmed = await showDialog( - context: context, - builder: (context) => AlertDialog( - title: const Text('Add to Home Screen'), - content: Text('Add "$name" to your home screen?'), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(false), - child: const Text('Cancel'), - ), - FilledButton( - onPressed: () => Navigator.of(context).pop(true), - child: const Text('Add'), - ), - ], - ), - ); + final confirmed = await showPwaInstallConfirmDialog(context, name); if (confirmed == true) { try { diff --git a/app/lib/features/settings/presentation/dialogs/log_details_dialog.dart b/app/lib/features/settings/presentation/dialogs/log_details_dialog.dart new file mode 100644 index 00000000..59c98eb5 --- /dev/null +++ b/app/lib/features/settings/presentation/dialogs/log_details_dialog.dart @@ -0,0 +1,211 @@ +/* + * Copyright (c) 2024-2026 Fabian Freund. + * + * This file is part of WebLibre + * (see https://weblibre.eu). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +// ignore_for_file: deprecated_member_use + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:intl/intl.dart'; +import 'package:logger/logger.dart'; +import 'package:weblibre/utils/ui_helper.dart'; + +IconData _levelIcon(Level level) { + return switch (level) { + Level.trace => Icons.blur_circular, + Level.debug => Icons.bug_report, + Level.info => Icons.info, + Level.warning => Icons.warning, + Level.error => Icons.error, + Level.fatal => Icons.dangerous, + Level.all => Icons.notes, + Level.verbose => Icons.chat_bubble_outline, + Level.wtf => Icons.question_mark, + Level.nothing => Icons.close, + Level.off => Icons.offline_bolt, + }; +} + +Color _levelColor(Level level) { + return switch (level) { + Level.trace => Colors.grey, + Level.debug => Colors.blue, + Level.info => Colors.cyan, + Level.warning => Colors.orange, + Level.error => Colors.red, + Level.fatal => Colors.purple, + Level.all => Colors.grey, + Level.verbose => Colors.teal, + Level.wtf => Colors.brown, + Level.nothing => Colors.grey, + Level.off => Colors.grey, + }; +} + +class LogDetailsDialog extends StatelessWidget { + const LogDetailsDialog({ + required this.level, + required this.message, + this.error, + this.stackTrace, + required this.time, + super.key, + }); + + final Level level; + final String message; + final String? error; + final String? stackTrace; + final DateTime time; + + String _formatTime(DateTime time) { + return DateFormat('HH:mm:ss.SSS').format(time); + } + + Future _copyEntryToClipboard(BuildContext context) async { + final text = StringBuffer(); + + text.writeln( + '[${_formatTime(time)}] ${level.name.toUpperCase()}: $message', + ); + if (error != null) { + text.writeln('Error: $error'); + } + if (stackTrace != null) { + text.writeln('Stack Trace:'); + text.writeln(stackTrace); + } + + await Clipboard.setData(ClipboardData(text: text.toString())); + + if (context.mounted) { + showInfoMessage(context, 'Entry copied'); + } + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Row( + children: [ + Icon(_levelIcon(level), color: _levelColor(level)), + const SizedBox(width: 8), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text(level.name.toUpperCase()), + Text( + _formatTime(time), + style: Theme.of(context).textTheme.bodySmall, + ), + ], + ), + ], + ), + content: SizedBox( + width: double.maxFinite, + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + if (message.isNotEmpty) ...[ + Text( + 'Message:', + style: TextStyle( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.primary, + ), + ), + const SizedBox(height: 4), + SelectableText( + message, + style: GoogleFonts.robotoMono(fontSize: 12), + ), + const SizedBox(height: 16), + ], + if (error != null) ...[ + Text( + 'Error:', + style: TextStyle( + fontWeight: FontWeight.bold, + color: _levelColor(level), + ), + ), + const SizedBox(height: 4), + SelectableText( + error!, + style: GoogleFonts.robotoMono(fontSize: 11), + ), + const SizedBox(height: 16), + ], + if (stackTrace != null) ...[ + Text( + 'Stack Trace:', + style: TextStyle( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.primary, + ), + ), + const SizedBox(height: 4), + SelectableText( + stackTrace!, + style: GoogleFonts.robotoMono(fontSize: 9), + ), + ], + ], + ), + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Close'), + ), + TextButton.icon( + onPressed: () => _copyEntryToClipboard(context), + icon: const Icon(Icons.copy), + label: const Text('Copy'), + ), + ], + ); + } +} + +Future showLogDetailsDialog( + BuildContext context, { + required Level level, + required String message, + String? error, + String? stackTrace, + required DateTime time, +}) { + return showDialog( + context: context, + builder: (context) => LogDetailsDialog( + level: level, + message: message, + error: error, + stackTrace: stackTrace, + time: time, + ), + ); +} diff --git a/app/lib/features/settings/presentation/screens/error_logs_screen.dart b/app/lib/features/settings/presentation/screens/error_logs_screen.dart index 891eda6b..62c364c2 100644 --- a/app/lib/features/settings/presentation/screens/error_logs_screen.dart +++ b/app/lib/features/settings/presentation/screens/error_logs_screen.dart @@ -25,11 +25,11 @@ import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:intl/intl.dart'; import 'package:logger/logger.dart'; import 'package:timeago/timeago.dart' as timeago; import 'package:weblibre/core/logger.dart'; import 'package:weblibre/features/settings/domain/providers/log_filter.dart'; +import 'package:weblibre/features/settings/presentation/dialogs/log_details_dialog.dart'; import 'package:weblibre/utils/ui_helper.dart'; IconData _levelIcon(Level level) { @@ -203,15 +203,13 @@ class _LogEntryTile extends StatelessWidget { margin: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0), child: InkWell( onTap: () async { - await showDialog( - context: context, - builder: (context) => _LogDetailsDialog( - level: level, - message: message, - error: error, - stackTrace: stackTrace, - time: time, - ), + await showLogDetailsDialog( + context, + level: level, + message: message, + error: error, + stackTrace: stackTrace, + time: time, ); }, child: ListTile( @@ -241,133 +239,3 @@ class _LogEntryTile extends StatelessWidget { ); } } - -class _LogDetailsDialog extends StatelessWidget { - const _LogDetailsDialog({ - required this.level, - required this.message, - this.error, - this.stackTrace, - required this.time, - }); - - final Level level; - final String message; - final String? error; - final String? stackTrace; - final DateTime time; - - String _formatTime(DateTime time) { - return DateFormat('HH:mm:ss.SSS').format(time); - } - - Future _copyEntryToClipboard(BuildContext context) async { - final text = StringBuffer(); - - text.writeln( - '[${_formatTime(time)}] ${level.name.toUpperCase()}: $message', - ); - if (error != null) { - text.writeln('Error: $error'); - } - if (stackTrace != null) { - text.writeln('Stack Trace:'); - text.writeln(stackTrace); - } - - await Clipboard.setData(ClipboardData(text: text.toString())); - - if (context.mounted) { - showInfoMessage(context, 'Entry copied'); - } - } - - @override - Widget build(BuildContext context) { - return AlertDialog( - title: Row( - children: [ - Icon(_levelIcon(level), color: _levelColor(level)), - const SizedBox(width: 8), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text(level.name.toUpperCase()), - Text( - _formatTime(time), - style: Theme.of(context).textTheme.bodySmall, - ), - ], - ), - ], - ), - content: SizedBox( - width: double.maxFinite, - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - if (message.isNotEmpty) ...[ - Text( - 'Message:', - style: TextStyle( - fontWeight: FontWeight.bold, - color: Theme.of(context).colorScheme.primary, - ), - ), - const SizedBox(height: 4), - SelectableText( - message, - style: GoogleFonts.robotoMono(fontSize: 12), - ), - const SizedBox(height: 16), - ], - if (error != null) ...[ - Text( - 'Error:', - style: TextStyle( - fontWeight: FontWeight.bold, - color: _levelColor(level), - ), - ), - const SizedBox(height: 4), - SelectableText( - error!, - style: GoogleFonts.robotoMono(fontSize: 11), - ), - const SizedBox(height: 16), - ], - if (stackTrace != null) ...[ - Text( - 'Stack Trace:', - style: TextStyle( - fontWeight: FontWeight.bold, - color: Theme.of(context).colorScheme.primary, - ), - ), - const SizedBox(height: 4), - SelectableText( - stackTrace!, - style: GoogleFonts.robotoMono(fontSize: 9), - ), - ], - ], - ), - ), - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text('Close'), - ), - TextButton.icon( - onPressed: () => _copyEntryToClipboard(context), - icon: const Icon(Icons.copy), - label: const Text('Copy'), - ), - ], - ); - } -}