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