From e7c6813ccb2afcda9a8b044570a1c0a27785a59f Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 7 Aug 2023 17:34:56 +0600 Subject: [PATCH] feat: paginated user playlists --- lib/components/library/user_playlists.dart | 106 ++++++++++-------- .../dialogs/playlist_add_track_dialog.dart | 33 +++++- lib/components/shared/heart_button.dart | 1 - lib/services/mutations/playlist.dart | 8 ++ lib/services/queries/album.dart | 3 +- lib/services/queries/playlist.dart | 14 ++- 6 files changed, 110 insertions(+), 55 deletions(-) diff --git a/lib/components/library/user_playlists.dart b/lib/components/library/user_playlists.dart index 830f5b876..3f4029fed 100644 --- a/lib/components/library/user_playlists.dart +++ b/lib/components/library/user_playlists.dart @@ -11,6 +11,7 @@ import 'package:spotube/components/playlist/playlist_create_dialog.dart'; import 'package:spotube/components/shared/shimmers/shimmer_playbutton_card.dart'; import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart'; import 'package:spotube/components/playlist/playlist_card.dart'; +import 'package:spotube/components/shared/waypoint.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/services/queries/queries.dart'; @@ -26,6 +27,12 @@ class UserPlaylists extends HookConsumerWidget { final playlistsQuery = useQueries.playlist.ofMine(ref); + final pagePlaylists = useMemoized( + () => playlistsQuery.pages + .expand((page) => page.items?.toList() ?? []), + [playlistsQuery.pages], + ); + final likedTracksPlaylist = useMemoized( () => PlaylistSimple() ..name = context.l10n.liked_tracks @@ -48,12 +55,12 @@ class UserPlaylists extends HookConsumerWidget { if (searchText.value.isEmpty) { return [ likedTracksPlaylist, - ...?playlistsQuery.data, + ...pagePlaylists, ]; } return [ likedTracksPlaylist, - ...?playlistsQuery.data, + ...pagePlaylists, ] .map((e) => (weightedRatio(e.name!, searchText.value), e)) .sorted((a, b) => b.$1.compareTo(a.$1)) @@ -61,9 +68,11 @@ class UserPlaylists extends HookConsumerWidget { .map((e) => e.$2) .toList(); }, - [playlistsQuery.data, searchText.value], + [pagePlaylists, searchText.value], ); + final controller = useScrollController(); + if (auth == null) { return const AnonymousFallback(); } @@ -71,50 +80,59 @@ class UserPlaylists extends HookConsumerWidget { return RefreshIndicator( onRefresh: playlistsQuery.refresh, child: SingleChildScrollView( + controller: controller, physics: const AlwaysScrollableScrollPhysics(), - child: SafeArea( - child: Column( - children: [ - Padding( - padding: const EdgeInsets.all(10), - child: SearchBar( - onChanged: (value) => searchText.value = value, - hintText: context.l10n.filter_playlists, - leading: const Icon(SpotubeIcons.filter), + child: Waypoint( + controller: controller, + onTouchEdge: () { + if (playlistsQuery.hasNextPage) { + playlistsQuery.fetchNext(); + } + }, + child: SafeArea( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(10), + child: SearchBar( + onChanged: (value) => searchText.value = value, + hintText: context.l10n.filter_playlists, + leading: const Icon(SpotubeIcons.filter), + ), ), - ), - AnimatedCrossFade( - duration: const Duration(milliseconds: 300), - crossFadeState: - playlistsQuery.isLoading || !playlistsQuery.hasData - ? CrossFadeState.showFirst - : CrossFadeState.showSecond, - firstChild: - const Center(child: ShimmerPlaybuttonCard(count: 7)), - secondChild: Wrap( - runSpacing: 10, - alignment: WrapAlignment.center, - children: [ - Row( - children: [ - const SizedBox(width: 10), - const PlaylistCreateDialogButton(), - const SizedBox(width: 10), - ElevatedButton.icon( - icon: const Icon(SpotubeIcons.magic), - label: Text(context.l10n.generate_playlist), - onPressed: () { - GoRouter.of(context).push("/library/generate"); - }, - ), - const SizedBox(width: 10), - ], - ), - ...playlists.map((playlist) => PlaylistCard(playlist)) - ], + AnimatedCrossFade( + duration: const Duration(milliseconds: 300), + crossFadeState: playlistsQuery.isLoadingPage || + !playlistsQuery.hasPageData + ? CrossFadeState.showFirst + : CrossFadeState.showSecond, + firstChild: + const Center(child: ShimmerPlaybuttonCard(count: 7)), + secondChild: Wrap( + runSpacing: 10, + alignment: WrapAlignment.center, + children: [ + Row( + children: [ + const SizedBox(width: 10), + const PlaylistCreateDialogButton(), + const SizedBox(width: 10), + ElevatedButton.icon( + icon: const Icon(SpotubeIcons.magic), + label: Text(context.l10n.generate_playlist), + onPressed: () { + GoRouter.of(context).push("/library/generate"); + }, + ), + const SizedBox(width: 10), + ], + ), + ...playlists.map((playlist) => PlaylistCard(playlist)) + ], + ), ), - ), - ], + ], + ), ), ), ), diff --git a/lib/components/shared/dialogs/playlist_add_track_dialog.dart b/lib/components/shared/dialogs/playlist_add_track_dialog.dart index 76dcd082d..29f64268d 100644 --- a/lib/components/shared/dialogs/playlist_add_track_dialog.dart +++ b/lib/components/shared/dialogs/playlist_add_track_dialog.dart @@ -1,3 +1,4 @@ +import 'package:async/async.dart'; import 'package:fl_query_hooks/fl_query_hooks.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; @@ -21,11 +22,33 @@ class PlaylistAddTrackDialog extends HookConsumerWidget { Widget build(BuildContext context, ref) { final spotify = ref.watch(spotifyProvider); final userPlaylists = useQueries.playlist.ofMine(ref); + + useEffect(() { + final op = CancelableOperation.fromFuture( + () async { + while (userPlaylists.hasNextPage) { + await userPlaylists.fetchNext(); + } + }(), + ); + + return () { + op.cancel(); + }; + }, [userPlaylists.hasNextPage]); + final me = useQueries.user.me(ref); - final filteredPlaylists = userPlaylists.data?.where( - (playlist) => - playlist.owner?.id != null && playlist.owner!.id == me.data?.id, + + final filteredPlaylists = useMemoized( + () => userPlaylists.pages + .expand((page) => page.items?.toList() ?? []) + .where( + (playlist) => + playlist.owner?.id != null && playlist.owner!.id == me.data?.id, + ), + [userPlaylists.pages, me.data?.id], ); + final playlistsCheck = useState({}); final queryClient = useQueryClient(); @@ -70,11 +93,11 @@ class PlaylistAddTrackDialog extends HookConsumerWidget { content: SizedBox( height: 300, width: 300, - child: !userPlaylists.hasData + child: userPlaylists.hasNextPage ? const Center(child: CircularProgressIndicator()) : ListView.builder( shrinkWrap: true, - itemCount: filteredPlaylists!.length, + itemCount: filteredPlaylists.length, itemBuilder: (context, index) { final playlist = filteredPlaylists.elementAt(index); return CheckboxListTile( diff --git a/lib/components/shared/heart_button.dart b/lib/components/shared/heart_button.dart index 3d68e1fce..cf7909182 100644 --- a/lib/components/shared/heart_button.dart +++ b/lib/components/shared/heart_button.dart @@ -156,7 +156,6 @@ class PlaylistHeartButton extends HookConsumerWidget { playlist.id!, refreshQueries: [ isLikedQuery.key, - "current-user-playlists", ], ); diff --git a/lib/services/mutations/playlist.dart b/lib/services/mutations/playlist.dart index 106f4cdcf..ee06ad9db 100644 --- a/lib/services/mutations/playlist.dart +++ b/lib/services/mutations/playlist.dart @@ -1,4 +1,5 @@ import 'package:fl_query/fl_query.dart'; +import 'package:fl_query_hooks/fl_query_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/hooks/use_spotify_mutation.dart'; @@ -9,7 +10,9 @@ class PlaylistMutations { WidgetRef ref, String playlistId, { List? refreshQueries, + List? refreshInfiniteQueries, }) { + final queryClient = useQueryClient(); return useSpotifyMutation( "toggle-playlist-like/$playlistId", (isLiked, spotify) async { @@ -22,6 +25,11 @@ class PlaylistMutations { }, ref: ref, refreshQueries: refreshQueries, + refreshInfiniteQueries: refreshInfiniteQueries, + onData: (data, recoveryData) async { + await queryClient + .refreshInfiniteQueryAllPages("current-user-playlists"); + }, ); } diff --git a/lib/services/queries/album.dart b/lib/services/queries/album.dart index d4ffa2f51..a0c968eb7 100644 --- a/lib/services/queries/album.dart +++ b/lib/services/queries/album.dart @@ -42,7 +42,8 @@ class AlbumQueries { return useSpotifyQuery( "is-saved-for-me/$album", (spotify) { - return spotify.me.isSavedAlbums([album]).then((value) => value.first); + return spotify.me + .containsSavedAlbums([album]).then((value) => value[album]); }, ref: ref, ); diff --git a/lib/services/queries/playlist.dart b/lib/services/queries/playlist.dart index 194e1e462..25da61997 100644 --- a/lib/services/queries/playlist.dart +++ b/lib/services/queries/playlist.dart @@ -126,12 +126,18 @@ class PlaylistQueries { ); } - Query, dynamic> ofMine(WidgetRef ref) { - return useSpotifyQuery, dynamic>( + InfiniteQuery, dynamic, int> ofMine(WidgetRef ref) { + return useSpotifyInfiniteQuery, dynamic, int>( "current-user-playlists", - (spotify) { - return spotify.playlists.me.all(); + (page, spotify) async { + final playlists = await spotify.playlists.me.getPage(10, page * 10); + return playlists; }, + initialPage: 0, + nextPage: (lastPage, lastPageData) => + (lastPageData.items?.length ?? 0) < 10 || lastPageData.isLast + ? null + : lastPage + 1, ref: ref, ); }