feat: use new providers for playlist and albums screen

This commit is contained in:
Kingkor Roy Tirtho 2024-03-17 10:49:17 +06:00
parent dc999b81f0
commit 84cb8d7988
9 changed files with 152 additions and 126 deletions

View File

@ -1,18 +1,18 @@
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/services/queries/queries.dart'; import 'package:spotube/provider/spotify/spotify.dart';
bool useIsUserPlaylist(WidgetRef ref, String playlistId) { bool useIsUserPlaylist(WidgetRef ref, String playlistId) {
final userPlaylistsQuery = useQueries.playlist.ofMineAll(ref); final userPlaylistsQuery = ref.watch(favoritePlaylistsProvider);
final me = useQueries.user.me(ref); final me = ref.watch(meProvider);
return useMemoized( return useMemoized(
() => () =>
userPlaylistsQuery.data?.any((e) => userPlaylistsQuery.value?.items.any((e) =>
e.id == playlistId && e.id == playlistId &&
me.data != null && me.value != null &&
e.owner?.id == me.data?.id) ?? e.owner?.id == me.value?.id) ??
false, false,
[userPlaylistsQuery.data, playlistId, me.data], [userPlaylistsQuery.value, playlistId, me.value],
); );
} }

View File

@ -1,15 +1,10 @@
import 'package:fl_query_hooks/fl_query_hooks.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/components/shared/tracks_view/track_view.dart'; import 'package:spotube/components/shared/tracks_view/track_view.dart';
import 'package:spotube/components/shared/tracks_view/track_view_props.dart'; import 'package:spotube/components/shared/tracks_view/track_view_props.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/infinite_query.dart'; import 'package:spotube/provider/spotify/spotify.dart';
import 'package:spotube/provider/spotify_provider.dart';
import 'package:spotube/services/mutations/mutations.dart';
import 'package:spotube/services/queries/queries.dart';
import 'package:spotube/utils/type_conversion_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart';
class AlbumPage extends HookConsumerWidget { class AlbumPage extends HookConsumerWidget {
@ -21,26 +16,10 @@ class AlbumPage extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final spotify = ref.watch(spotifyProvider); final tracks = ref.watch(albumTracksProvider(album));
final tracksQuery = useQueries.album.tracksOf(ref, album); final tracksNotifier = ref.watch(albumTracksProvider(album).notifier);
final favoriteAlbumsNotifier = ref.watch(favoriteAlbumsProvider.notifier);
final tracks = useMemoized(() { final isSavedAlbum = ref.watch(albumsIsSavedProvider(album.id!));
return tracksQuery.pages.expand((element) => element).toList();
}, [tracksQuery.pages]);
final client = useQueryClient();
final albumIsSaved = useQueries.album.isSavedForMe(ref, album.id!);
final isLiked = albumIsSaved.data ?? false;
final toggleAlbumLike = useMutations.album.toggleFavorite(
ref,
album.id!,
refreshQueries: [albumIsSaved.key],
onData: (_, __) async {
await client.refreshInfiniteQueryAllPages("current-user-albums");
},
);
return InheritedTrackView( return InheritedTrackView(
collectionId: album.id!, collectionId: album.id!,
@ -51,29 +30,33 @@ class AlbumPage extends HookConsumerWidget {
title: album.name!, title: album.name!,
description: description:
"${context.l10n.released}${album.releaseDate}${album.artists!.first.name}", "${context.l10n.released}${album.releaseDate}${album.artists!.first.name}",
tracks: tracks, tracks: tracks.value?.items ?? [],
pagination: PaginationProps.fromQuery( pagination: PaginationProps(
tracksQuery, hasNextPage: tracks.value?.hasMore ?? false,
onFetchAll: () { isLoading: tracks.isLoading,
return tracksQuery.fetchAllTracks(getAllTracks: () async { onFetchMore: () async {
final res = await spotify.albums.tracks(album.id!).all(); await tracksNotifier.fetchMore();
},
return res onFetchAll: () async {
.map((track) => return tracksNotifier.fetchAll();
TypeConversionUtils.simpleTrack_X_Track(track, album)) },
.toList(); onRefresh: () async {
}); ref.invalidate(albumTracksProvider(album));
}, },
), ),
routePath: "/album/${album.id}", routePath: "/album/${album.id}",
shareUrl: album.externalUrls!.spotify!, shareUrl: album.externalUrls!.spotify!,
isLiked: isLiked, isLiked: isSavedAlbum.value ?? false,
onHeart: albumIsSaved.hasData onHeart: isSavedAlbum.value == null
? () async { ? null
await toggleAlbumLike.mutate(isLiked); : () async {
return null; if (isSavedAlbum.value!) {
await favoriteAlbumsNotifier.removeFavorites([album.id!]);
} else {
await favoriteAlbumsNotifier.addFavorites([album.id!]);
} }
: null, return null;
},
child: const TrackView(), child: const TrackView(),
); );
} }

View File

@ -3,7 +3,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/components/shared/tracks_view/track_view.dart'; import 'package:spotube/components/shared/tracks_view/track_view.dart';
import 'package:spotube/components/shared/tracks_view/track_view_props.dart'; import 'package:spotube/components/shared/tracks_view/track_view_props.dart';
import 'package:spotube/services/queries/queries.dart'; import 'package:spotube/provider/spotify/spotify.dart';
class LikedPlaylistPage extends HookConsumerWidget { class LikedPlaylistPage extends HookConsumerWidget {
final PlaylistSimple playlist; final PlaylistSimple playlist;
@ -14,8 +14,8 @@ class LikedPlaylistPage extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final likedTracks = useQueries.playlist.likedTracksQuery(ref); final likedTracks = ref.watch(likedTracksProvider);
final tracks = likedTracks.data ?? <Track>[]; final tracks = likedTracks.value ?? <Track>[];
return InheritedTrackView( return InheritedTrackView(
collectionId: playlist.id!, collectionId: playlist.id!,
@ -28,7 +28,7 @@ class LikedPlaylistPage extends HookConsumerWidget {
return tracks.toList(); return tracks.toList();
}, },
onRefresh: () async { onRefresh: () async {
await likedTracks.refresh(); ref.invalidate(likedTracksProvider);
}, },
), ),
title: playlist.name!, title: playlist.name!,

View File

@ -1,5 +1,4 @@
import 'package:flutter/material.dart' hide Page; import 'package:flutter/material.dart' hide Page;
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/components/shared/dialogs/prompt_dialog.dart'; import 'package:spotube/components/shared/dialogs/prompt_dialog.dart';
@ -7,10 +6,7 @@ import 'package:spotube/components/shared/tracks_view/sections/body/use_is_user_
import 'package:spotube/components/shared/tracks_view/track_view.dart'; import 'package:spotube/components/shared/tracks_view/track_view.dart';
import 'package:spotube/components/shared/tracks_view/track_view_props.dart'; import 'package:spotube/components/shared/tracks_view/track_view_props.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/infinite_query.dart'; import 'package:spotube/provider/spotify/spotify.dart';
import 'package:spotube/provider/spotify_provider.dart';
import 'package:spotube/services/mutations/mutations.dart';
import 'package:spotube/services/queries/queries.dart';
import 'package:spotube/utils/type_conversion_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart';
class PlaylistPage extends HookConsumerWidget { class PlaylistPage extends HookConsumerWidget {
@ -22,31 +18,13 @@ class PlaylistPage extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final spotify = ref.watch(spotifyProvider); final tracks = ref.watch(playlistTracksProvider(playlist.id!));
final tracksQuery = useQueries.playlist.tracksOfQuery(ref, playlist.id!); final tracksNotifier =
ref.watch(playlistTracksProvider(playlist.id!).notifier);
final tracks = useMemoized( final isFavoritePlaylist =
() { ref.watch(isFavoritePlaylistProvider(playlist.id!));
return tracksQuery.pages.expand((page) => page).toList(); final favoritePlaylistsNotifier =
}, ref.watch(favoritePlaylistsProvider.notifier);
[tracksQuery.pages],
);
final me = useQueries.user.me(ref);
final isLikedQuery = useQueries.playlist.doesUserFollow(
ref,
playlist.id!,
me.data?.id ?? '',
);
final togglePlaylistLike = useMutations.playlist.toggleFavorite(
ref,
playlist.id!,
refreshQueries: [
isLikedQuery.key,
],
);
final isUserPlaylist = useIsUserPlaylist(ref, playlist.id!); final isUserPlaylist = useIsUserPlaylist(ref, playlist.id!);
@ -56,29 +34,26 @@ class PlaylistPage extends HookConsumerWidget {
playlist.images, playlist.images,
placeholder: ImagePlaceholder.collection, placeholder: ImagePlaceholder.collection,
), ),
pagination: PaginationProps.fromQuery( pagination: PaginationProps(
tracksQuery, hasNextPage: tracks.value?.hasMore ?? false,
onFetchAll: () { isLoading: tracks.isLoading,
return tracksQuery.fetchAllTracks( onFetchMore: tracksNotifier.fetchMore,
getAllTracks: () async { onRefresh: () async {
final res = await spotify.playlists ref.invalidate(playlistTracksProvider(playlist.id!));
.getTracksByPlaylistId(playlist.id!)
.all();
return res.toList();
}, },
); onFetchAll: () async {
return await tracksNotifier.fetchAll();
}, },
), ),
title: playlist.name!, title: playlist.name!,
description: playlist.description, description: playlist.description,
tracks: tracks, tracks: tracks.value?.items ?? [],
routePath: '/playlist/${playlist.id}', routePath: '/playlist/${playlist.id}',
isLiked: isLikedQuery.data ?? false, isLiked: isFavoritePlaylist.value ?? false,
shareUrl: playlist.externalUrls?.spotify ?? "", shareUrl: playlist.externalUrls?.spotify ?? "",
onHeart: () async { onHeart: isFavoritePlaylist.value == null
if (!isLikedQuery.hasData || togglePlaylistLike.isMutating) { ? null
return false; : () async {
}
final confirmed = isUserPlaylist final confirmed = isUserPlaylist
? await showPromptDialog( ? await showPromptDialog(
context: context, context: context,
@ -86,11 +61,14 @@ class PlaylistPage extends HookConsumerWidget {
message: context.l10n.delete_playlist_confirmation, message: context.l10n.delete_playlist_confirmation,
) )
: true; : true;
if (confirmed) { if (!confirmed) return null;
await togglePlaylistLike.mutate(isLikedQuery.data!);
return isUserPlaylist; if (isFavoritePlaylist.value!) {
await favoritePlaylistsNotifier.removeFavorite(playlist);
} else {
await favoritePlaylistsNotifier.addFavorite(playlist);
} }
return null; return isUserPlaylist;
}, },
child: const TrackView(), child: const TrackView(),
); );

View File

@ -55,6 +55,10 @@ class FavoriteAlbumNotifier
], ],
); );
}); });
for (final id in ids) {
ref.invalidate(albumsIsSavedProvider(id));
}
} }
Future<void> removeFavorites(List<String> ids) async { Future<void> removeFavorites(List<String> ids) async {
@ -69,6 +73,10 @@ class FavoriteAlbumNotifier
.toList(), .toList(),
); );
}); });
for (final id in ids) {
ref.invalidate(albumsIsSavedProvider(id));
}
} }
} }

