feat: add support for entity specific search

This commit is contained in:
Kingkor Roy Tirtho 2025-07-13 15:11:56 +06:00
parent 412c427cec
commit 7de6423935
20 changed files with 696 additions and 89 deletions

View File

@ -107,7 +107,7 @@ class SiblingTracksSheet extends HookConsumerWidget {
return siblingType.info; return siblingType.info;
})); }));
final activeSourceInfo = activeTrackSource as TrackSourceInfo; final activeSourceInfo = activeTrackSource?.info as TrackSourceInfo;
return results return results
..removeWhere((element) => element.id == activeSourceInfo.id) ..removeWhere((element) => element.id == activeSourceInfo.id)
@ -127,7 +127,7 @@ class SiblingTracksSheet extends HookConsumerWidget {
return siblingType.info; return siblingType.info;
}), }),
); );
final activeSourceInfo = activeTrackSource as TrackSourceInfo; final activeSourceInfo = activeTrackSource?.info as TrackSourceInfo;
return searchResults return searchResults
..removeWhere((element) => element.id == activeSourceInfo.id) ..removeWhere((element) => element.id == activeSourceInfo.id)
..insert( ..insert(

View File

@ -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,
};
}
}

View File

@ -195,7 +195,9 @@ class ArtistPageHeader extends HookConsumerWidget {
Flexible( Flexible(
child: AutoSizeText( child: AutoSizeText(
context.l10n.followers( context.l10n.followers(
PrimitiveUtils.toReadableNumber( artist.followers == null
? double.infinity
: PrimitiveUtils.toReadableNumber(
artist.followers!.toDouble(), artist.followers!.toDouble(),
), ),
), ),

View File

@ -1,5 +1,4 @@
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_undraw/flutter_undraw.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:flutter_hooks/flutter_hooks.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/routes.gr.dart';
import 'package:spotube/collections/spotube_icons.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/fallbacks/anonymous_fallback.dart';
import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/components/titlebar/titlebar.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.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/hooks/controllers/use_shadcn_text_editing_controller.dart';
import 'package:spotube/pages/search/sections/albums.dart'; import 'package:spotube/pages/search/tabs/albums.dart';
import 'package:spotube/pages/search/sections/artists.dart'; import 'package:spotube/pages/search/tabs/all.dart';
import 'package:spotube/pages/search/sections/playlists.dart'; import 'package:spotube/pages/search/tabs/artists.dart';
import 'package:spotube/pages/search/sections/tracks.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/auth.dart';
import 'package:spotube/provider/metadata_plugin/search/all.dart'; import 'package:spotube/provider/metadata_plugin/search/all.dart';
import 'package:spotube/services/kv_store/kv_store.dart'; import 'package:spotube/services/kv_store/kv_store.dart';
@ -35,18 +34,23 @@ class SearchPage extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final theme = Theme.of(context);
final mediaQuery = MediaQuery.sizeOf(context);
final scrollController = useScrollController();
final controller = useShadcnTextEditingController(); final controller = useShadcnTextEditingController();
final focusNode = useFocusNode(); final focusNode = useFocusNode();
final authenticated = ref.watch(metadataPluginAuthenticatedProvider); final authenticated = ref.watch(metadataPluginAuthenticatedProvider);
final searchTerm = ref.watch(searchTermStateProvider); final searchTerm = ref.watch(searchTermStateProvider);
final searchSnapshot = final searchChipSnapshot = ref.watch(metadataPluginSearchChipsProvider);
ref.watch(metadataPluginSearchAllProvider(searchTerm)); final selectedChip = useState<String?>(
searchChipSnapshot.asData?.value.first ?? "all",
);
ref.listen(
metadataPluginSearchChipsProvider,
(previous, next) {
selectedChip.value = next.asData?.value.first ?? "all";
},
);
useEffect(() { useEffect(() {
controller.text = searchTerm; controller.text = searchTerm;
@ -88,7 +92,10 @@ class SearchPage extends HookConsumerWidget {
children: [ children: [
Expanded( Expanded(
child: Padding( child: Padding(
padding: const EdgeInsets.all(20), padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 10,
),
child: ListenableBuilder( child: ListenableBuilder(
listenable: controller, listenable: controller,
builder: (context, _) { 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( Expanded(
child: AnimatedSwitcher( child: AnimatedSwitcher(
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
child: switch (( child: switch (selectedChip.value) {
searchTerm.isEmpty, "tracks" => const SearchPageTracksTab(),
searchSnapshot.isLoading "albums" => const SearchPageAlbumsTab(),
)) { "artists" => const SearchPageArtistsTab(),
(true, false) => Column( "playlists" => const SearchPagePlaylistsTab(),
children: [ _ => const SearchPageAllTab(),
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(),
],
),
),
),
),
),
}, },
), ),
), ),

View File

@ -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]),
),
],
),
),
);
}
}

View File

@ -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(),
],
),
),
),
),
),
);
}
}

