From b3d975dd2383dc6d98c7871086e74e6560461765 Mon Sep 17 00:00:00 2001 From: Fabian Freund Date: Sun, 15 Mar 2026 06:20:38 +0100 Subject: [PATCH] add bookmark contextual button and improve button availability logic --- .../domain/entities/toolbar_button_id.dart | 1 + .../domain/entities/toolbar_button_spec.dart | 6 + .../presentation/toolbar_button_registry.dart | 142 ++++++++++++++---- .../widgets/contextual_bar_buttons.dart | 14 +- 4 files changed, 129 insertions(+), 34 deletions(-) diff --git a/app/lib/features/geckoview/features/browser/features/contextual_toolbar/domain/entities/toolbar_button_id.dart b/app/lib/features/geckoview/features/browser/features/contextual_toolbar/domain/entities/toolbar_button_id.dart index 3e07e21d..d7534d10 100644 --- a/app/lib/features/geckoview/features/browser/features/contextual_toolbar/domain/entities/toolbar_button_id.dart +++ b/app/lib/features/geckoview/features/browser/features/contextual_toolbar/domain/entities/toolbar_button_id.dart @@ -22,6 +22,7 @@ enum ToolbarButtonId { back, forward, bookmarks, + bookmarkToggle, share, addTab, tabsCount, diff --git a/app/lib/features/geckoview/features/browser/features/contextual_toolbar/domain/entities/toolbar_button_spec.dart b/app/lib/features/geckoview/features/browser/features/contextual_toolbar/domain/entities/toolbar_button_spec.dart index 5c792eb7..8488f713 100644 --- a/app/lib/features/geckoview/features/browser/features/contextual_toolbar/domain/entities/toolbar_button_spec.dart +++ b/app/lib/features/geckoview/features/browser/features/contextual_toolbar/domain/entities/toolbar_button_spec.dart @@ -51,6 +51,11 @@ const bookmarksToolbarButtonSpec = ToolbarButtonSpec( defaultVisible: false, ); +const bookmarkToggleToolbarButtonSpec = ToolbarButtonSpec( + id: ToolbarButtonId.bookmarkToggle, + defaultVisible: false, +); + const shareToolbarButtonSpec = ToolbarButtonSpec( id: ToolbarButtonId.share, defaultVisible: false, @@ -157,6 +162,7 @@ const toolbarButtonSpecs = [ backToolbarButtonSpec, forwardToolbarButtonSpec, bookmarksToolbarButtonSpec, + bookmarkToggleToolbarButtonSpec, shareToolbarButtonSpec, addTabToolbarButtonSpec, tabsCountToolbarButtonSpec, 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 b291c2c2..66a4b8b2 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 @@ -125,10 +125,19 @@ final List toolbarButtonRegistry = [ longPressActions: ['Add Bookmark', 'Remove Bookmark'], builder: (scope, context, ref) => _BookmarkToolbarButton(scope: scope), ), + ToolbarButtonDefinition( + spec: bookmarkToggleToolbarButtonSpec, + label: 'Bookmark', + icon: Icons.bookmark_border, + longPressActions: ['Open Bookmarks'], + builder: (scope, context, ref) => + _BookmarkToggleToolbarButton(scope: scope), + ), ToolbarButtonDefinition( spec: shareToolbarButtonSpec, label: 'Share', icon: Icons.share, + isPrimaryAvailable: (scope, ref) => scope.selectedTabId != null, builder: (scope, context, ref) => scope.isPreview ? ShareMenuButtonView(onPressed: () {}) : ShareMenuButton(selectedTabId: scope.selectedTabId), @@ -190,6 +199,7 @@ final List toolbarButtonRegistry = [ label: 'Reload', icon: Icons.refresh, longPressActions: ['Hard Refresh (bypass cache)'], + isPrimaryAvailable: (scope, ref) => scope.selectedTabId != null, builder: (scope, context, ref) => _ReloadToolbarButton(scope: scope), ), ToolbarButtonDefinition( @@ -227,6 +237,7 @@ final List toolbarButtonRegistry = [ spec: desktopToolbarButtonSpec, label: 'Desktop Site', icon: Icons.desktop_windows, + isPrimaryAvailable: (scope, ref) => scope.selectedTabId != null, builder: (scope, context, ref) { if (scope.isPreview) { return IconButton( @@ -243,6 +254,10 @@ final List toolbarButtonRegistry = [ icon: Icons.translate, longPressActions: ['Show Translation Options'], isPrimaryAvailable: (scope, ref) { + if (scope.selectedTabId == null) { + return false; + } + final engineState = ref.read(translationEngineStateProvider); final readerActive = scope.tabState?.readerableState.active ?? false; return !readerActive && engineState?.isEngineSupported == true; @@ -258,6 +273,7 @@ final List toolbarButtonRegistry = [ spec: findInPageToolbarButtonSpec, label: 'Find in Page', icon: Icons.search, + isPrimaryAvailable: (scope, ref) => scope.selectedTabId != null, builder: (scope, context, ref) { return IconButton( onPressed: scope.isPreview @@ -277,6 +293,7 @@ final List toolbarButtonRegistry = [ label: 'Close Tab', icon: MdiIcons.tabMinus, longPressActions: ['Close Others', 'Close from Same Host'], + isPrimaryAvailable: (scope, ref) => scope.selectedTabId != null, builder: (scope, context, ref) => _CloseTabToolbarButton(scope: scope), ), ToolbarButtonDefinition( @@ -319,6 +336,7 @@ final List toolbarButtonRegistry = [ 'Clone as Private', 'Clone as Isolated', ], + isPrimaryAvailable: (scope, ref) => scope.selectedTabId != null, builder: (scope, context, ref) { return scope.isPreview ? CloneTabButtonView(onPressed: () {}, onLongPress: () {}) @@ -367,6 +385,7 @@ final List toolbarButtonRegistry = [ label: 'Page Up', icon: MdiIcons.chevronDoubleUp, longPressActions: ['Scroll to Top'], + isPrimaryAvailable: (scope, ref) => scope.selectedTabId != null, builder: (scope, context, ref) { return IconButton( onPressed: scope.isPreview @@ -398,6 +417,7 @@ final List toolbarButtonRegistry = [ label: 'Page Down', icon: MdiIcons.chevronDoubleDown, longPressActions: ['Scroll to Bottom'], + isPrimaryAvailable: (scope, ref) => scope.selectedTabId != null, builder: (scope, context, ref) { return IconButton( onPressed: scope.isPreview @@ -643,32 +663,10 @@ class _BookmarkToolbarButton extends HookConsumerWidget { final tabUrl = scope.tabState?.url; final bookmarkable = tabUrl != null && !scope.isPreview; - // Walk the in-memory bookmark tree to find GUIDs for the current URL. final existingGuids = ref.watch( - bookmarksRepositoryProvider.select((async) { - final result = []; - - if (!bookmarkable) return const []; - - final root = async.value; - if (root == null) return const []; - - void collect(BookmarkItem item) { - if (item is BookmarkEntry && item.url == tabUrl) { - result.add(item.guid); - } - - if (item is BookmarkFolder) { - for (final child in item.children ?? const []) { - collect(child); - } - } - } - - collect(root); - - return result; - }), + bookmarksRepositoryProvider.select( + (async) => _bookmarkGuidsForUrl(async.value, tabUrl, bookmarkable), + ), ); final isBookmarked = existingGuids.isNotEmpty; @@ -736,6 +734,94 @@ class _BookmarkToolbarButton extends HookConsumerWidget { } } +class _BookmarkToggleToolbarButton extends ConsumerWidget { + final ContextualToolbarScope scope; + + const _BookmarkToggleToolbarButton({required this.scope}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final tabUrl = scope.tabState?.url; + final bookmarkable = tabUrl != null && !scope.isPreview; + final existingGuids = ref.watch( + bookmarksRepositoryProvider.select( + (async) => _bookmarkGuidsForUrl(async.value, tabUrl, bookmarkable), + ), + ); + final isBookmarked = existingGuids.isNotEmpty; + + return IconButton( + tooltip: isBookmarked ? 'Remove bookmark' : 'Add bookmark', + onPressed: scope.isPreview + ? () {} + : !bookmarkable + ? null + : () async { + if (isBookmarked) { + for (final guid in existingGuids) { + await ref + .read(bookmarksRepositoryProvider.notifier) + .delete(guid); + } + + if (context.mounted) { + ui_helper.showInfoMessage(context, 'Bookmark removed'); + } + + return; + } + + await ref + .read(bookmarksRepositoryProvider.notifier) + .addBookmark( + parentGuid: BookmarkRoot.mobile.id, + url: tabUrl, + title: scope.tabState!.titleOrAuthority, + ); + + if (context.mounted) { + ui_helper.showInfoMessage(context, 'Bookmark added'); + } + }, + onLongPress: scope.isPreview + ? null + : () async { + await BookmarkListRoute( + entryGuid: BookmarkRoot.root.id, + ).push(context); + }, + icon: Icon(isBookmarked ? Icons.bookmark : Icons.bookmark_border), + ); + } +} + +List _bookmarkGuidsForUrl( + BookmarkItem? root, + Uri? tabUrl, + bool bookmarkable, +) { + final result = []; + + if (!bookmarkable || root == null || tabUrl == null) { + return result; + } + + void collect(BookmarkItem item) { + if (item is BookmarkEntry && item.url == tabUrl) { + result.add(item.guid); + } + + if (item is BookmarkFolder) { + for (final child in item.children ?? const []) { + collect(child); + } + } + } + + collect(root); + return result; +} + Future _adjustFontSize( BuildContext context, WidgetRef ref, { @@ -814,9 +900,9 @@ class _DesktopModeToolbarButton extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { if (selectedTabId == null) { - return IconButton( - onPressed: () {}, - icon: const Icon(Icons.desktop_windows), + return const IconButton( + onPressed: null, + icon: Icon(Icons.desktop_windows), ); } diff --git a/app/lib/features/geckoview/features/browser/features/contextual_toolbar/presentation/widgets/contextual_bar_buttons.dart b/app/lib/features/geckoview/features/browser/features/contextual_toolbar/presentation/widgets/contextual_bar_buttons.dart index 5db45989..963c89ff 100644 --- a/app/lib/features/geckoview/features/browser/features/contextual_toolbar/presentation/widgets/contextual_bar_buttons.dart +++ b/app/lib/features/geckoview/features/browser/features/contextual_toolbar/presentation/widgets/contextual_bar_buttons.dart @@ -47,12 +47,14 @@ class ShareMenuButton extends StatelessWidget { @override Widget build(BuildContext context) { return ShareMenuButtonView( - onPressed: () async { - final tabId = selectedTabId; - if (tabId != null) { - await showShareBottomSheet(context, selectedTabId: tabId); - } - }, + onPressed: selectedTabId == null + ? null + : () async { + await showShareBottomSheet( + context, + selectedTabId: selectedTabId!, + ); + }, ); } }