View File

@ -1,6 +1,6 @@
part of '../spotify.dart'; part of '../spotify.dart';
class AlbumTracksState extends PaginatedState<TrackSimple> { class AlbumTracksState extends PaginatedState<Track> {
AlbumTracksState({ AlbumTracksState({
required super.items, required super.items,
required super.offset, required super.offset,
@ -10,7 +10,7 @@ class AlbumTracksState extends PaginatedState<TrackSimple> {
@override @override
AlbumTracksState copyWith({ AlbumTracksState copyWith({
List<TrackSimple>? items, List<Track>? items,
int? offset, int? offset,
int? limit, int? limit,
bool? hasMore, bool? hasMore,
@ -24,14 +24,17 @@ class AlbumTracksState extends PaginatedState<TrackSimple> {
} }
} }
class AlbumTracksNotifier extends FamilyPaginatedAsyncNotifier<TrackSimple, class AlbumTracksNotifier
AlbumTracksState, String> { extends FamilyPaginatedAsyncNotifier<Track, AlbumTracksState, AlbumSimple> {
AlbumTracksNotifier() : super(); AlbumTracksNotifier() : super();
@override @override
fetch(arg, offset, limit) async { fetch(arg, offset, limit) async {
final tracks = await spotify.albums.tracks(arg).getPage(limit, offset); final tracks = await spotify.albums.tracks(arg.id!).getPage(limit, offset);
return tracks.items?.toList() ?? []; return tracks.items
?.map((e) => TypeConversionUtils.simpleTrack_X_Track(e, arg))
.toList() ??
[];
} }
@override @override
@ -47,7 +50,7 @@ class AlbumTracksNotifier extends FamilyPaginatedAsyncNotifier<TrackSimple,
} }
} }
final albumTracksProvider = final albumTracksProvider = AsyncNotifierProviderFamily<AlbumTracksNotifier,
AsyncNotifierProviderFamily<AlbumTracksNotifier, AlbumTracksState, String>( AlbumTracksState, AlbumSimple>(
() => AlbumTracksNotifier(), () => AlbumTracksNotifier(),
); );

View File

@ -52,21 +52,25 @@ class FavoritePlaylistsNotifier
} }
Future<void> addFavorite(PlaylistSimple playlist) async { Future<void> addFavorite(PlaylistSimple playlist) async {
update((state) async { await update((state) async {
await spotify.playlists.followPlaylist(playlist.id!); await spotify.playlists.followPlaylist(playlist.id!);
return state.copyWith( return state.copyWith(
items: [...state.items, playlist], items: [...state.items, playlist],
); );
}); });
ref.invalidate(isFavoritePlaylistProvider(playlist.id!));
} }
Future<void> removeFavorite(PlaylistSimple playlist) async { Future<void> removeFavorite(PlaylistSimple playlist) async {
update((state) async { await update((state) async {
await spotify.playlists.unfollowPlaylist(playlist.id!); await spotify.playlists.unfollowPlaylist(playlist.id!);
return state.copyWith( return state.copyWith(
items: state.items.where((e) => e.id != playlist.id).toList(), items: state.items.where((e) => e.id != playlist.id).toList(),
); );
}); });
ref.invalidate(isFavoritePlaylistProvider(playlist.id!));
} }
} }

View File

@ -54,7 +54,7 @@ class PlaylistNotifier extends FamilyAsyncNotifier<Playlist, String> {
state.value!.id!, state.value!.id!,
); );
ref.refresh(playlistTracksProvider(state.value!.id!)); ref.invalidate(playlistTracksProvider(state.value!.id!));
} }
Future<void> modify(PlaylistInput input) async { Future<void> modify(PlaylistInput input) async {

View File

@ -80,6 +80,31 @@ abstract class FamilyPaginatedAsyncNotifier<
}, },
); );
} }
Future<List<K>> fetchAll() async {
if (state.value == null) return [];
if (!state.value!.hasMore) return state.value!.items;
bool hasMore = true;
while (hasMore) {
await update((state) async {
final items = await fetch(
arg,
state.offset + state.limit,
state.limit,
);
hasMore = items.length == state.limit;
return state.copyWith(
items: [...state.items, ...items],
offset: state.offset + state.limit,
hasMore: hasMore,
) as T;
});
}
return state.value!.items;
}
} }
abstract class FamilyCursorPaginatedAsyncNotifier< abstract class FamilyCursorPaginatedAsyncNotifier<
@ -111,4 +136,29 @@ abstract class FamilyCursorPaginatedAsyncNotifier<
}, },
); );
} }
Future<List<K>> fetchAll() async {
if (state.value == null) return [];
if (!state.value!.hasMore) return state.value!.items;
bool hasMore = true;
while (hasMore) {
await update((state) async {
final items = await fetch(
arg,
state.offset,
state.limit,
);
hasMore = items.$1.length == state.limit;
return state.copyWith(
items: [...state.items, ...items.$1],
offset: items.$2,
hasMore: hasMore,
) as T;
});
}
return state.value!.items;
}
} }