From 82b1cfa0d775e3958c666280943a893c9113d468 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 23 Mar 2024 20:19:34 +0600 Subject: [PATCH] feat: search history support #1236 --- lib/collections/spotube_icons.dart | 1 + lib/pages/search/search.dart | 96 +++++++++++++++++++++++++---- lib/services/kv_store/kv_store.dart | 6 ++ 3 files changed, 91 insertions(+), 12 deletions(-) diff --git a/lib/collections/spotube_icons.dart b/lib/collections/spotube_icons.dart index 6cf920851..98c8ad450 100644 --- a/lib/collections/spotube_icons.dart +++ b/lib/collections/spotube_icons.dart @@ -115,4 +115,5 @@ abstract class SpotubeIcons { static const github = SimpleIcons.github; static const openCollective = SimpleIcons.opencollective; static const anonymous = FeatherIcons.user; + static const history = FeatherIcons.clock; } diff --git a/lib/pages/search/search.dart b/lib/pages/search/search.dart index e666c9aab..ca66e02a5 100644 --- a/lib/pages/search/search.dart +++ b/lib/pages/search/search.dart @@ -1,7 +1,9 @@ import 'dart:async'; +import 'package:collection/collection.dart'; import 'package:flutter/material.dart' hide Page; import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:fuzzywuzzy/fuzzywuzzy.dart'; import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -12,15 +14,16 @@ import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart'; import 'package:spotube/components/shared/page_window_title_bar.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; +import 'package:spotube/hooks/utils/use_force_update.dart'; import 'package:spotube/pages/search/sections/albums.dart'; import 'package:spotube/pages/search/sections/artists.dart'; import 'package:spotube/pages/search/sections/playlists.dart'; import 'package:spotube/pages/search/sections/tracks.dart'; import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/spotify/spotify.dart'; +import 'package:spotube/services/kv_store/kv_store.dart'; import 'package:spotube/utils/platform.dart'; -import 'package:collection/collection.dart'; class SearchPage extends HookConsumerWidget { const SearchPage({super.key}); @@ -29,7 +32,7 @@ class SearchPage extends HookConsumerWidget { Widget build(BuildContext context, ref) { final theme = Theme.of(context); final searchTerm = ref.watch(searchTermStateProvider); - final controller = useTextEditingController(text: searchTerm); + final controller = useSearchController(); ref.watch(AuthenticationNotifier.provider); final authenticationNotifier = @@ -45,6 +48,12 @@ class SearchPage extends HookConsumerWidget { final isFetching = queries.every((s) => s.isLoading); + useEffect(() { + controller.text = searchTerm; + + return null; + }, []); + final resultWidget = HookBuilder( builder: (context) { final controller = useScrollController(); @@ -88,24 +97,87 @@ class SearchPage extends HookConsumerWidget { vertical: 10, ), color: theme.scaffoldBackgroundColor, - child: TextField( - controller: controller, - autofocus: - queries.none((s) => s.value != null && !s.hasError) && - !kIsMobile, - decoration: InputDecoration( - prefixIcon: const Icon(SpotubeIcons.search), - hintText: "${context.l10n.search}...", - ), - onSubmitted: (value) async { + child: SearchAnchor( + searchController: controller, + viewBuilder: (_) => HookBuilder(builder: (context) { + final searchController = useListenable(controller); + final update = useForceUpdate(); + final suggestions = searchController.text.isEmpty + ? KVStoreService.recentSearches + : KVStoreService.recentSearches + .where( + (s) => + weightedRatio( + s.toLowerCase(), + searchController.text.toLowerCase(), + ) > + 50, + ) + .toList(); + + return ListView.builder( + itemCount: suggestions.length, + itemBuilder: (context, index) { + final suggestion = suggestions[index]; + + return ListTile( + leading: const Icon(SpotubeIcons.history), + title: Text(suggestion), + trailing: IconButton( + icon: const Icon(SpotubeIcons.trash), + onPressed: () { + KVStoreService.setRecentSearches( + KVStoreService.recentSearches + .where((s) => s != suggestion) + .toList(), + ); + update(); + }, + ), + onTap: () { + controller.closeView(suggestion); + ref + .read(searchTermStateProvider.notifier) + .state = suggestion; + }, + ); + }, + ); + }), + suggestionsBuilder: (context, controller) { + return []; + }, + viewOnSubmitted: (value) async { + controller.closeView(value); Timer( const Duration(milliseconds: 50), () { ref.read(searchTermStateProvider.notifier).state = value; + if (value.trim().isEmpty) { + return; + } + KVStoreService.setRecentSearches( + { + value, + ...KVStoreService.recentSearches, + }.toList(), + ); }, ); }, + builder: (context, controller) { + return SearchBar( + autoFocus: queries.none( + (s) => s.value != null && !s.hasError) && + !kIsMobile, + controller: controller, + leading: const Icon(SpotubeIcons.search), + hintText: "${context.l10n.search}...", + onTap: controller.openView, + onChanged: (_) => controller.openView(), + ); + }, ), ), Expanded( diff --git a/lib/services/kv_store/kv_store.dart b/lib/services/kv_store/kv_store.dart index 5845b1207..f94ec4ee6 100644 --- a/lib/services/kv_store/kv_store.dart +++ b/lib/services/kv_store/kv_store.dart @@ -17,4 +17,10 @@ abstract class KVStoreService { sharedPreferences.getBool('askedForBatteryOptimization') ?? false; static Future setAskedForBatteryOptimization(bool value) async => await sharedPreferences.setBool('askedForBatteryOptimization', value); + + static List get recentSearches => + sharedPreferences.getStringList('recentSearches') ?? []; + + static Future setRecentSearches(List value) async => + await sharedPreferences.setStringList('recentSearches', value); }