Skip to content

Commit

Permalink
feat: paginated user playlists
Browse files Browse the repository at this point in the history
  • Loading branch information
KRTirtho committed Aug 7, 2023
1 parent 38dc4be commit e7c6813
Show file tree
Hide file tree
Showing 6 changed files with 110 additions and 55 deletions.
106 changes: 62 additions & 44 deletions lib/components/library/user_playlists.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -26,6 +27,12 @@ class UserPlaylists extends HookConsumerWidget {

final playlistsQuery = useQueries.playlist.ofMine(ref);

final pagePlaylists = useMemoized(
() => playlistsQuery.pages
.expand((page) => page.items?.toList() ?? <PlaylistSimple>[]),
[playlistsQuery.pages],
);

final likedTracksPlaylist = useMemoized(
() => PlaylistSimple()
..name = context.l10n.liked_tracks
Expand All @@ -48,73 +55,84 @@ 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))
.where((e) => e.$1 > 50)
.map((e) => e.$2)
.toList();
},
[playlistsQuery.data, searchText.value],
[pagePlaylists, searchText.value],
);

final controller = useScrollController();

if (auth == null) {
return const AnonymousFallback();
}

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))
],
),
),
),
],
],
),
),
),
),
Expand Down
33 changes: 28 additions & 5 deletions lib/components/shared/dialogs/playlist_add_track_dialog.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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() ?? <PlaylistSimple>[])
.where(
(playlist) =>
playlist.owner?.id != null && playlist.owner!.id == me.data?.id,
),
[userPlaylists.pages, me.data?.id],
);

final playlistsCheck = useState(<String, bool>{});
final queryClient = useQueryClient();

Expand Down Expand Up @@ -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(
Expand Down
1 change: 0 additions & 1 deletion lib/components/shared/heart_button.dart
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,6 @@ class PlaylistHeartButton extends HookConsumerWidget {
playlist.id!,
refreshQueries: [
isLikedQuery.key,
"current-user-playlists",
],
);

Expand Down
8 changes: 8 additions & 0 deletions lib/services/mutations/playlist.dart
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -9,7 +10,9 @@ class PlaylistMutations {
WidgetRef ref,
String playlistId, {
List<String>? refreshQueries,
List<String>? refreshInfiniteQueries,
}) {
final queryClient = useQueryClient();
return useSpotifyMutation<bool, dynamic, bool, dynamic>(
"toggle-playlist-like/$playlistId",
(isLiked, spotify) async {
Expand All @@ -22,6 +25,11 @@ class PlaylistMutations {
},
ref: ref,
refreshQueries: refreshQueries,
refreshInfiniteQueries: refreshInfiniteQueries,
onData: (data, recoveryData) async {
await queryClient
.refreshInfiniteQueryAllPages("current-user-playlists");
},
);
}

Expand Down
3 changes: 2 additions & 1 deletion lib/services/queries/album.dart
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ class AlbumQueries {
return useSpotifyQuery<bool, dynamic>(
"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,
);
Expand Down
14 changes: 10 additions & 4 deletions lib/services/queries/playlist.dart
Original file line number Diff line number Diff line change
Expand Up @@ -126,12 +126,18 @@ class PlaylistQueries {
);
}

Query<Iterable<PlaylistSimple>, dynamic> ofMine(WidgetRef ref) {
return useSpotifyQuery<Iterable<PlaylistSimple>, dynamic>(
InfiniteQuery<Page<PlaylistSimple>, dynamic, int> ofMine(WidgetRef ref) {
return useSpotifyInfiniteQuery<Page<PlaylistSimple>, 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,
);
}
Expand Down

0 comments on commit e7c6813

Please sign in to comment.