From 3ef567cc1983457c56d65b30b5cd8d31147fe5b5 Mon Sep 17 00:00:00 2001 From: Fabian Freund Date: Wed, 25 Feb 2026 15:52:44 +0100 Subject: [PATCH] refactor search suggestions --- .../full_search_suggestions.dart | 261 ++++++++++-------- 1 file changed, 151 insertions(+), 110 deletions(-) diff --git a/app/lib/features/geckoview/features/search/presentation/widgets/search_modules/full_search_suggestions.dart b/app/lib/features/geckoview/features/search/presentation/widgets/search_modules/full_search_suggestions.dart index 2925bd2d..a1f709b2 100644 --- a/app/lib/features/geckoview/features/search/presentation/widgets/search_modules/full_search_suggestions.dart +++ b/app/lib/features/geckoview/features/search/presentation/widgets/search_modules/full_search_suggestions.dart @@ -50,10 +50,11 @@ class FullSearchTermSuggestions extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final searchTextIsNotEmpty = useListenableSelector( + final searchText = useListenableSelector( searchTextController, - () => searchTextController.text.isNotEmpty, + () => searchTextController.text, ); + final searchTextIsNotEmpty = searchText.isNotEmpty; final searchSuggestions = ref.watch(searchSuggestionsProvider()); final searchHistory = ref.watch(searchHistoryProvider); @@ -65,115 +66,22 @@ class FullSearchTermSuggestions extends HookConsumerWidget { .addQuery(searchTextController.text); }); - Widget buildSuggestionChip( - String query, { - Widget? avatar, - Future Function()? onDelete, - }) { - return InkWell( - onLongPress: () { - searchTextController.text = query; - }, - child: InputChip( - avatar: avatar ?? const Icon(Icons.search), - label: Text(query), - onSelected: (value) async { - if (value) { - await submitSearch(query); - } - }, - onDeleted: onDelete == null - ? null - : () async { - await onDelete(); - }, - ), - ); - } + final showHistory = + !searchTextIsNotEmpty && (searchHistory.value.isNotEmpty); - final List suggestionChips; - - if (!searchTextIsNotEmpty && (searchHistory.value.isNotEmpty)) { - final entries = searchHistory.value!; - - suggestionChips = entries.map((entry) { - final query = entry.searchQuery; - - return buildSuggestionChip( - query, - avatar: const Icon(Icons.history), - onDelete: () async { - await ref - .read(bangDataRepositoryProvider.notifier) - .removeSearchEntry(query); - }, - ); - }).toList(); - } else { - final prioritizedSuggestions = [ - if (searchTextIsNotEmpty) searchTextController.text, - if (searchSuggestions.value != null) - ...searchSuggestions.value!.whereNot( - (suggestion) => suggestion == searchTextController.text, - ), - ]; - - suggestionChips = prioritizedSuggestions.map((query) { - return buildSuggestionChip(query); - }).toList(); - } - - final toggleButton = IconButton( - onPressed: () { - ref.read(searchSuggestionsExpandedProvider.notifier).toggle(); - }, - icon: Icon(expanded ? Icons.unfold_less : Icons.unfold_more), + final suggestionQueries = useMemoized( + () => showHistory + ? searchHistory.value!.map((e) => e.searchQuery).toList() + : [ + if (searchTextIsNotEmpty) searchText, + if (searchSuggestions.value != null) + ...searchSuggestions.value!.whereNot( + (s) => s == searchText, + ), + ], + [showHistory, searchText, searchHistory.value, searchSuggestions.value], ); - final suggestionsContent = expanded - ? Padding( - padding: const EdgeInsets.only(top: 8.0), - child: ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 150), - child: FadingScroll( - fadingSize: 25, - builder: (context, controller) { - return CustomScrollView( - shrinkWrap: true, - controller: controller, - slivers: [ - SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.only(left: 16.0), - child: Wrap(spacing: 8.0, children: suggestionChips), - ), - ), - ], - ); - }, - ), - ), - ) - : Padding( - padding: const EdgeInsets.only(left: 16.0, top: 8.0), - child: SizedBox( - height: 44, - child: FadingScroll( - fadingSize: 25, - builder: (context, controller) { - return ListView.separated( - controller: controller, - scrollDirection: Axis.horizontal, - itemCount: suggestionChips.length, - separatorBuilder: (context, index) => - const SizedBox(width: 8), - itemBuilder: (context, index) => suggestionChips[index], - ); - }, - ), - ), - ); - return MultiSliver( children: [ SliverToBoxAdapter( @@ -189,10 +97,28 @@ class FullSearchTermSuggestions extends HookConsumerWidget { child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Expanded(child: suggestionsContent), + Expanded( + child: _SuggestionsContent( + expanded: expanded, + queries: suggestionQueries, + showHistory: showHistory, + searchTextController: searchTextController, + submitSearch: submitSearch, + onDeleteHistory: (query) => ref + .read(bangDataRepositoryProvider.notifier) + .removeSearchEntry(query), + ), + ), Padding( padding: const EdgeInsets.only(top: 6.0), - child: toggleButton, + child: IconButton( + onPressed: ref + .read(searchSuggestionsExpandedProvider.notifier) + .toggle, + icon: Icon( + expanded ? Icons.unfold_less : Icons.unfold_more, + ), + ), ), ], ), @@ -201,3 +127,118 @@ class FullSearchTermSuggestions extends HookConsumerWidget { ); } } + +class _SuggestionsContent extends StatelessWidget { + final bool expanded; + final List queries; + final bool showHistory; + final TextEditingController searchTextController; + final Future Function(String query) submitSearch; + final Future Function(String query) onDeleteHistory; + + const _SuggestionsContent({ + required this.expanded, + required this.queries, + required this.showHistory, + required this.searchTextController, + required this.submitSearch, + required this.onDeleteHistory, + }); + + @override + Widget build(BuildContext context) { + if (expanded) { + return Padding( + padding: const EdgeInsets.only(top: 8.0), + child: ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 150), + child: FadingScroll( + fadingSize: 25, + builder: (context, controller) { + return CustomScrollView( + shrinkWrap: true, + controller: controller, + slivers: [ + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.only(left: 16.0), + child: Wrap( + spacing: 8.0, + children: [ + for (final query in queries) + _SuggestionChip( + query: query, + showHistory: showHistory, + searchTextController: searchTextController, + submitSearch: submitSearch, + onDeleteHistory: onDeleteHistory, + ), + ], + ), + ), + ), + ], + ); + }, + ), + ), + ); + } + + return Padding( + padding: const EdgeInsets.only(left: 16.0, top: 8.0), + child: SizedBox( + height: 44, + child: FadingScroll( + fadingSize: 25, + builder: (context, controller) { + return ListView.separated( + controller: controller, + scrollDirection: Axis.horizontal, + itemCount: queries.length, + separatorBuilder: (context, index) => const SizedBox(width: 8), + itemBuilder: (context, index) => _SuggestionChip( + query: queries[index], + showHistory: showHistory, + searchTextController: searchTextController, + submitSearch: submitSearch, + onDeleteHistory: onDeleteHistory, + ), + ); + }, + ), + ), + ); + } +} + +class _SuggestionChip extends StatelessWidget { + final String query; + final bool showHistory; + final TextEditingController searchTextController; + final Future Function(String query) submitSearch; + final Future Function(String query) onDeleteHistory; + + const _SuggestionChip({ + required this.query, + required this.showHistory, + required this.searchTextController, + required this.submitSearch, + required this.onDeleteHistory, + }); + + @override + Widget build(BuildContext context) { + return InkWell( + onLongPress: () => searchTextController.text = query, + child: InputChip( + avatar: Icon(showHistory ? Icons.history : Icons.search), + label: Text(query), + onSelected: (value) async { + if (value) await submitSearch(query); + }, + onDeleted: showHistory ? () => onDeleteHistory(query) : null, + ), + ); + } +}