diff --git a/app/lib/features/geckoview/features/browser/presentation/dialogs/clear_site_data_dialog.dart b/app/lib/features/geckoview/features/browser/presentation/dialogs/clear_site_data_dialog.dart new file mode 100644 index 00000000..6ab7d5dd --- /dev/null +++ b/app/lib/features/geckoview/features/browser/presentation/dialogs/clear_site_data_dialog.dart @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2024-2025 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 confirmation dialog before clearing site data. +/// +/// Returns true if the user confirms, false otherwise. +Future showClearSiteDataDialog( + BuildContext context, { + required String host, + required String formattedTypes, +}) { + return showDialog( + context: context, + builder: (context) => AlertDialog( + icon: const Icon(Icons.warning), + title: const Text('Clear Site Data'), + content: Text( + 'This will clear $formattedTypes for $host.\n\n' + 'You may need to log in again.', + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, false), + child: const Text('Cancel'), + ), + FilledButton( + onPressed: () => Navigator.pop(context, true), + child: const Text('Clear'), + ), + ], + ), + ); +} diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/clear_site_data_section.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/clear_site_data_section.dart index 8b030c36..f4614742 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/clear_site_data_section.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/clear_site_data_section.dart @@ -22,6 +22,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:weblibre/features/geckoview/domain/providers/tab_session.dart'; +import 'package:weblibre/features/geckoview/features/browser/presentation/dialogs/clear_site_data_dialog.dart'; import 'package:weblibre/utils/ui_helper.dart'; /// Section widget for clearing site data @@ -140,26 +141,10 @@ class ClearSiteDataSection extends HookConsumerWidget { return; } - final confirmed = await showDialog( - context: context, - builder: (context) => AlertDialog( - icon: const Icon(Icons.warning), - title: const Text('Clear Site Data'), - content: Text( - 'This will clear ${_formatTypes(selectedTypes.value)} for ${url.host}.\n\n' - 'You may need to log in again.', - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context, false), - child: const Text('Cancel'), - ), - FilledButton( - onPressed: () => Navigator.pop(context, true), - child: const Text('Clear'), - ), - ], - ), + final confirmed = await showClearSiteDataDialog( + context, + host: url.host, + formattedTypes: _formatTypes(selectedTypes.value), ); if (confirmed == true && context.mounted) { diff --git a/app/lib/features/geckoview/features/search/presentation/dialogs/reset_bang_dialog.dart b/app/lib/features/geckoview/features/search/presentation/dialogs/reset_bang_dialog.dart new file mode 100644 index 00000000..84537b2d --- /dev/null +++ b/app/lib/features/geckoview/features/search/presentation/dialogs/reset_bang_dialog.dart @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2024-2025 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 confirmation dialog for resetting a bang's usage frequency. +/// +/// Returns true if the user confirms reset, false otherwise. +Future showResetBangDialog( + BuildContext context, { + required String triggerName, +}) { + return showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text('Reset usage frequency of $triggerName?'), + content: const Text('This will remove the Bang from quick select.'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, false), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () => Navigator.pop(context, true), + child: const Text('Reset'), + ), + ], + ), + ); +} diff --git a/app/lib/features/geckoview/features/search/presentation/widgets/bang_chips.dart b/app/lib/features/geckoview/features/search/presentation/widgets/bang_chips.dart index f2f8a30a..bc947fd4 100644 --- a/app/lib/features/geckoview/features/search/presentation/widgets/bang_chips.dart +++ b/app/lib/features/geckoview/features/search/presentation/widgets/bang_chips.dart @@ -47,29 +47,6 @@ class BangChips extends HookConsumerWidget { super.key, }); - static Future resetBangDialog( - BuildContext context, - String triggerName, - ) { - return showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text('Reset usage frequency of $triggerName?'), - content: const Text('This will remove the Bang from quick select.'), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context, false), - child: const Text('Cancel'), - ), - TextButton( - onPressed: () => Navigator.pop(context, true), - child: const Text('Reset'), - ), - ], - ), - ); - } - @override Widget build(BuildContext context, WidgetRef ref) { final availableBangs = ref.watch(seamlessBangProvider); diff --git a/app/lib/features/geckoview/features/search/presentation/widgets/search_modules/full_search_suggestions.dart b/app/lib/features/geckoview/features/search/presentation/widgets/search_modules/full_search_suggestions.dart index e83936c5..aab17cc7 100644 --- a/app/lib/features/geckoview/features/search/presentation/widgets/search_modules/full_search_suggestions.dart +++ b/app/lib/features/geckoview/features/search/presentation/widgets/search_modules/full_search_suggestions.dart @@ -29,6 +29,7 @@ import 'package:weblibre/features/bangs/domain/providers/bangs.dart'; import 'package:weblibre/features/bangs/domain/repositories/data.dart'; import 'package:weblibre/features/geckoview/features/browser/domain/providers.dart'; import 'package:weblibre/features/geckoview/features/search/domain/providers/search_suggestions.dart'; +import 'package:weblibre/features/geckoview/features/search/presentation/dialogs/reset_bang_dialog.dart'; import 'package:weblibre/features/geckoview/features/search/presentation/widgets/bang_chips.dart'; class FullSearchTermSuggestions extends HookConsumerWidget { @@ -168,9 +169,9 @@ class FullSearchTermSuggestions extends HookConsumerWidget { .read(selectedBangTriggerProvider().notifier) .clearTrigger(); } else { - final dialogResult = await BangChips.resetBangDialog( + final dialogResult = await showResetBangDialog( context, - bang.trigger, + triggerName: bang.trigger, ); if (dialogResult == true) { diff --git a/app/lib/features/settings/presentation/dialogs/delete_all_exceptions_dialog.dart b/app/lib/features/settings/presentation/dialogs/delete_all_exceptions_dialog.dart new file mode 100644 index 00000000..630adff5 --- /dev/null +++ b/app/lib/features/settings/presentation/dialogs/delete_all_exceptions_dialog.dart @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2024-2025 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 confirmation dialog for deleting all tracking protection exceptions. +/// +/// Returns true if the user confirms deletion, false otherwise. +Future showDeleteAllExceptionsDialog(BuildContext context) { + return showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Delete All Exceptions?'), + content: const Text( + 'This will re-enable tracking protection for all exception sites.', + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, false), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () => Navigator.pop(context, true), + child: const Text('Delete'), + ), + ], + ), + ); +} diff --git a/app/lib/features/settings/presentation/screens/tracking_protection_exceptions.dart b/app/lib/features/settings/presentation/screens/tracking_protection_exceptions.dart index a585fe1d..f321fc99 100644 --- a/app/lib/features/settings/presentation/screens/tracking_protection_exceptions.dart +++ b/app/lib/features/settings/presentation/screens/tracking_protection_exceptions.dart @@ -23,6 +23,7 @@ import 'package:flutter_material_design_icons/flutter_material_design_icons.dart import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:weblibre/features/geckoview/features/browser/domain/repositories/tracking_protection.dart'; +import 'package:weblibre/features/settings/presentation/dialogs/delete_all_exceptions_dialog.dart'; import 'package:weblibre/presentation/widgets/url_icon.dart'; import 'package:weblibre/utils/ui_helper.dart'; @@ -91,36 +92,19 @@ class TrackingProtectionExceptionsScreen extends HookConsumerWidget { } Future _showDeleteAllDialog(BuildContext context, WidgetRef ref) async { - await showDialog( - context: context, - builder: (context) => AlertDialog( - title: const Text('Delete All Exceptions?'), - content: const Text( - 'This will re-enable tracking protection for all exception sites.', - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text('Cancel'), - ), - TextButton( - onPressed: () async { - Navigator.pop(context); - try { - await ref - .read(trackingProtectionRepositoryProvider.notifier) - .removeAllExceptions(); - } catch (e) { - if (context.mounted) { - showErrorMessage(context, 'Failed to delete exceptions: $e'); - } - } - }, - child: const Text('Delete'), - ), - ], - ), - ); + final confirmed = await showDeleteAllExceptionsDialog(context); + + if (confirmed == true) { + try { + await ref + .read(trackingProtectionRepositoryProvider.notifier) + .removeAllExceptions(); + } catch (e) { + if (context.mounted) { + showErrorMessage(context, 'Failed to delete exceptions: $e'); + } + } + } } Future _deleteException( diff --git a/app/lib/features/user/domain/presentation/dialogs/quit_browser_dialog.dart b/app/lib/features/user/domain/presentation/dialogs/quit_browser_dialog.dart new file mode 100644 index 00000000..131282cc --- /dev/null +++ b/app/lib/features/user/domain/presentation/dialogs/quit_browser_dialog.dart @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2024-2025 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 confirmation dialog for quitting the browser. +/// +/// Returns true if the user confirms, false if cancelled, null if dismissed. +Future showQuitBrowserDialog(BuildContext context) { + return showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + icon: const Icon(Icons.warning), + title: const Text('Quit Browser'), + content: const Text( + 'This will properly shutdown the browser and clear private tabs', + ), + actions: [ + TextButton( + onPressed: () { + Navigator.pop(context, false); + }, + child: const Text('Cancel'), + ), + TextButton( + onPressed: () { + Navigator.pop(context, true); + }, + child: const Text('Quit'), + ), + ], + ); + }, + ); +} diff --git a/app/lib/features/user/domain/presentation/dialogs/select_profile.dart b/app/lib/features/user/domain/presentation/dialogs/select_profile.dart index 21c71f3a..529fe3d1 100644 --- a/app/lib/features/user/domain/presentation/dialogs/select_profile.dart +++ b/app/lib/features/user/domain/presentation/dialogs/select_profile.dart @@ -22,6 +22,7 @@ import 'package:flutter_material_design_icons/flutter_material_design_icons.dart import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:weblibre/core/filesystem.dart'; import 'package:weblibre/core/routing/routes.dart'; +import 'package:weblibre/features/user/domain/presentation/dialogs/quit_browser_dialog.dart'; import 'package:weblibre/features/user/domain/presentation/utils/profile_switch_handler.dart'; import 'package:weblibre/features/user/domain/repositories/profile.dart'; import 'package:weblibre/presentation/widgets/failure_widget.dart'; @@ -70,32 +71,7 @@ class SelectProfileDialog extends HookConsumerWidget { iconAlignment: IconAlignment.start, label: const Text('Quit Browser'), onPressed: () async { - final result = await showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - icon: const Icon(Icons.warning), - title: const Text('Quit Browser'), - content: const Text( - 'This will properly shutdown the browser and clear private tabs', - ), - actions: [ - TextButton( - onPressed: () { - Navigator.pop(context, false); - }, - child: const Text('Cancel'), - ), - TextButton( - onPressed: () { - Navigator.pop(context, true); - }, - child: const Text('Quit'), - ), - ], - ); - }, - ); + final result = await showQuitBrowserDialog(context); if (result == true) { await exitApp(ref.container); diff --git a/app/lib/features/user/domain/presentation/dialogs/switch_profile_dialog.dart b/app/lib/features/user/domain/presentation/dialogs/switch_profile_dialog.dart new file mode 100644 index 00000000..88845b1c --- /dev/null +++ b/app/lib/features/user/domain/presentation/dialogs/switch_profile_dialog.dart @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2024-2025 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'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:go_router/go_router.dart'; + +/// Result of the switch profile dialog. +/// - `shouldSwitch`: Whether the user confirmed the switch. +/// - `clearCache`: Whether to clear the shared cache. +typedef SwitchProfileDialogResult = ({bool shouldSwitch, bool clearCache}); + +/// Shows a confirmation dialog for switching user profiles. +/// +/// Returns a [SwitchProfileDialogResult] if the user confirms, or null if dismissed. +/// If [duplicateMozillaProfile] is provided, shows an option to clear shared cache. +Future showSwitchProfileDialog( + BuildContext context, { + required String profileName, + String? duplicateMozillaProfile, +}) { + return showDialog( + context: context, + builder: (context) => HookBuilder( + builder: (context) { + final clearCache = useState(false); + + return AlertDialog( + icon: const Icon(Icons.warning), + title: const Text('Switch User'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "Switching to User '$profileName' will require a restart of the Browser.", + style: const TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + if (duplicateMozillaProfile != null) + SwitchListTile( + contentPadding: EdgeInsets.zero, + value: clearCache.value, + title: const Text('Clear Shared Cache'), + subtitle: const Text( + 'This User has been created based on an exisiting Mozilla Profile Identifier. Clearing cache will affect all linked accounts.', + ), + onChanged: (value) { + clearCache.value = value; + }, + ), + ], + ), + actions: [ + TextButton( + onPressed: () { + context.pop((shouldSwitch: false, clearCache: false)); + }, + child: const Text('Cancel'), + ), + TextButton( + onPressed: () { + context.pop((shouldSwitch: true, clearCache: clearCache.value)); + }, + child: const Text('Switch Profile'), + ), + ], + ); + }, + ), + ); +} diff --git a/app/lib/features/user/domain/presentation/utils/profile_switch_handler.dart b/app/lib/features/user/domain/presentation/utils/profile_switch_handler.dart index 9237a667..0b42db61 100644 --- a/app/lib/features/user/domain/presentation/utils/profile_switch_handler.dart +++ b/app/lib/features/user/domain/presentation/utils/profile_switch_handler.dart @@ -18,11 +18,10 @@ * along with this program. If not, see . */ import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:weblibre/core/filesystem.dart'; import 'package:weblibre/domain/entities/profile.dart'; +import 'package:weblibre/features/user/domain/presentation/dialogs/switch_profile_dialog.dart'; import 'package:weblibre/features/user/domain/repositories/profile.dart'; import 'package:weblibre/utils/exit_app.dart'; import 'package:weblibre/utils/ui_helper.dart' as ui_helper; @@ -54,58 +53,14 @@ Future handleSwitchProfile( if (!context.mounted) return; - final result = await showDialog<(bool, bool)>( - context: context, - builder: (context) => HookBuilder( - builder: (context) { - final clearCache = useState(false); - - return AlertDialog( - icon: const Icon(Icons.warning), - title: const Text('Switch User'), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - "Switching to User '${profile.name}' will require a restart of the Browser.", - style: const TextStyle(fontWeight: FontWeight.bold), - ), - const SizedBox(height: 8), - if (duplicateMozillaProfile != null) - SwitchListTile( - contentPadding: EdgeInsets.zero, - value: clearCache.value, - title: const Text('Clear Shared Cache'), - subtitle: const Text( - 'This User has been created based on an exisiting Mozilla Profile Identifier. Clearing cache will affect all linked accounts.', - ), - onChanged: (value) { - clearCache.value = value; - }, - ), - ], - ), - actions: [ - TextButton( - onPressed: () { - context.pop((false, false)); - }, - child: const Text('Cancel'), - ), - TextButton( - onPressed: () { - context.pop((true, clearCache.value)); - }, - child: const Text('Switch Profile'), - ), - ], - ); - }, - ), + final result = await showSwitchProfileDialog( + context, + profileName: profile.name, + duplicateMozillaProfile: duplicateMozillaProfile, ); - if (result?.$1 == true) { - if (duplicateMozillaProfile != null && result?.$2 == true) { + if (result?.shouldSwitch == true) { + if (duplicateMozillaProfile != null && result?.clearCache == true) { await filesystem.clearMozillaProfileCache(duplicateMozillaProfile); }