refactor tab close dialog

This commit is contained in:
Fabian Freund
2026-03-12 11:13:48 +01:00
parent e4f5a62d9a
commit ee6520f6a5
5 changed files with 108 additions and 61 deletions
@@ -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'),
@@ -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 <http://www.gnu.org/licenses/>.
*/
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<String> tabIds,
) {
final idsToClose = tabIds.toSet();
if (idsToClose.isEmpty) return 0;
final allStates = ref.read(tabStatesProvider);
final isolatedCloseCounts = <String, int>{};
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<bool> confirmBulkTabCloseIfNeeded(
BuildContext context,
WidgetRef ref,
Iterable<String> 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<bool> closeTabsWithConfirmation(
BuildContext context,
WidgetRef ref,
Iterable<String> 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;
}
@@ -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);
@@ -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 = <String, int>{};
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
@@ -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