diff --git a/lib/modules/player/sibling_tracks_sheet.dart b/lib/modules/player/sibling_tracks_sheet.dart index fee0c46a..4e36aa09 100644 --- a/lib/modules/player/sibling_tracks_sheet.dart +++ b/lib/modules/player/sibling_tracks_sheet.dart @@ -107,7 +107,7 @@ class SiblingTracksSheet extends HookConsumerWidget { return siblingType.info; })); - final activeSourceInfo = activeTrackSource as TrackSourceInfo; + final activeSourceInfo = activeTrackSource?.info as TrackSourceInfo; return results ..removeWhere((element) => element.id == activeSourceInfo.id) @@ -127,7 +127,7 @@ class SiblingTracksSheet extends HookConsumerWidget { return siblingType.info; }), ); - final activeSourceInfo = activeTrackSource as TrackSourceInfo; + final activeSourceInfo = activeTrackSource?.info as TrackSourceInfo; return searchResults ..removeWhere((element) => element.id == activeSourceInfo.id) ..insert( diff --git a/lib/modules/search/loading.dart b/lib/modules/search/loading.dart new file mode 100644 index 00000000..8ca2820f --- /dev/null +++ b/lib/modules/search/loading.dart @@ -0,0 +1,68 @@ +import 'package:flutter_undraw/flutter_undraw.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart'; +import 'package:shadcn_flutter/shadcn_flutter_extension.dart'; +import 'package:spotube/extensions/constrains.dart'; +import 'package:spotube/extensions/context.dart'; +import 'package:spotube/pages/search/search.dart'; + +class SearchPlaceholder extends HookConsumerWidget { + final AsyncValue snapshot; + final Widget child; + const SearchPlaceholder({ + super.key, + required this.child, + required this.snapshot, + }); + + @override + Widget build(BuildContext context, ref) { + final theme = context.theme; + final mediaQuery = MediaQuery.sizeOf(context); + + final searchTerm = ref.watch(searchTermStateProvider); + + return switch ((searchTerm.isEmpty, snapshot.isLoading)) { + (true, false) => Column( + children: [ + SizedBox( + height: mediaQuery.height * 0.2, + ), + Undraw( + illustration: UndrawIllustration.explore, + color: theme.colorScheme.primary, + height: 200 * theme.scaling, + ), + const SizedBox(height: 20), + Text(context.l10n.search_to_get_results).large(), + ], + ), + (false, true) => Container( + constraints: BoxConstraints( + maxWidth: + mediaQuery.lgAndUp ? mediaQuery.width * 0.5 : mediaQuery.width, + ), + padding: const EdgeInsets.symmetric( + horizontal: 20, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + context.l10n.crunching_results, + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w900, + color: theme.colorScheme.foreground.withValues(alpha: 0.7), + ), + ), + const SizedBox(height: 20), + const LinearProgressIndicator(), + ], + ), + ), + _ => child, + }; + } +} diff --git a/lib/pages/search/sections/albums.dart b/lib/modules/search/sections/albums.dart similarity index 100% rename from lib/pages/search/sections/albums.dart rename to lib/modules/search/sections/albums.dart diff --git a/lib/pages/search/sections/artists.dart b/lib/modules/search/sections/artists.dart similarity index 100% rename from lib/pages/search/sections/artists.dart rename to lib/modules/search/sections/artists.dart diff --git a/lib/pages/search/sections/playlists.dart b/lib/modules/search/sections/playlists.dart similarity index 100% rename from lib/pages/search/sections/playlists.dart rename to lib/modules/search/sections/playlists.dart diff --git a/lib/pages/search/sections/tracks.dart b/lib/modules/search/sections/tracks.dart similarity index 100% rename from lib/pages/search/sections/tracks.dart rename to lib/modules/search/sections/tracks.dart diff --git a/lib/pages/artist/section/header.dart b/lib/pages/artist/section/header.dart index b11e8d66..2ce9178c 100644 --- a/lib/pages/artist/section/header.dart +++ b/lib/pages/artist/section/header.dart @@ -195,9 +195,11 @@ class ArtistPageHeader extends HookConsumerWidget { Flexible( child: AutoSizeText( context.l10n.followers( - PrimitiveUtils.toReadableNumber( - artist.followers!.toDouble(), - ), + artist.followers == null + ? double.infinity + : PrimitiveUtils.toReadableNumber( + artist.followers!.toDouble(), + ), ), maxLines: 1, overflow: TextOverflow.ellipsis, diff --git a/lib/pages/search/search.dart b/lib/pages/search/search.dart index 955ff59b..0db34810 100644 --- a/lib/pages/search/search.dart +++ b/lib/pages/search/search.dart @@ -1,5 +1,4 @@ import 'package:flutter/services.dart'; -import 'package:flutter_undraw/flutter_undraw.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; @@ -8,16 +7,16 @@ 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/inter_scrollbar/inter_scrollbar.dart'; import 'package:spotube/components/fallbacks/anonymous_fallback.dart'; import 'package:spotube/components/titlebar/titlebar.dart'; -import 'package:spotube/extensions/constrains.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/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/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'; @@ -35,18 +34,23 @@ class SearchPage extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { - final theme = Theme.of(context); - final mediaQuery = MediaQuery.sizeOf(context); - - final scrollController = useScrollController(); final controller = useShadcnTextEditingController(); final focusNode = useFocusNode(); final authenticated = ref.watch(metadataPluginAuthenticatedProvider); final searchTerm = ref.watch(searchTermStateProvider); - final searchSnapshot = - ref.watch(metadataPluginSearchAllProvider(searchTerm)); + 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; @@ -88,7 +92,10 @@ class SearchPage extends HookConsumerWidget { children: [ Expanded( child: Padding( - padding: const EdgeInsets.all(20), + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 10, + ), child: ListenableBuilder( listenable: controller, builder: (context, _) { @@ -168,78 +175,50 @@ class SearchPage extends HookConsumerWidget { ), ], ), + 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 (( - searchTerm.isEmpty, - searchSnapshot.isLoading - )) { - (true, false) => Column( - children: [ - SizedBox( - height: mediaQuery.height * 0.2, - ), - Undraw( - illustration: UndrawIllustration.explore, - color: theme.colorScheme.primary, - height: 200 * theme.scaling, - ), - const SizedBox(height: 20), - Text(context.l10n.search_to_get_results) - .large(), - ], - ), - (false, true) => Container( - constraints: BoxConstraints( - maxWidth: mediaQuery.lgAndUp - ? mediaQuery.width * 0.5 - : mediaQuery.width, - ), - padding: const EdgeInsets.symmetric( - horizontal: 20, - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - context.l10n.crunching_results, - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w900, - color: theme.colorScheme.foreground - .withValues(alpha: 0.7), - ), - ), - const SizedBox(height: 20), - const LinearProgressIndicator(), - ], - ), - ), - _ => InterScrollbar( - controller: scrollController, - child: SingleChildScrollView( - controller: scrollController, - child: const Padding( - padding: EdgeInsets.symmetric(vertical: 8), - child: SafeArea( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - SearchTracksSection(), - SearchPlaylistsSection(), - Gap(20), - SearchArtistsSection(), - Gap(20), - SearchAlbumsSection(), - ], - ), - ), - ), - ), - ), + child: switch (selectedChip.value) { + "tracks" => const SearchPageTracksTab(), + "albums" => const SearchPageAlbumsTab(), + "artists" => const SearchPageArtistsTab(), + "playlists" => const SearchPagePlaylistsTab(), + _ => const SearchPageAllTab(), }, ), ), diff --git a/lib/pages/search/tabs/albums.dart b/lib/pages/search/tabs/albums.dart new file mode 100644 index 00000000..19781c05 --- /dev/null +++ b/lib/pages/search/tabs/albums.dart @@ -0,0 +1,48 @@ +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart'; +import 'package:spotube/collections/fake.dart'; +import 'package:spotube/components/playbutton_view/playbutton_view.dart'; +import 'package:spotube/modules/album/album_card.dart'; +import 'package:spotube/modules/search/loading.dart'; +import 'package:spotube/pages/search/search.dart'; +import 'package:spotube/provider/metadata_plugin/search/albums.dart'; + +class SearchPageAlbumsTab extends HookConsumerWidget { + const SearchPageAlbumsTab({super.key}); + + @override + Widget build(BuildContext context, ref) { + final controller = useScrollController(); + + final searchTerm = ref.watch(searchTermStateProvider); + final searchAlbumsSnapshot = + ref.watch(metadataPluginSearchAlbumsProvider(searchTerm)); + final searchAlbumsNotifier = + ref.read(metadataPluginSearchAlbumsProvider(searchTerm).notifier); + final searchAlbums = + searchAlbumsSnapshot.asData?.value.items ?? [FakeData.albumSimple]; + + return SearchPlaceholder( + snapshot: searchAlbumsSnapshot, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: CustomScrollView( + slivers: [ + PlaybuttonView( + controller: controller, + itemCount: searchAlbums.length, + hasMore: searchAlbumsSnapshot.asData?.value.hasMore == true, + isLoading: searchAlbumsSnapshot.isLoading, + onRequestMore: searchAlbumsNotifier.fetchMore, + gridItemBuilder: (context, index) => + AlbumCard(searchAlbums[index]), + listItemBuilder: (context, index) => + AlbumCard.tile(searchAlbums[index]), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/search/tabs/all.dart b/lib/pages/search/tabs/all.dart new file mode 100644 index 00000000..42ff1e69 --- /dev/null +++ b/lib/pages/search/tabs/all.dart @@ -0,0 +1,48 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart'; +import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart'; +import 'package:spotube/modules/search/loading.dart'; +import 'package:spotube/pages/search/search.dart'; +import 'package:spotube/modules/search/sections/albums.dart'; +import 'package:spotube/modules/search/sections/artists.dart'; +import 'package:spotube/modules/search/sections/playlists.dart'; +import 'package:spotube/modules/search/sections/tracks.dart'; +import 'package:spotube/provider/metadata_plugin/search/all.dart'; + +class SearchPageAllTab extends HookConsumerWidget { + const SearchPageAllTab({super.key}); + + @override + Widget build(BuildContext context, ref) { + final scrollController = ScrollController(); + final searchTerm = ref.watch(searchTermStateProvider); + final searchSnapshot = + ref.watch(metadataPluginSearchAllProvider(searchTerm)); + + return SearchPlaceholder( + snapshot: searchSnapshot, + child: InterScrollbar( + controller: scrollController, + child: SingleChildScrollView( + controller: scrollController, + child: const Padding( + padding: EdgeInsets.symmetric(vertical: 8), + child: SafeArea( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SearchTracksSection(), + SearchPlaylistsSection(), + Gap(20), + SearchArtistsSection(), + Gap(20), + SearchAlbumsSection(), + ], + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/pages/search/tabs/artists.dart b/lib/pages/search/tabs/artists.dart new file mode 100644 index 00000000..59c77a70 --- /dev/null +++ b/lib/pages/search/tabs/artists.dart @@ -0,0 +1,94 @@ +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_undraw/flutter_undraw.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart'; +import 'package:shadcn_flutter/shadcn_flutter_extension.dart'; +import 'package:skeletonizer/skeletonizer.dart'; +import 'package:spotube/collections/fake.dart'; +import 'package:spotube/components/waypoint.dart'; +import 'package:spotube/extensions/constrains.dart'; +import 'package:spotube/extensions/context.dart'; +import 'package:spotube/modules/artist/artist_card.dart'; +import 'package:spotube/modules/search/loading.dart'; +import 'package:spotube/pages/search/search.dart'; +import 'package:spotube/provider/metadata_plugin/search/artists.dart'; + +class SearchPageArtistsTab extends HookConsumerWidget { + const SearchPageArtistsTab({super.key}); + + @override + Widget build(BuildContext context, ref) { + final controller = useScrollController(); + + final searchTerm = ref.watch(searchTermStateProvider); + final searchArtistsSnapshot = + ref.watch(metadataPluginSearchArtistsProvider(searchTerm)); + final searchArtistsNotifier = + ref.read(metadataPluginSearchArtistsProvider(searchTerm).notifier); + final searchArtists = searchArtistsSnapshot.asData?.value.items ?? []; + + return SearchPlaceholder( + snapshot: searchArtistsSnapshot, + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: LayoutBuilder(builder: (context, constrains) { + if (searchArtistsSnapshot.hasValue && searchArtists.isEmpty) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Column( + mainAxisSize: MainAxisSize.min, + spacing: 10, + children: [ + Undraw( + height: 200 * context.theme.scaling, + illustration: UndrawIllustration.taken, + color: Theme.of(context).colorScheme.primary, + ), + Text( + context.l10n.nothing_found, + textAlign: TextAlign.center, + ).muted().small() + ], + ), + ); + } + + return GridView.builder( + padding: const EdgeInsets.all(16), + itemCount: searchArtists.length + 1, + gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 200, + mainAxisExtent: constrains.smAndDown ? 225 : 250, + crossAxisSpacing: 8, + mainAxisSpacing: 8, + ), + itemBuilder: (context, index) { + if (searchArtists.isNotEmpty && index == searchArtists.length) { + if (searchArtistsSnapshot.asData?.value.hasMore != true) { + return const SizedBox.shrink(); + } + + return Waypoint( + controller: controller, + isGrid: true, + onTouchEdge: searchArtistsNotifier.fetchMore, + child: Skeletonizer( + enabled: true, + child: ArtistCard(FakeData.artist), + ), + ); + } + + return Skeletonizer( + enabled: searchArtistsSnapshot.isLoading, + child: ArtistCard( + searchArtists.elementAtOrNull(index) ?? FakeData.artist, + ), + ); + }, + ); + }), + ), + ); + } +} diff --git a/lib/pages/search/tabs/playlists.dart b/lib/pages/search/tabs/playlists.dart new file mode 100644 index 00000000..2ea9d430 --- /dev/null +++ b/lib/pages/search/tabs/playlists.dart @@ -0,0 +1,48 @@ +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart'; +import 'package:spotube/collections/fake.dart'; +import 'package:spotube/components/playbutton_view/playbutton_view.dart'; +import 'package:spotube/modules/playlist/playlist_card.dart'; +import 'package:spotube/modules/search/loading.dart'; +import 'package:spotube/pages/search/search.dart'; +import 'package:spotube/provider/metadata_plugin/search/playlists.dart'; + +class SearchPagePlaylistsTab extends HookConsumerWidget { + const SearchPagePlaylistsTab({super.key}); + + @override + Widget build(BuildContext context, ref) { + final controller = useScrollController(); + + final searchTerm = ref.watch(searchTermStateProvider); + final searchPlaylistsSnapshot = + ref.watch(metadataPluginSearchPlaylistsProvider(searchTerm)); + final searchPlaylistsNotifier = + ref.read(metadataPluginSearchPlaylistsProvider(searchTerm).notifier); + final searchPlaylists = searchPlaylistsSnapshot.asData?.value.items ?? + [FakeData.playlistSimple]; + + return SearchPlaceholder( + snapshot: searchPlaylistsSnapshot, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: CustomScrollView( + slivers: [ + PlaybuttonView( + controller: controller, + itemCount: searchPlaylists.length, + hasMore: searchPlaylistsSnapshot.asData?.value.hasMore == true, + isLoading: searchPlaylistsSnapshot.isLoading, + onRequestMore: searchPlaylistsNotifier.fetchMore, + gridItemBuilder: (context, index) => + PlaylistCard(searchPlaylists[index]), + listItemBuilder: (context, index) => + PlaylistCard.tile(searchPlaylists[index]), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/search/tabs/tracks.dart b/lib/pages/search/tabs/tracks.dart new file mode 100644 index 00000000..2212c010 --- /dev/null +++ b/lib/pages/search/tabs/tracks.dart @@ -0,0 +1,119 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart'; +import 'package:skeletonizer/skeletonizer.dart'; +import 'package:spotube/collections/fake.dart'; +import 'package:spotube/components/dialogs/prompt_dialog.dart'; +import 'package:spotube/components/dialogs/select_device_dialog.dart'; +import 'package:spotube/components/track_tile/track_tile.dart'; +import 'package:spotube/extensions/context.dart'; +import 'package:spotube/models/connect/connect.dart'; +import 'package:spotube/modules/search/loading.dart'; +import 'package:spotube/pages/search/search.dart'; +import 'package:spotube/provider/audio_player/audio_player.dart'; +import 'package:spotube/provider/connect/connect.dart'; +import 'package:spotube/provider/metadata_plugin/search/tracks.dart'; +import 'package:spotube/provider/metadata_plugin/utils/common.dart'; +import 'package:very_good_infinite_list/very_good_infinite_list.dart'; + +class SearchPageTracksTab extends HookConsumerWidget { + const SearchPageTracksTab({super.key}); + + @override + Widget build(BuildContext context, ref) { + final searchTerm = ref.watch(searchTermStateProvider); + final searchTracksSnapshot = + ref.watch(metadataPluginSearchTracksProvider(searchTerm)); + final searchTracksNotifier = + ref.read(metadataPluginSearchTracksProvider(searchTerm).notifier); + final searchTracks = + searchTracksSnapshot.asData?.value.items ?? [FakeData.track]; + + final playlist = ref.watch(audioPlayerProvider); + final playlistNotifier = ref.watch(audioPlayerProvider.notifier); + + return SearchPlaceholder( + snapshot: searchTracksSnapshot, + child: InfiniteList( + itemCount: searchTracksSnapshot.asData?.value.items.length ?? 0, + hasReachedMax: searchTracksSnapshot.asData?.value.hasMore != true, + isLoading: searchTracksSnapshot.isLoading && + !searchTracksSnapshot.isLoadingNextPage, + loadingBuilder: (context) { + return Skeletonizer( + enabled: true, + child: TrackTile(track: FakeData.track, playlist: playlist), + ); + }, + onFetchData: () { + searchTracksNotifier.fetchMore(); + }, + itemBuilder: (context, index) { + final track = searchTracks[index]; + + return TrackTile( + track: track, + playlist: playlist, + index: index, + onTap: () async { + final isRemoteDevice = await showSelectDeviceDialog(context, ref); + + if (isRemoteDevice == null) return; + + if (isRemoteDevice) { + final remotePlayback = ref.read(connectProvider.notifier); + final remotePlaylist = ref.read(queueProvider); + + final isTrackPlaying = + remotePlaylist.activeTrack?.id == track.id; + + if (!isTrackPlaying && context.mounted) { + final shouldPlay = (playlist.tracks.length) > 20 + ? await showPromptDialog( + context: context, + title: context.l10n.playing_track( + track.name, + ), + message: context.l10n.queue_clear_alert( + playlist.tracks.length, + ), + ) + : true; + + if (shouldPlay) { + await remotePlayback.load( + WebSocketLoadEventData.playlist( + tracks: [track], + ), + ); + } + } + } else { + final isTrackPlaying = playlist.activeTrack?.id == track.id; + if (!isTrackPlaying && context.mounted) { + final shouldPlay = (playlist.tracks.length) > 20 + ? await showPromptDialog( + context: context, + title: context.l10n.playing_track( + track.name, + ), + message: context.l10n.queue_clear_alert( + playlist.tracks.length, + ), + ) + : true; + + if (shouldPlay) { + await playlistNotifier.load( + [track], + autoPlay: true, + ); + } + } + } + }, + ); + }, + ), + ); + } +} diff --git a/lib/provider/metadata_plugin/search/albums.dart b/lib/provider/metadata_plugin/search/albums.dart new file mode 100644 index 00000000..40bb62e6 --- /dev/null +++ b/lib/provider/metadata_plugin/search/albums.dart @@ -0,0 +1,46 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotube/models/metadata/metadata.dart'; +import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart'; +import 'package:spotube/provider/metadata_plugin/utils/common.dart'; +import 'package:spotube/provider/metadata_plugin/utils/family_paginated.dart'; + +class MetadataPluginSearchAlbumsNotifier + extends AutoDisposeFamilyPaginatedAsyncNotifier { + MetadataPluginSearchAlbumsNotifier() : super(); + + @override + fetch(offset, limit) async { + if (arg.isEmpty) { + return SpotubePaginationResponseObject( + limit: limit, + nextOffset: null, + total: 0, + items: [], + hasMore: false, + ); + } + + final res = await (await metadataPlugin).search.albums( + arg, + offset: offset, + limit: limit, + ); + + return res; + } + + @override + build(arg) async { + ref.cacheFor(); + + ref.watch(metadataPluginProvider); + return await fetch(0, 20); + } +} + +final metadataPluginSearchAlbumsProvider = + AutoDisposeAsyncNotifierProviderFamily, String>( + () => MetadataPluginSearchAlbumsNotifier(), +); diff --git a/lib/provider/metadata_plugin/search/all.dart b/lib/provider/metadata_plugin/search/all.dart index 53c79f7c..92f60175 100644 --- a/lib/provider/metadata_plugin/search/all.dart +++ b/lib/provider/metadata_plugin/search/all.dart @@ -17,3 +17,14 @@ final metadataPluginSearchAllProvider = return metadataPlugin.search.all(query); }, ); + +final metadataPluginSearchChipsProvider = FutureProvider((ref) async { + final metadataPlugin = await ref.watch(metadataPluginProvider.future); + + if (metadataPlugin == null) { + throw MetadataPluginException.noDefaultPlugin( + "No default metadata plugin found", + ); + } + return metadataPlugin.search.chips; +}); diff --git a/lib/provider/metadata_plugin/search/artists.dart b/lib/provider/metadata_plugin/search/artists.dart new file mode 100644 index 00000000..b4d619f7 --- /dev/null +++ b/lib/provider/metadata_plugin/search/artists.dart @@ -0,0 +1,46 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotube/models/metadata/metadata.dart'; +import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart'; +import 'package:spotube/provider/metadata_plugin/utils/common.dart'; +import 'package:spotube/provider/metadata_plugin/utils/family_paginated.dart'; + +class MetadataPluginSearchArtistsNotifier + extends AutoDisposeFamilyPaginatedAsyncNotifier { + MetadataPluginSearchArtistsNotifier() : super(); + + @override + fetch(offset, limit) async { + if (arg.isEmpty) { + return SpotubePaginationResponseObject( + limit: limit, + nextOffset: null, + total: 0, + items: [], + hasMore: false, + ); + } + + final res = await (await metadataPlugin).search.artists( + arg, + offset: offset, + limit: limit, + ); + + return res; + } + + @override + build(arg) async { + ref.cacheFor(); + + ref.watch(metadataPluginProvider); + return await fetch(0, 20); + } +} + +final metadataPluginSearchArtistsProvider = + AutoDisposeAsyncNotifierProviderFamily, String>( + () => MetadataPluginSearchArtistsNotifier(), +); diff --git a/lib/provider/metadata_plugin/search/playlists.dart b/lib/provider/metadata_plugin/search/playlists.dart new file mode 100644 index 00000000..dbf54250 --- /dev/null +++ b/lib/provider/metadata_plugin/search/playlists.dart @@ -0,0 +1,48 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotube/models/metadata/metadata.dart'; +import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart'; +import 'package:spotube/provider/metadata_plugin/utils/common.dart'; +import 'package:spotube/provider/metadata_plugin/utils/family_paginated.dart'; + +class MetadataPluginSearchPlaylistsNotifier + extends AutoDisposeFamilyPaginatedAsyncNotifier { + MetadataPluginSearchPlaylistsNotifier() : super(); + + @override + fetch(offset, limit) async { + if (arg.isEmpty) { + return SpotubePaginationResponseObject( + limit: limit, + nextOffset: null, + total: 0, + items: [], + hasMore: false, + ); + } + + final res = await (await metadataPlugin).search.playlists( + arg, + offset: offset, + limit: limit, + ); + + return res; + } + + @override + build(arg) async { + ref.cacheFor(); + + ref.watch(metadataPluginProvider); + return await fetch(0, 20); + } +} + +final metadataPluginSearchPlaylistsProvider = + AutoDisposeAsyncNotifierProviderFamily< + MetadataPluginSearchPlaylistsNotifier, + SpotubePaginationResponseObject, + String>( + () => MetadataPluginSearchPlaylistsNotifier(), +); diff --git a/lib/provider/metadata_plugin/search/tracks.dart b/lib/provider/metadata_plugin/search/tracks.dart new file mode 100644 index 00000000..0b6ac141 --- /dev/null +++ b/lib/provider/metadata_plugin/search/tracks.dart @@ -0,0 +1,46 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotube/models/metadata/metadata.dart'; +import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart'; +import 'package:spotube/provider/metadata_plugin/utils/common.dart'; +import 'package:spotube/provider/metadata_plugin/utils/family_paginated.dart'; + +class MetadataPluginSearchTracksNotifier + extends AutoDisposeFamilyPaginatedAsyncNotifier { + MetadataPluginSearchTracksNotifier() : super(); + + @override + fetch(offset, limit) async { + if (arg.isEmpty) { + return SpotubePaginationResponseObject( + limit: limit, + nextOffset: null, + total: 0, + items: [], + hasMore: false, + ); + } + + final tracks = await (await metadataPlugin).search.tracks( + arg, + offset: offset, + limit: limit, + ); + + return tracks; + } + + @override + build(arg) async { + ref.cacheFor(); + + ref.watch(metadataPluginProvider); + return await fetch(0, 20); + } +} + +final metadataPluginSearchTracksProvider = + AutoDisposeAsyncNotifierProviderFamily, String>( + () => MetadataPluginSearchTracksNotifier(), +); diff --git a/lib/services/metadata/endpoints/search.dart b/lib/services/metadata/endpoints/search.dart index be4e8e30..c2e14765 100644 --- a/lib/services/metadata/endpoints/search.dart +++ b/lib/services/metadata/endpoints/search.dart @@ -10,6 +10,10 @@ class MetadataPluginSearchEndpoint { (hetu.fetch("metadataPlugin") as HTInstance).memberGet("search") as HTInstance; + List get chips { + return (hetuMetadataSearch.memberGet("chips") as List).cast(); + } + Future all(String query) async { if (query.isEmpty) { return SpotubeSearchResponseObject( diff --git a/pubspec.lock b/pubspec.lock index 680e068f..f7798e5f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1226,7 +1226,7 @@ packages: description: path: "." ref: main - resolved-ref: d3720be2a92022f7b95a3082d40322d8458c70da + resolved-ref: "7e9032c054c547f7900c9c9fe4b76e29c8ac1cd1" url: "https://github.com/hetu-community/hetu_std.git" source: git version: "1.0.0"