add bookmark contextual button and improve button availability logic

This commit is contained in:
Fabian Freund
2026-03-15 06:20:38 +01:00
parent 305377c0af
commit b3d975dd23
4 changed files with 129 additions and 34 deletions
@@ -22,6 +22,7 @@ enum ToolbarButtonId {
back, back,
forward, forward,
bookmarks, bookmarks,
bookmarkToggle,
share, share,
addTab, addTab,
tabsCount, tabsCount,
@@ -51,6 +51,11 @@ const bookmarksToolbarButtonSpec = ToolbarButtonSpec(
defaultVisible: false, defaultVisible: false,
); );
const bookmarkToggleToolbarButtonSpec = ToolbarButtonSpec(
id: ToolbarButtonId.bookmarkToggle,
defaultVisible: false,
);
const shareToolbarButtonSpec = ToolbarButtonSpec( const shareToolbarButtonSpec = ToolbarButtonSpec(
id: ToolbarButtonId.share, id: ToolbarButtonId.share,
defaultVisible: false, defaultVisible: false,
@@ -157,6 +162,7 @@ const toolbarButtonSpecs = [
backToolbarButtonSpec, backToolbarButtonSpec,
forwardToolbarButtonSpec, forwardToolbarButtonSpec,
bookmarksToolbarButtonSpec, bookmarksToolbarButtonSpec,
bookmarkToggleToolbarButtonSpec,
shareToolbarButtonSpec, shareToolbarButtonSpec,
addTabToolbarButtonSpec, addTabToolbarButtonSpec,
tabsCountToolbarButtonSpec, tabsCountToolbarButtonSpec,
@@ -125,10 +125,19 @@ final List<ToolbarButtonDefinition> toolbarButtonRegistry = [
longPressActions: ['Add Bookmark', 'Remove Bookmark'], longPressActions: ['Add Bookmark', 'Remove Bookmark'],
builder: (scope, context, ref) => _BookmarkToolbarButton(scope: scope), 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( ToolbarButtonDefinition(
spec: shareToolbarButtonSpec, spec: shareToolbarButtonSpec,
label: 'Share', label: 'Share',
icon: Icons.share, icon: Icons.share,
isPrimaryAvailable: (scope, ref) => scope.selectedTabId != null,
builder: (scope, context, ref) => scope.isPreview builder: (scope, context, ref) => scope.isPreview
? ShareMenuButtonView(onPressed: () {}) ? ShareMenuButtonView(onPressed: () {})
: ShareMenuButton(selectedTabId: scope.selectedTabId), : ShareMenuButton(selectedTabId: scope.selectedTabId),
@@ -190,6 +199,7 @@ final List<ToolbarButtonDefinition> toolbarButtonRegistry = [
label: 'Reload', label: 'Reload',
icon: Icons.refresh, icon: Icons.refresh,
longPressActions: ['Hard Refresh (bypass cache)'], longPressActions: ['Hard Refresh (bypass cache)'],
isPrimaryAvailable: (scope, ref) => scope.selectedTabId != null,
builder: (scope, context, ref) => _ReloadToolbarButton(scope: scope), builder: (scope, context, ref) => _ReloadToolbarButton(scope: scope),
), ),
ToolbarButtonDefinition( ToolbarButtonDefinition(
@@ -227,6 +237,7 @@ final List<ToolbarButtonDefinition> toolbarButtonRegistry = [
spec: desktopToolbarButtonSpec, spec: desktopToolbarButtonSpec,
label: 'Desktop Site', label: 'Desktop Site',
icon: Icons.desktop_windows, icon: Icons.desktop_windows,
isPrimaryAvailable: (scope, ref) => scope.selectedTabId != null,
builder: (scope, context, ref) { builder: (scope, context, ref) {
if (scope.isPreview) { if (scope.isPreview) {
return IconButton( return IconButton(
@@ -243,6 +254,10 @@ final List<ToolbarButtonDefinition> toolbarButtonRegistry = [
icon: Icons.translate, icon: Icons.translate,
longPressActions: ['Show Translation Options'], longPressActions: ['Show Translation Options'],
isPrimaryAvailable: (scope, ref) { isPrimaryAvailable: (scope, ref) {
if (scope.selectedTabId == null) {
return false;
}
final engineState = ref.read(translationEngineStateProvider); final engineState = ref.read(translationEngineStateProvider);
final readerActive = scope.tabState?.readerableState.active ?? false; final readerActive = scope.tabState?.readerableState.active ?? false;
return !readerActive && engineState?.isEngineSupported == true; return !readerActive && engineState?.isEngineSupported == true;
@@ -258,6 +273,7 @@ final List<ToolbarButtonDefinition> toolbarButtonRegistry = [
spec: findInPageToolbarButtonSpec, spec: findInPageToolbarButtonSpec,
label: 'Find in Page', label: 'Find in Page',
icon: Icons.search, icon: Icons.search,
isPrimaryAvailable: (scope, ref) => scope.selectedTabId != null,
builder: (scope, context, ref) { builder: (scope, context, ref) {
return IconButton( return IconButton(
onPressed: scope.isPreview onPressed: scope.isPreview
@@ -277,6 +293,7 @@ final List<ToolbarButtonDefinition> toolbarButtonRegistry = [
label: 'Close Tab', label: 'Close Tab',
icon: MdiIcons.tabMinus, icon: MdiIcons.tabMinus,
longPressActions: ['Close Others', 'Close from Same Host'], longPressActions: ['Close Others', 'Close from Same Host'],
isPrimaryAvailable: (scope, ref) => scope.selectedTabId != null,
builder: (scope, context, ref) => _CloseTabToolbarButton(scope: scope), builder: (scope, context, ref) => _CloseTabToolbarButton(scope: scope),
), ),
ToolbarButtonDefinition( ToolbarButtonDefinition(
@@ -319,6 +336,7 @@ final List<ToolbarButtonDefinition> toolbarButtonRegistry = [
'Clone as Private', 'Clone as Private',
'Clone as Isolated', 'Clone as Isolated',
], ],
isPrimaryAvailable: (scope, ref) => scope.selectedTabId != null,
builder: (scope, context, ref) { builder: (scope, context, ref) {
return scope.isPreview return scope.isPreview
? CloneTabButtonView(onPressed: () {}, onLongPress: () {}) ? CloneTabButtonView(onPressed: () {}, onLongPress: () {})
@@ -367,6 +385,7 @@ final List<ToolbarButtonDefinition> toolbarButtonRegistry = [
label: 'Page Up', label: 'Page Up',
icon: MdiIcons.chevronDoubleUp, icon: MdiIcons.chevronDoubleUp,
longPressActions: ['Scroll to Top'], longPressActions: ['Scroll to Top'],
isPrimaryAvailable: (scope, ref) => scope.selectedTabId != null,
builder: (scope, context, ref) { builder: (scope, context, ref) {
return IconButton( return IconButton(
onPressed: scope.isPreview onPressed: scope.isPreview
@@ -398,6 +417,7 @@ final List<ToolbarButtonDefinition> toolbarButtonRegistry = [
label: 'Page Down', label: 'Page Down',
icon: MdiIcons.chevronDoubleDown, icon: MdiIcons.chevronDoubleDown,
longPressActions: ['Scroll to Bottom'], longPressActions: ['Scroll to Bottom'],
isPrimaryAvailable: (scope, ref) => scope.selectedTabId != null,
builder: (scope, context, ref) { builder: (scope, context, ref) {
return IconButton( return IconButton(
onPressed: scope.isPreview onPressed: scope.isPreview
@@ -643,32 +663,10 @@ class _BookmarkToolbarButton extends HookConsumerWidget {
final tabUrl = scope.tabState?.url; final tabUrl = scope.tabState?.url;
final bookmarkable = tabUrl != null && !scope.isPreview; final bookmarkable = tabUrl != null && !scope.isPreview;
// Walk the in-memory bookmark tree to find GUIDs for the current URL.
final existingGuids = ref.watch( final existingGuids = ref.watch(
bookmarksRepositoryProvider.select((async) { bookmarksRepositoryProvider.select(
final result = <String>[]; (async) => _bookmarkGuidsForUrl(async.value, tabUrl, bookmarkable),
),
if (!bookmarkable) return const <String>[];
final root = async.value;
if (root == null) return const <String>[];
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 <BookmarkItem>[]) {
collect(child);
}
}
}
collect(root);
return result;
}),
); );
final isBookmarked = existingGuids.isNotEmpty; 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<String> _bookmarkGuidsForUrl(
BookmarkItem? root,
Uri? tabUrl,
bool bookmarkable,
) {
final result = <String>[];
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 <BookmarkItem>[]) {
collect(child);
}
}
}
collect(root);
return result;
}
Future<void> _adjustFontSize( Future<void> _adjustFontSize(
BuildContext context, BuildContext context,
WidgetRef ref, { WidgetRef ref, {
@@ -814,9 +900,9 @@ class _DesktopModeToolbarButton extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
if (selectedTabId == null) { if (selectedTabId == null) {
return IconButton( return const IconButton(
onPressed: () {}, onPressed: null,
icon: const Icon(Icons.desktop_windows), icon: Icon(Icons.desktop_windows),
); );
} }
@@ -47,11 +47,13 @@ class ShareMenuButton extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ShareMenuButtonView( return ShareMenuButtonView(
onPressed: () async { onPressed: selectedTabId == null
final tabId = selectedTabId; ? null
if (tabId != null) { : () async {
await showShareBottomSheet(context, selectedTabId: tabId); await showShareBottomSheet(
} context,
selectedTabId: selectedTabId!,
);
}, },
); );
} }