From e609eae25cd7e08286c05154490ffd6573c91c3b Mon Sep 17 00:00:00 2001 From: Fabian Freund Date: Sat, 7 Mar 2026 10:12:18 +0100 Subject: [PATCH] added bookmark search module --- .../bookmarks/domain/providers/bookmarks.dart | 23 +++++ .../domain/providers/bookmarks.g.dart | 53 ++++++++++++ .../domain/providers/search_modules_view.dart | 2 +- .../search/presentation/screens/search.dart | 39 ++++++++- .../presentation/widgets/search_field.dart | 4 +- .../search_modules/bookmark_search.dart | 84 +++++++++++++++++++ 6 files changed, 200 insertions(+), 5 deletions(-) create mode 100644 app/lib/features/geckoview/features/search/presentation/widgets/search_modules/bookmark_search.dart diff --git a/app/lib/features/geckoview/features/bookmarks/domain/providers/bookmarks.dart b/app/lib/features/geckoview/features/bookmarks/domain/providers/bookmarks.dart index 07d32420..f534d067 100644 --- a/app/lib/features/geckoview/features/bookmarks/domain/providers/bookmarks.dart +++ b/app/lib/features/geckoview/features/bookmarks/domain/providers/bookmarks.dart @@ -125,6 +125,29 @@ class BookmarksSearch extends _$BookmarksSearch { } } +@Riverpod() +class BookmarkSearchResults extends _$BookmarkSearchResults { + final _service = GeckoBookmarksService(); + + Future search(String query, {int limit = 10}) async { + if (query.isEmpty) { + state = []; + return; + } + + final results = await _service.searchBookmarks(query, limit: limit); + state = results + .map(BookmarkItem.parseRecursive) + .whereType() + .toList(); + } + + @override + List build() { + return []; + } +} + @Riverpod() AsyncValue bookmarks( Ref ref, diff --git a/app/lib/features/geckoview/features/bookmarks/domain/providers/bookmarks.g.dart b/app/lib/features/geckoview/features/bookmarks/domain/providers/bookmarks.g.dart index e1a2f409..65209475 100644 --- a/app/lib/features/geckoview/features/bookmarks/domain/providers/bookmarks.g.dart +++ b/app/lib/features/geckoview/features/bookmarks/domain/providers/bookmarks.g.dart @@ -53,6 +53,59 @@ abstract class _$BookmarksSearch extends $StreamNotifier> { } } +@ProviderFor(BookmarkSearchResults) +final bookmarkSearchResultsProvider = BookmarkSearchResultsProvider._(); + +final class BookmarkSearchResultsProvider + extends $NotifierProvider> { + BookmarkSearchResultsProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'bookmarkSearchResultsProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$bookmarkSearchResultsHash(); + + @$internal + @override + BookmarkSearchResults create() => BookmarkSearchResults(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(List value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider>(value), + ); + } +} + +String _$bookmarkSearchResultsHash() => + r'f1413466db26c1e3407501453b8cec97504a771d'; + +abstract class _$BookmarkSearchResults extends $Notifier> { + List build(); + @$mustCallSuper + @override + void runBuild() { + final ref = this.ref as $Ref, List>; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, List>, + List, + Object?, + Object? + >; + element.handleCreate(ref, build); + } +} + @ProviderFor(bookmarks) final bookmarksProvider = BookmarksFamily._(); diff --git a/app/lib/features/geckoview/features/search/domain/providers/search_modules_view.dart b/app/lib/features/geckoview/features/search/domain/providers/search_modules_view.dart index c641b302..afa89886 100644 --- a/app/lib/features/geckoview/features/search/domain/providers/search_modules_view.dart +++ b/app/lib/features/geckoview/features/search/domain/providers/search_modules_view.dart @@ -21,7 +21,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'search_modules_view.g.dart'; -enum SearchModuleType { tabs, articles, history } +enum SearchModuleType { tabs, articles, bookmarks, history } enum SearchModuleDisplayState { preview, expanded, collapsed } diff --git a/app/lib/features/geckoview/features/search/presentation/screens/search.dart b/app/lib/features/geckoview/features/search/presentation/screens/search.dart index 367b5ca8..cba826e5 100644 --- a/app/lib/features/geckoview/features/search/presentation/screens/search.dart +++ b/app/lib/features/geckoview/features/search/presentation/screens/search.dart @@ -36,6 +36,7 @@ import 'package:weblibre/features/geckoview/features/browser/domain/providers.da import 'package:weblibre/features/geckoview/features/search/presentation/widgets/animated_tab_type_switcher.dart'; import 'package:weblibre/features/geckoview/features/search/presentation/widgets/clipboard_fill.dart'; import 'package:weblibre/features/geckoview/features/search/presentation/widgets/search_field.dart'; +import 'package:weblibre/features/geckoview/features/search/presentation/widgets/search_modules/bookmark_search.dart'; import 'package:weblibre/features/geckoview/features/search/presentation/widgets/search_modules/feed_search.dart'; import 'package:weblibre/features/geckoview/features/search/presentation/widgets/search_modules/full_search_suggestions.dart'; import 'package:weblibre/features/geckoview/features/search/presentation/widgets/search_modules/history_suggestions.dart'; @@ -381,8 +382,7 @@ class SearchScreen extends HookConsumerWidget { focusNode: searchFocusNode, maxLines: isEditMode ? 3 : 1, autofocus: true, - label: const Text('Address / Search'), - hint: const Text('Search or type URL'), + label: const Text('Search or enter URL'), unfocusOnTapOutside: false, onSubmitted: (value) async { if (value.isNotEmpty) { @@ -491,6 +491,41 @@ class SearchScreen extends HookConsumerWidget { domain: isEditMode ? existingTabState.url.host : null, ), TabSearch(searchTextListenable: sampledSearchText), + BookmarkSearch( + searchTextListenable: sampledSearchText, + onUriSelected: (uri) async { + if (isEditMode) { + await ref + .read(tabSessionProvider(tabId: tabId).notifier) + .loadUrl(url: uri); + } else { + await ref + .read(tabRepositoryProvider.notifier) + .addTab( + url: uri, + tabMode: effectiveTabMode, + parentId: (selectedTabType.value == TabType.child) + ? ref.read(selectedTabProvider) + : null, + launchedFromIntent: launchedFromIntent, + selectTab: true, + containerSelection: selectedContainer == null + ? const TabContainerSelection.unassigned() + : TabContainerSelection.specific( + selectedContainer, + ), + ); + } + + if (context.mounted) { + ref + .read(bottomSheetControllerProvider.notifier) + .requestDismiss(); + + const BrowserRoute().go(context); + } + }, + ), FeedSearch(searchTextNotifier: sampledSearchText), HistorySuggestions( searchTextListenable: sampledSearchText, diff --git a/app/lib/features/geckoview/features/search/presentation/widgets/search_field.dart b/app/lib/features/geckoview/features/search/presentation/widgets/search_field.dart index 38b893c8..6d0c2160 100644 --- a/app/lib/features/geckoview/features/search/presentation/widgets/search_field.dart +++ b/app/lib/features/geckoview/features/search/presentation/widgets/search_field.dart @@ -53,7 +53,7 @@ class SearchField extends HookConsumerWidget { required this.onSubmitted, required this.activeBang, required this.showSuggestions, - this.label, + required this.label, this.focusNode, this.maxLines = 1, this.minLines = 1, @@ -121,7 +121,7 @@ class SearchField extends HookConsumerWidget { child: UrlIcon([activeBang!.getDefaultUrl()], iconSize: 24.0), ) : null, - label: label ?? const Text('Search'), + label: label, hint: hint, floatingLabelBehavior: FloatingLabelBehavior.always, suffixIcon: hasText diff --git a/app/lib/features/geckoview/features/search/presentation/widgets/search_modules/bookmark_search.dart b/app/lib/features/geckoview/features/search/presentation/widgets/search_modules/bookmark_search.dart new file mode 100644 index 00000000..a010daa2 --- /dev/null +++ b/app/lib/features/geckoview/features/search/presentation/widgets/search_modules/bookmark_search.dart @@ -0,0 +1,84 @@ +/* + * 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/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:weblibre/features/geckoview/features/bookmarks/domain/providers/bookmarks.dart'; +import 'package:weblibre/features/geckoview/features/search/domain/providers/search_modules_view.dart'; +import 'package:weblibre/features/geckoview/features/search/presentation/widgets/search_modules/search_module_section.dart'; +import 'package:weblibre/presentation/widgets/uri_breadcrumb.dart'; +import 'package:weblibre/presentation/widgets/url_icon.dart'; + +class BookmarkSearch extends HookConsumerWidget { + final ValueListenable searchTextListenable; + final void Function(Uri uri) onUriSelected; + + const BookmarkSearch({ + super.key, + required this.searchTextListenable, + required this.onUriSelected, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final bookmarkResults = ref.watch(bookmarkSearchResultsProvider); + final totalResults = bookmarkResults.length; + + useOnListenableChange(searchTextListenable, () async { + await ref + .read(bookmarkSearchResultsProvider.notifier) + .search(searchTextListenable.value.text); + }); + + if (bookmarkResults.isEmpty) { + return const SliverToBoxAdapter(child: SizedBox.shrink()); + } + + return SearchModuleSection( + title: 'Bookmarks', + moduleType: SearchModuleType.bookmarks, + totalCount: totalResults, + contentSliverBuilder: + ({required bool isCollapsed, required int visibleCount}) => [ + if (!isCollapsed) + SliverList.builder( + itemCount: visibleCount, + itemBuilder: (context, index) { + final bookmark = bookmarkResults[index]; + + return ListTile( + leading: RepaintBoundary( + child: UrlIcon([bookmark.url], iconSize: 24), + ), + title: Text( + bookmark.title, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + subtitle: UriBreadcrumb(uri: bookmark.url), + onTap: () => onUriSelected(bookmark.url), + ); + }, + ), + ], + ); + } +}