diff --git a/app/lib/features/geckoview/features/browser/features/contextual_toolbar/presentation/toolbar_button_registry.dart b/app/lib/features/geckoview/features/browser/features/contextual_toolbar/presentation/toolbar_button_registry.dart index 6d3d457f..d4f02d12 100644 --- a/app/lib/features/geckoview/features/browser/features/contextual_toolbar/presentation/toolbar_button_registry.dart +++ b/app/lib/features/geckoview/features/browser/features/contextual_toolbar/presentation/toolbar_button_registry.dart @@ -36,6 +36,7 @@ import 'package:weblibre/features/geckoview/features/browser/domain/entities/fon import 'package:weblibre/features/geckoview/features/browser/features/contextual_toolbar/domain/entities/toolbar_button_spec.dart'; import 'package:weblibre/features/geckoview/features/browser/features/contextual_toolbar/presentation/models/contextual_toolbar_scope.dart'; import 'package:weblibre/features/geckoview/features/browser/features/contextual_toolbar/presentation/widgets/contextual_bar_buttons.dart'; +import 'package:weblibre/features/geckoview/features/browser/presentation/utils/tab_close_confirmation.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/extension_shortcut_menu.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/font_size_bottom_sheet.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/navigation_buttons.dart'; @@ -542,9 +543,7 @@ class _CloseTabToolbarButton extends HookConsumerWidget { .where((id) => id != scope.selectedTabId) .toList(); if (otherIds.isNotEmpty) { - await ref - .read(tabRepositoryProvider.notifier) - .closeTabs(otherIds); + await closeTabsWithConfirmation(context, ref, otherIds); } }, child: const Text('Close Others'), @@ -559,9 +558,7 @@ class _CloseTabToolbarButton extends HookConsumerWidget { .map((e) => e.key) .toList(); if (sameHostIds.isNotEmpty) { - await ref - .read(tabRepositoryProvider.notifier) - .closeTabs(sameHostIds); + await closeTabsWithConfirmation(context, ref, sameHostIds); } }, child: const Text('Close from Same Host'), diff --git a/app/lib/features/geckoview/features/browser/presentation/utils/tab_close_confirmation.dart b/app/lib/features/geckoview/features/browser/presentation/utils/tab_close_confirmation.dart new file mode 100644 index 00000000..124de475 --- /dev/null +++ b/app/lib/features/geckoview/features/browser/presentation/utils/tab_close_confirmation.dart @@ -0,0 +1,86 @@ +/* + * 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'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart'; +import 'package:weblibre/features/geckoview/domain/repositories/tab.dart'; +import 'package:weblibre/utils/ui_helper.dart' as ui_helper; + +int countIsolatedGroupsRemovedByClosingTabs( + WidgetRef ref, + Iterable tabIds, +) { + final idsToClose = tabIds.toSet(); + if (idsToClose.isEmpty) return 0; + + final allStates = ref.read(tabStatesProvider); + final isolatedCloseCounts = {}; + + for (final tabId in idsToClose) { + final contextId = allStates[tabId]?.isolationContextId; + if (contextId != null) { + isolatedCloseCounts.update( + contextId, + (count) => count + 1, + ifAbsent: () => 1, + ); + } + } + + return isolatedCloseCounts.entries.where((entry) { + final totalInGroup = allStates.values + .where((state) => state.isolationContextId == entry.key) + .length; + return totalInGroup == entry.value; + }).length; +} + +Future confirmBulkTabCloseIfNeeded( + BuildContext context, + WidgetRef ref, + Iterable tabIds, +) async { + final groupsToDelete = countIsolatedGroupsRemovedByClosingTabs(ref, tabIds); + if (groupsToDelete <= 0) return true; + if (!context.mounted) return false; + + return ui_helper.confirmIsolatedTabClose(context, groupCount: groupsToDelete); +} + +Future closeTabsWithConfirmation( + BuildContext context, + WidgetRef ref, + Iterable tabIds, +) async { + final idsToClose = tabIds.toSet().toList(); + if (idsToClose.isEmpty) return false; + + final tabRepository = ref.read(tabRepositoryProvider.notifier); + + if (!await confirmBulkTabCloseIfNeeded(context, ref, idsToClose)) { + return false; + } + if (!context.mounted) { + return false; + } + + await tabRepository.closeTabs(idsToClose); + return true; +} diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/browser_menu_sheet.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/browser_menu_sheet.dart index d9555ed3..a0ed08a9 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/browser_menu_sheet.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/browser_menu_sheet.dart @@ -48,6 +48,7 @@ import 'package:weblibre/features/geckoview/domain/providers/web_extensions_stat import 'package:weblibre/features/geckoview/domain/repositories/tab.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/dialogs/content_selection_dialog.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/dialogs/qr_code.dart'; +import 'package:weblibre/features/geckoview/features/browser/presentation/utils/tab_close_confirmation.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/extension_badge_icon.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/history_menu.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/translation_bottom_sheet.dart'; @@ -319,9 +320,7 @@ class _NavigationRow extends HookConsumerWidget { .where((id) => id != selectedTabId) .toList(); if (otherIds.isNotEmpty) { - await ref - .read(tabRepositoryProvider.notifier) - .closeTabs(otherIds); + await closeTabsWithConfirmation(context, ref, otherIds); } if (context.mounted) { Navigator.pop(context); @@ -339,9 +338,7 @@ class _NavigationRow extends HookConsumerWidget { .map((e) => e.key) .toList(); if (sameHostIds.isNotEmpty) { - await ref - .read(tabRepositoryProvider.notifier) - .closeTabs(sameHostIds); + await closeTabsWithConfirmation(context, ref, sameHostIds); } if (context.mounted) { Navigator.pop(context); diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/tab_view/tab_tree_view.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/tab_view/tab_tree_view.dart index dfad59e4..8ee359fc 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/tab_view/tab_tree_view.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/tab_view/tab_tree_view.dart @@ -29,6 +29,7 @@ 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_view_controllers.dart'; +import 'package:weblibre/features/geckoview/features/browser/presentation/utils/tab_close_confirmation.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/tab_preview.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/tab_view_header.dart'; import 'package:weblibre/features/geckoview/features/browser/utils/grid_calculations.dart'; @@ -139,32 +140,14 @@ class _TabTreePreview extends HookConsumerWidget { final tabs = await ref .read(tabDataRepositoryProvider.notifier) .getTabDescendants(entity.rootId); + if (!context.mounted) return; - final allStates = ref.read(tabStatesProvider); - final isolatedContextInCloseSet = {}; - - for (final tabId in tabs.keys) { - final contextId = allStates[tabId]?.isolationContextId; - if (contextId == null) continue; - isolatedContextInCloseSet[contextId] = - (isolatedContextInCloseSet[contextId] ?? 0) + 1; - } - - final groupsToDelete = isolatedContextInCloseSet.entries.where(( - entry, - ) { - final totalInGroup = allStates.values - .where((state) => state.isolationContextId == entry.key) - .length; - return totalInGroup == entry.value; - }).length; - - if (groupsToDelete > 0 && context.mounted) { - final confirmed = await ui_helper.confirmIsolatedTabClose( - context, - groupCount: groupsToDelete, - ); - if (!confirmed) return; + if (!await confirmBulkTabCloseIfNeeded( + context, + ref, + tabs.keys, + )) { + return; } await ref diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/tab_view/tab_view_header.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/tab_view/tab_view_header.dart index 9f1b6a4f..afcbd613 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/tab_view/tab_view_header.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/tab_view/tab_view_header.dart @@ -41,6 +41,7 @@ import 'package:weblibre/features/geckoview/features/browser/domain/services/bro import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/tab_view_controllers.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/dialogs/bookmark_all_dialog.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/dialogs/select_folder_dialog.dart'; +import 'package:weblibre/features/geckoview/features/browser/presentation/utils/tab_close_confirmation.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/dialogs/clear_container_data_dialog.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/dialogs/close_all_private_tabs_dialog.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/dialogs/close_all_tabs_dialog.dart'; @@ -846,34 +847,17 @@ class TabViewHeader extends HookConsumerWidget { .getFilteredTabIds( selectedContainerId, ); + if (!context.mounted) return; if (filteredIds.isEmpty) return; // Check for isolated tabs - final allStates = ref.read( - tabStatesProvider, - ); - final isolatedContextIds = filteredIds - .map((id) => allStates[id]) - .where( - (s) => - s != null && - s.tabMode - is IsolatedTabMode && - s.isolationContextId != null, - ) - .map((s) => s!.isolationContextId!) - .toSet(); - - if (isolatedContextIds.isNotEmpty && - context.mounted) { - final confirmed = await ui_helper - .confirmIsolatedTabClose( - context, - groupCount: - isolatedContextIds.length, - ); - if (!confirmed) return; + if (!await confirmBulkTabCloseIfNeeded( + context, + ref, + filteredIds, + )) { + return; } await ref