import 'package:flutter/services.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:fuzzywuzzy/fuzzywuzzy.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/collections/routes.gr.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/fallbacks/anonymous_fallback.dart'; import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/string.dart'; import 'package:spotube/hooks/controllers/use_shadcn_text_editing_controller.dart'; import 'package:spotube/pages/search/tabs/albums.dart'; import 'package:spotube/pages/search/tabs/all.dart'; import 'package:spotube/pages/search/tabs/artists.dart'; import 'package:spotube/pages/search/tabs/playlists.dart'; import 'package:spotube/pages/search/tabs/tracks.dart'; import 'package:spotube/provider/metadata_plugin/auth.dart'; import 'package:spotube/provider/metadata_plugin/search/all.dart'; import 'package:spotube/services/kv_store/kv_store.dart'; import 'package:auto_route/auto_route.dart'; final searchTermStateProvider = StateProvider((ref) { return ""; }); @RoutePage() class SearchPage extends HookConsumerWidget { static const name = "search"; const SearchPage({super.key}); @override Widget build(BuildContext context, ref) { final controller = useShadcnTextEditingController(); final focusNode = useFocusNode(); final authenticated = ref.watch(metadataPluginAuthenticatedProvider); final searchTerm = ref.watch(searchTermStateProvider); final searchChipSnapshot = ref.watch(metadataPluginSearchChipsProvider); final selectedChip = useState( searchChipSnapshot.asData?.value.first ?? "all", ); ref.listen( metadataPluginSearchChipsProvider, (previous, next) { selectedChip.value = next.asData?.value.first ?? "all"; }, ); useEffect(() { controller.text = searchTerm; return null; }, []); void onSubmitted(String value) { ref.read(searchTermStateProvider.notifier).state = value; if (value.trim().isEmpty) { return; } KVStoreService.setRecentSearches( { value, ...KVStoreService.recentSearches, }.toList(), ); } return PopScope( canPop: false, onPopInvokedWithResult: (didPop, result) { context.navigateTo(const HomeRoute()); }, child: SafeArea( bottom: false, child: Scaffold( headers: [ if (kTitlebarVisible) const TitleBar(automaticallyImplyLeading: false, height: 30) ], child: authenticated.asData?.value != true ? const AnonymousFallback() : Column( children: [ Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ Expanded( child: Padding( padding: const EdgeInsets.symmetric( horizontal: 20, vertical: 10, ), child: ListenableBuilder( listenable: controller, builder: (context, _) { final suggestions = controller.text.isEmpty ? KVStoreService.recentSearches : KVStoreService.recentSearches .where( (s) => weightedRatio( s.toLowerCase(), controller.text.toLowerCase(), ) > 50, ) .toList(); return KeyboardListener( focusNode: focusNode, autofocus: true, onKeyEvent: (value) { final isEnter = value.logicalKey == LogicalKeyboardKey.enter; if (isEnter) { onSubmitted(controller.text); focusNode.unfocus(); } }, child: AutoComplete( suggestions: suggestions.length <= 2 ? [ ...suggestions, "Twenty One Pilots", "Linkin Park", "d4vd" ] : suggestions, completer: (suggestion) => suggestion, mode: AutoCompleteMode.replaceAll, child: TextField( autofocus: true, controller: controller, features: [ const InputFeature.leading( Icon(SpotubeIcons.search), ), InputFeature.trailing( AnimatedCrossFade( duration: const Duration( milliseconds: 300), crossFadeState: controller .text.isNotEmpty ? CrossFadeState.showFirst : CrossFadeState.showSecond, firstChild: IconButton.ghost( size: ButtonSize.small, icon: const Icon( SpotubeIcons.close), onPressed: () { controller.clear(); }, ), secondChild: const SizedBox.square( dimension: 28), ), ) ], textInputAction: TextInputAction.search, placeholder: Text(context.l10n.search), onSubmitted: onSubmitted, ), ), ); }), ), ), ], ), Row( spacing: 8, children: [ const Gap(12), if (searchChipSnapshot.asData?.value != null) for (final chip in searchChipSnapshot.asData!.value) Chip( style: selectedChip.value == chip ? ButtonVariance.primary.copyWith( decoration: (context, states, value) { return ButtonVariance.primary .decoration(context, states) .copyWithIfBoxDecoration( borderRadius: BorderRadius.circular(100), ); }, ) : ButtonVariance.secondary.copyWith( decoration: (context, states, value) { return ButtonVariance.secondary .decoration(context, states) .copyWithIfBoxDecoration( borderRadius: BorderRadius.circular(100), ); }, ), child: Text(chip.capitalize()), onPressed: () { selectedChip.value = chip; }, ), ], ), Expanded( child: AnimatedSwitcher( duration: const Duration(milliseconds: 300), child: switch (selectedChip.value) { "tracks" => const SearchPageTracksTab(), "albums" => const SearchPageAlbumsTab(), "artists" => const SearchPageArtistsTab(), "playlists" => const SearchPagePlaylistsTab(), _ => const SearchPageAllTab(), }, ), ), ], ), ), ), ); } }