From bcacc4c1e8d3f1efd05b34698a55bb2b96b79ca2 Mon Sep 17 00:00:00 2001 From: Fabian Freund Date: Sun, 24 Aug 2025 12:22:15 +0200 Subject: [PATCH] tab close undo --- .../geckoview/domain/repositories/tab.dart | 4 ++ .../browser/presentation/screens/browser.dart | 17 +++++ .../browser_modules/bottom_app_bar.dart | 12 ++++ .../widgets/browser_modules/browser_view.dart | 2 - .../widgets/sheets/view_tabs.dart | 43 ++++-------- .../widgets/tab_creation_menu.dart | 8 +++ .../presentation/widgets/tab_preview.dart | 70 ++++++++++--------- .../tabs/domain/repositories/tab.dart | 8 ++- app/lib/utils/ui_helper.dart | 17 +++++ .../components/Core.kt | 3 +- 10 files changed, 114 insertions(+), 70 deletions(-) diff --git a/app/lib/features/geckoview/domain/repositories/tab.dart b/app/lib/features/geckoview/domain/repositories/tab.dart index f7025182..56913b80 100644 --- a/app/lib/features/geckoview/domain/repositories/tab.dart +++ b/app/lib/features/geckoview/domain/repositories/tab.dart @@ -245,6 +245,10 @@ class TabRepository extends _$TabRepository { return _tabsService.removeTabs(ids: tabIds); } + Future undoClose() { + return _tabsService.undo(); + } + @override void build() { final eventSerivce = ref.watch(eventServiceProvider); diff --git a/app/lib/features/geckoview/features/browser/presentation/screens/browser.dart b/app/lib/features/geckoview/features/browser/presentation/screens/browser.dart index 6f369a75..1e0b13e6 100644 --- a/app/lib/features/geckoview/features/browser/presentation/screens/browser.dart +++ b/app/lib/features/geckoview/features/browser/presentation/screens/browser.dart @@ -272,6 +272,13 @@ class BrowserScreen extends HookConsumerWidget { await ref .read(tabRepositoryProvider.notifier) .closeTab(details.data.tabId); + + if (context.mounted) { + ui_helper.showTabUndoClose( + context, + ref.read(tabRepositoryProvider.notifier).undoClose, + ); + } }, builder: (context, _, _) { return OverlayPortal( @@ -372,6 +379,16 @@ class BrowserScreen extends HookConsumerWidget { await ref .read(tabRepositoryProvider.notifier) .closeTab(tabState.id); + + if (context.mounted) { + ui_helper.showTabUndoClose( + context, + ref + .read(tabRepositoryProvider.notifier) + .undoClose, + ); + } + return true; } else { //Mark back as unhandled and navigator will pop diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart index 8a5027c9..362e20e4 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart @@ -410,7 +410,19 @@ class BrowserBottomAppBar extends HookConsumerWidget { await ref .read(tabRepositoryProvider.notifier) .closeTab(selectedTabId); + trippleDotMenuController.close(); + + if (context.mounted) { + ui_helper.showTabUndoClose( + context, + ref + .read( + tabRepositoryProvider.notifier, + ) + .undoClose, + ); + } }, icon: const Icon(Icons.close), ), diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/browser_view.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/browser_view.dart index fb8cd506..fffc31aa 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/browser_view.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/browser_view.dart @@ -108,8 +108,6 @@ class _BrowserViewState extends ConsumerState _periodicScreenshotUpdate?.cancel(); _periodicScreenshotUpdate = null; _timerPaused = false; - - ScaffoldMessenger.of(context).hideCurrentSnackBar(); } if (next.isLoading == false && diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/view_tabs.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/view_tabs.dart index c1601ea7..168a2d5b 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/view_tabs.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/view_tabs.dart @@ -31,6 +31,7 @@ import 'package:weblibre/core/routing/routes.dart'; import 'package:weblibre/data/models/drag_data.dart'; import 'package:weblibre/features/geckoview/domain/providers/selected_tab.dart'; import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart'; +import 'package:weblibre/features/geckoview/domain/repositories/tab.dart'; import 'package:weblibre/features/geckoview/features/browser/domain/providers.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/tab_suggestions.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/tree_view.dart'; @@ -47,6 +48,7 @@ import 'package:weblibre/features/tor/presentation/controllers/start_tor_proxy.d import 'package:weblibre/features/user/domain/repositories/general_settings.dart'; import 'package:weblibre/presentation/hooks/listenable_callback.dart'; import 'package:weblibre/presentation/widgets/speech_to_text_button.dart'; +import 'package:weblibre/utils/ui_helper.dart' as ui_helper; class _TabDraggable extends HookConsumerWidget { final TabEntity entity; @@ -216,38 +218,17 @@ class _TabSheetHeader extends HookConsumerWidget { TextButton.icon( onPressed: () async { final container = ref.read(selectedContainerProvider); - final result = await showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - title: const Text('Close All Tabs'), - content: Text( - (container != null) - ? 'Are you sure you want to close all container tabs?' - : 'Are you sure you want to close all unassigned tabs?', - ), - actions: [ - TextButton( - onPressed: () { - Navigator.pop(context, false); - }, - child: const Text('Cancel'), - ), - TextButton( - onPressed: () { - Navigator.pop(context, true); - }, - child: const Text('Close'), - ), - ], - ); - }, - ); - if (result == true) { - await ref - .read(tabDataRepositoryProvider.notifier) - .closeAllTabsByContainer(container); + final count = await ref + .read(tabDataRepositoryProvider.notifier) + .closeAllTabsByContainer(container); + + if (context.mounted) { + ui_helper.showTabUndoClose( + context, + ref.read(tabRepositoryProvider.notifier).undoClose, + count: count, + ); } }, icon: const Icon(MdiIcons.closeBoxMultiple), diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/tab_creation_menu.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/tab_creation_menu.dart index adb26945..9e11c7bb 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/tab_creation_menu.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/tab_creation_menu.dart @@ -23,6 +23,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:weblibre/core/routing/routes.dart'; import 'package:weblibre/features/geckoview/domain/repositories/tab.dart'; import 'package:weblibre/features/user/domain/repositories/general_settings.dart'; +import 'package:weblibre/utils/ui_helper.dart' as ui_helper; class TabCreationMenu extends HookConsumerWidget { final Widget child; @@ -56,6 +57,13 @@ class TabCreationMenu extends HookConsumerWidget { await ref .read(tabRepositoryProvider.notifier) .closeTab(selectedTabId!); + + if (context.mounted) { + ui_helper.showTabUndoClose( + context, + ref.read(tabRepositoryProvider.notifier).undoClose, + ); + } }, leadingIcon: const Icon(Icons.close), child: const Text('Close Tab'), diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/tab_preview.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/tab_preview.dart index c21cb24f..ad7ecb91 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/tab_preview.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/tab_preview.dart @@ -36,6 +36,7 @@ import 'package:weblibre/features/geckoview/features/tabs/domain/providers.dart' import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/container.dart'; import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart'; import 'package:weblibre/presentation/hooks/menu_controller.dart'; +import 'package:weblibre/utils/ui_helper.dart' as ui_helper; class TabContainer extends StatelessWidget { final bool isActive; @@ -305,6 +306,13 @@ class SingleTabPreview extends HookConsumerWidget { onHorizontalDragEnd: (details) async { if (draggedDistance.value >= deleteThreshold) { await ref.read(tabRepositoryProvider.notifier).closeTab(tabId); + + if (context.mounted) { + ui_helper.showTabUndoClose( + context, + ref.read(tabRepositoryProvider.notifier).undoClose, + ); + } } draggedDistance.value = 0.0; @@ -335,9 +343,17 @@ class SingleTabPreview extends HookConsumerWidget { .read(tabDataRepositoryProvider.notifier) .getContainerTabId(tabId); - await ref + final count = await ref .read(tabDataRepositoryProvider.notifier) .closeAllTabsByHost(containerId, host); + + if (context.mounted) { + ui_helper.showTabUndoClose( + context, + ref.read(tabRepositoryProvider.notifier).undoClose, + count: count, + ); + } }, // onDoubleTap: () { // ref.read(overlayDialogControllerProvider.notifier).show( @@ -350,6 +366,13 @@ class SingleTabPreview extends HookConsumerWidget { // }, onDelete: () async { await ref.read(tabRepositoryProvider.notifier).closeTab(tabId); + + if (context.mounted) { + ui_helper.showTabUndoClose( + context, + ref.read(tabRepositoryProvider.notifier).undoClose, + ); + } }, ), ), @@ -510,41 +533,20 @@ class TabTreePreview extends HookConsumerWidget { // ); // }, onDelete: () async { - final result = (entity.totalTabs > 1) - ? await showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - title: const Text('Close Tab Tree'), - content: Text( - 'Are you sure you want to close the entire tree containing ${entity.totalTabs} tabs?', - ), - actions: [ - TextButton( - onPressed: () { - Navigator.pop(context, false); - }, - child: const Text('Cancel'), - ), - TextButton( - onPressed: () { - Navigator.pop(context, true); - }, - child: const Text('Close'), - ), - ], - ); - }, - ) - : true; + final tabs = await ref.read( + tabDescendantsProvider(entity.rootId).future, + ); - if (result == true) { - final tabs = await ref.read( - tabDescendantsProvider(entity.rootId).future, + await ref + .read(tabRepositoryProvider.notifier) + .closeTabs(tabs.keys.toList()); + + if (context.mounted) { + ui_helper.showTabUndoClose( + context, + ref.read(tabRepositoryProvider.notifier).undoClose, + count: tabs.length, ); - await ref - .read(tabRepositoryProvider.notifier) - .closeTabs(tabs.keys.toList()); } }, ), diff --git a/app/lib/features/geckoview/features/tabs/domain/repositories/tab.dart b/app/lib/features/geckoview/features/tabs/domain/repositories/tab.dart index dabf1c3d..a11e3abd 100644 --- a/app/lib/features/geckoview/features/tabs/domain/repositories/tab.dart +++ b/app/lib/features/geckoview/features/tabs/domain/repositories/tab.dart @@ -71,7 +71,7 @@ class TabDataRepository extends _$TabDataRepository { .assignOrderKey(tabId, orderKey: orderKey); } - Future closeAllTabsByContainer(String? containerId) async { + Future closeAllTabsByContainer(String? containerId) async { final tabIds = await ref .read(tabDatabaseProvider) .containerDao @@ -81,9 +81,11 @@ class TabDataRepository extends _$TabDataRepository { if (tabIds.isNotEmpty) { await ref.read(tabRepositoryProvider.notifier).closeTabs(tabIds); } + + return tabIds.length; } - Future closeAllTabsByHost(String? containerId, String host) async { + Future closeAllTabsByHost(String? containerId, String host) async { final tabs = await ref .read(tabDatabaseProvider) .containerDao @@ -98,6 +100,8 @@ class TabDataRepository extends _$TabDataRepository { if (filtered.isNotEmpty) { await ref.read(tabRepositoryProvider.notifier).closeTabs(filtered); } + + return filtered.length; } Future getContainerTabId(String tabId) { diff --git a/app/lib/utils/ui_helper.dart b/app/lib/utils/ui_helper.dart index e9b83287..b6c1be88 100644 --- a/app/lib/utils/ui_helper.dart +++ b/app/lib/utils/ui_helper.dart @@ -148,3 +148,20 @@ Future launchUrlFeedback( } } } + +void showTabUndoClose( + BuildContext context, + VoidCallback onUndo, { + int count = 1, + Duration duration = const Duration(seconds: 2), +}) { + final snackBar = SnackBar( + content: (count > 1) + ? Text('$count Tabs closed') + : const Text('Tab closed'), + action: SnackBarAction(label: 'Undo', onPressed: onUndo), + duration: duration, + ); + + ScaffoldMessenger.of(context).showSnackBar(snackBar); +} diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/components/Core.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/components/Core.kt index 9300b451..ab5dd674 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/components/Core.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/components/Core.kt @@ -51,6 +51,7 @@ import mozilla.components.feature.prompts.file.FileUploadsDirCleaner import mozilla.components.feature.readerview.ReaderViewMiddleware import mozilla.components.feature.session.HistoryDelegate import mozilla.components.feature.session.middleware.LastAccessMiddleware +import mozilla.components.feature.session.middleware.undo.UndoMiddleware import mozilla.components.feature.sitepermissions.OnDiskSitePermissionsStorage import mozilla.components.feature.webnotifications.WebNotificationFeature import mozilla.components.support.base.worker.Frequency @@ -163,7 +164,7 @@ class Core(private val context: Context, DownloadMiddleware(context, DownloadService::class.java, {false}), ThumbnailsMiddleware(thumbnailStorage), ReaderViewMiddleware(), -// UndoMiddleware(), + UndoMiddleware(), LastAccessMiddleware(), // PromptMiddleware(), SessionPrioritizationMiddleware(),