refactor search suggestions

This commit is contained in:
Fabian Freund
2026-02-25 15:52:44 +01:00
parent dc5f7902ab
commit 3ef567cc19
@@ -50,10 +50,11 @@ class FullSearchTermSuggestions extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final searchTextIsNotEmpty = useListenableSelector( final searchText = useListenableSelector(
searchTextController, searchTextController,
() => searchTextController.text.isNotEmpty, () => searchTextController.text,
); );
final searchTextIsNotEmpty = searchText.isNotEmpty;
final searchSuggestions = ref.watch(searchSuggestionsProvider()); final searchSuggestions = ref.watch(searchSuggestionsProvider());
final searchHistory = ref.watch(searchHistoryProvider); final searchHistory = ref.watch(searchHistoryProvider);
@@ -65,113 +66,20 @@ class FullSearchTermSuggestions extends HookConsumerWidget {
.addQuery(searchTextController.text); .addQuery(searchTextController.text);
}); });
Widget buildSuggestionChip( final showHistory =
String query, { !searchTextIsNotEmpty && (searchHistory.value.isNotEmpty);
Widget? avatar,
Future<void> 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 List<Widget> suggestionChips; final suggestionQueries = useMemoized(
() => showHistory
if (!searchTextIsNotEmpty && (searchHistory.value.isNotEmpty)) { ? searchHistory.value!.map((e) => e.searchQuery).toList()
final entries = searchHistory.value!; : [
if (searchTextIsNotEmpty) searchText,
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) if (searchSuggestions.value != null)
...searchSuggestions.value!.whereNot( ...searchSuggestions.value!.whereNot(
(suggestion) => suggestion == searchTextController.text, (s) => s == searchText,
),
];
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 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),
),
), ),
], ],
); [showHistory, searchText, searchHistory.value, searchSuggestions.value],
},
),
),
)
: 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( return MultiSliver(
@@ -189,10 +97,28 @@ class FullSearchTermSuggestions extends HookConsumerWidget {
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ 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(
padding: const EdgeInsets.only(top: 6.0), 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<String> queries;
final bool showHistory;
final TextEditingController searchTextController;
final Future<void> Function(String query) submitSearch;
final Future<void> 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<void> Function(String query) submitSearch;
final Future<void> 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,
),
);
}
}