View File

@ -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,
),
);
},
);
}),
),
);
}
}

View File

@ -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]),
),
],
),
),
);
}
}

View File

@ -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,
);
}
}
}
},
);
},
),
);
}
}

View File

@ -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<SpotubeSimpleAlbumObject,
String> {
MetadataPluginSearchAlbumsNotifier() : super();
@override
fetch(offset, limit) async {
if (arg.isEmpty) {
return SpotubePaginationResponseObject<SpotubeSimpleAlbumObject>(
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<MetadataPluginSearchAlbumsNotifier,
SpotubePaginationResponseObject<SpotubeSimpleAlbumObject>, String>(
() => MetadataPluginSearchAlbumsNotifier(),
);

View File

@ -17,3 +17,14 @@ final metadataPluginSearchAllProvider =
return metadataPlugin.search.all(query); 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;
});

View File

@ -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<SpotubeFullArtistObject,
String> {
MetadataPluginSearchArtistsNotifier() : super();
@override
fetch(offset, limit) async {
if (arg.isEmpty) {
return SpotubePaginationResponseObject<SpotubeFullArtistObject>(
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<MetadataPluginSearchArtistsNotifier,
SpotubePaginationResponseObject<SpotubeFullArtistObject>, String>(
() => MetadataPluginSearchArtistsNotifier(),
);

View File

@ -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<SpotubeSimplePlaylistObject,
String> {
MetadataPluginSearchPlaylistsNotifier() : super();
@override
fetch(offset, limit) async {
if (arg.isEmpty) {
return SpotubePaginationResponseObject<SpotubeSimplePlaylistObject>(
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<SpotubeSimplePlaylistObject>,
String>(
() => MetadataPluginSearchPlaylistsNotifier(),
);

View File

@ -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<SpotubeFullTrackObject,
String> {
MetadataPluginSearchTracksNotifier() : super();
@override
fetch(offset, limit) async {
if (arg.isEmpty) {
return SpotubePaginationResponseObject<SpotubeFullTrackObject>(
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<MetadataPluginSearchTracksNotifier,
SpotubePaginationResponseObject<SpotubeFullTrackObject>, String>(
() => MetadataPluginSearchTracksNotifier(),
);

View File

@ -10,6 +10,10 @@ class MetadataPluginSearchEndpoint {
(hetu.fetch("metadataPlugin") as HTInstance).memberGet("search") (hetu.fetch("metadataPlugin") as HTInstance).memberGet("search")
as HTInstance; as HTInstance;
List<String> get chips {
return (hetuMetadataSearch.memberGet("chips") as List).cast<String>();
}
Future<SpotubeSearchResponseObject> all(String query) async { Future<SpotubeSearchResponseObject> all(String query) async {
if (query.isEmpty) { if (query.isEmpty) {
return SpotubeSearchResponseObject( return SpotubeSearchResponseObject(

View File

@ -1226,7 +1226,7 @@ packages:
description: description:
path: "." path: "."
ref: main ref: main
resolved-ref: d3720be2a92022f7b95a3082d40322d8458c70da resolved-ref: "7e9032c054c547f7900c9c9fe4b76e29c8ac1cd1"
url: "https://github.com/hetu-community/hetu_std.git" url: "https://github.com/hetu-community/hetu_std.git"
source: git source: git
version: "1.0.0" version: "1.0.0"