From 02db40d74ce5f6867c5c7d5ced441baa242e436b Mon Sep 17 00:00:00 2001 From: Fabian Freund Date: Thu, 12 Mar 2026 08:37:21 +0100 Subject: [PATCH] add long press menus to bottom sheet --- .../widgets/browser_menu_sheet.dart | 244 +++++++++++++----- 1 file changed, 177 insertions(+), 67 deletions(-) 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 a4c2fe4c..a72663c4 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 @@ -49,6 +49,7 @@ 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/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'; import 'package:weblibre/features/geckoview/features/find_in_page/presentation/controllers/find_in_page.dart'; import 'package:weblibre/features/geckoview/features/open_link_tools/domain/services/url_cleaner_catalog_service.dart'; @@ -72,6 +73,7 @@ import 'package:weblibre/features/user/domain/providers.dart'; import 'package:weblibre/features/user/domain/repositories/general_settings.dart'; import 'package:weblibre/presentation/controllers/website_title.dart'; import 'package:weblibre/presentation/hooks/cached_future.dart'; +import 'package:weblibre/presentation/hooks/menu_controller.dart'; import 'package:weblibre/presentation/icons/tor_icons.dart'; import 'package:weblibre/utils/exit_app.dart'; import 'package:weblibre/utils/ui_helper.dart' as ui_helper; @@ -224,84 +226,190 @@ class _NavigationRow extends HookConsumerWidget { final history = ref.watch( tabStateProvider(selectedTabId).select((value) => value?.historyState), ); + final host = ref.watch( + tabStateProvider(selectedTabId).select((value) => value?.url.host), + ); final isLoading = ref.watch( tabStateProvider( selectedTabId, ).select((state) => state?.isLoading ?? false), ); + final isReaderActive = ref.watch( + tabStateProvider( + selectedTabId, + ).select((state) => state?.readerableState.active ?? false), + ); + + final backMenuController = useMenuController(); + final forwardMenuController = useMenuController(); + final closeMenuController = useMenuController(); + final reloadMenuController = useMenuController(); return Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - _buildNavIcon( - icon: isLoading ? Icons.close : Icons.arrow_back, - label: isLoading ? 'Stop' : 'Back', - disabled: !isLoading && history?.canGoBack != true, - onTap: () async { - final controller = ref.read( - tabSessionProvider(tabId: selectedTabId).notifier, - ); - if (isLoading) { - await controller.stopLoading(); - } else { - await controller.goBack(); - } - if (context.mounted) Navigator.pop(context); - }, - ), - _buildNavIcon( - icon: Icons.arrow_forward, - label: 'Forward', - disabled: history?.canGoForward != true, - onTap: () async { - await ref - .read(tabSessionProvider(tabId: selectedTabId).notifier) - .goForward(); - if (context.mounted) Navigator.pop(context); - }, - ), - _buildNavIcon( - icon: MdiIcons.tabMinus, - label: 'Close Tab', - onTap: () async { - final tabState = ref.read(tabStateProvider(selectedTabId)); - if (tabState != null && tabState.tabMode is IsolatedTabMode) { - final allStates = ref.read(tabStatesProvider); - final groupCount = allStates.values - .where( - (s) => s.isolationContextId == tabState.isolationContextId, - ) - .length; - if (groupCount <= 1 && context.mounted) { - final confirmed = await ui_helper.confirmIsolatedTabClose( - context, - ); - if (!confirmed) return; - } - } - - await ref - .read(tabRepositoryProvider.notifier) - .closeTab(selectedTabId); - - if (context.mounted) { - Navigator.pop(context); - ui_helper.showTabUndoClose( - context, - ref.read(tabRepositoryProvider.notifier).undoClose, + HistoryMenu( + selectedTabId: selectedTabId, + controller: backMenuController, + direction: HistoryMenuDirection.back, + child: _buildNavIcon( + icon: isLoading ? Icons.close : Icons.arrow_back, + label: isLoading ? 'Stop' : 'Back', + disabled: !isLoading && history?.canGoBack != true, + onTap: () async { + final controller = ref.read( + tabSessionProvider(tabId: selectedTabId).notifier, ); - } - }, + if (isLoading) { + await controller.stopLoading(); + } else if (isReaderActive) { + await ref + .read(readerableScreenControllerProvider.notifier) + .toggleReaderView(false); + } else { + await controller.goBack(); + } + if (context.mounted) Navigator.pop(context); + }, + onLongPress: isLoading || history?.canGoBack != true + ? null + : () { + if (!backMenuController.isOpen) { + backMenuController.open(); + } + }, + ), ), - _buildNavIcon( - icon: Icons.refresh, - label: 'Reload', - onTap: () async { - await ref - .read(tabSessionProvider(tabId: selectedTabId).notifier) - .reload(); - if (context.mounted) Navigator.pop(context); - }, + HistoryMenu( + selectedTabId: selectedTabId, + controller: forwardMenuController, + direction: HistoryMenuDirection.forward, + child: _buildNavIcon( + icon: Icons.arrow_forward, + label: 'Forward', + disabled: history?.canGoForward != true, + onTap: () async { + await ref + .read(tabSessionProvider(tabId: selectedTabId).notifier) + .goForward(); + if (context.mounted) Navigator.pop(context); + }, + onLongPress: history?.canGoForward != true + ? null + : () { + if (!forwardMenuController.isOpen) { + forwardMenuController.open(); + } + }, + ), + ), + MenuAnchor( + controller: closeMenuController, + builder: (context, controller, child) => child!, + menuChildren: [ + MenuItemButton( + leadingIcon: const Icon(Icons.tab), + onPressed: () async { + final tabStates = ref.read(tabStatesProvider); + final otherIds = tabStates.keys + .where((id) => id != selectedTabId) + .toList(); + if (otherIds.isNotEmpty) { + await ref + .read(tabRepositoryProvider.notifier) + .closeTabs(otherIds); + } + if (context.mounted) { + Navigator.pop(context); + } + }, + child: const Text('Close Others'), + ), + if (host != null && host.isNotEmpty) + MenuItemButton( + leadingIcon: const Icon(Icons.language), + onPressed: () async { + final tabStates = ref.read(tabStatesProvider); + final sameHostIds = tabStates.entries + .where((e) => e.value.url.host == host) + .map((e) => e.key) + .toList(); + if (sameHostIds.isNotEmpty) { + await ref + .read(tabRepositoryProvider.notifier) + .closeTabs(sameHostIds); + } + if (context.mounted) { + Navigator.pop(context); + } + }, + child: const Text('Close from Same Host'), + ), + ], + child: _buildNavIcon( + icon: MdiIcons.tabMinus, + label: 'Close Tab', + onTap: () async { + final tabState = ref.read(tabStateProvider(selectedTabId)); + if (tabState != null && tabState.tabMode is IsolatedTabMode) { + final allStates = ref.read(tabStatesProvider); + final groupCount = allStates.values + .where( + (s) => + s.isolationContextId == tabState.isolationContextId, + ) + .length; + if (groupCount <= 1 && context.mounted) { + final confirmed = await ui_helper.confirmIsolatedTabClose( + context, + ); + if (!confirmed) return; + } + } + + await ref + .read(tabRepositoryProvider.notifier) + .closeTab(selectedTabId); + + if (context.mounted) { + Navigator.pop(context); + ui_helper.showTabUndoClose( + context, + ref.read(tabRepositoryProvider.notifier).undoClose, + ); + } + }, + onLongPress: closeMenuController.open, + ), + ), + MenuAnchor( + controller: reloadMenuController, + builder: (context, controller, child) => child!, + menuChildren: [ + MenuItemButton( + leadingIcon: const Icon(Icons.refresh), + onPressed: () async { + await ref + .read(tabSessionProvider(tabId: selectedTabId).notifier) + .reload(flags: LoadUrlFlags.BYPASS_CACHE); + if (context.mounted) { + Navigator.pop(context); + } + }, + child: const Text('Hard Refresh'), + ), + ], + child: _buildNavIcon( + icon: Icons.refresh, + label: 'Reload', + onTap: () async { + await ref + .read(tabSessionProvider(tabId: selectedTabId).notifier) + .reload(); + if (context.mounted) Navigator.pop(context); + }, + onLongPress: reloadMenuController.open, + ), ), ], ); @@ -312,12 +420,14 @@ class _NavigationRow extends HookConsumerWidget { required String label, bool disabled = false, required VoidCallback onTap, + VoidCallback? onLongPress, }) { return Builder( builder: (context) { final colorScheme = Theme.of(context).colorScheme; return InkWell( onTap: disabled ? null : onTap, + onLongPress: disabled ? null : onLongPress, borderRadius: BorderRadius.circular(8), child: Padding( padding: const EdgeInsets.symmetric(