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: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) {
final userPlaylistsQuery = useQueries.playlist.ofMineAll(ref);
final me = useQueries.user.me(ref);
final userPlaylistsQuery = ref.watch(favoritePlaylistsProvider);
final me = ref.watch(meProvider);
return useMemoized(
() =>
userPlaylistsQuery.data?.any((e) =>
userPlaylistsQuery.value?.items.any((e) =>
e.id == playlistId &&
me.data != null &&
e.owner?.id == me.data?.id) ??
me.value != null &&
e.owner?.id == me.value?.id) ??
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_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.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_props.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/infinite_query.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/provider/spotify/spotify.dart';
import 'package:spotube/utils/type_conversion_utils.dart';
class AlbumPage extends HookConsumerWidget {
@ -21,26 +16,10 @@ class AlbumPage extends HookConsumerWidget {
@override
Widget build(BuildContext context, ref) {
final spotify = ref.watch(spotifyProvider);
final tracksQuery = useQueries.album.tracksOf(ref, album);
final tracks = useMemoized(() {
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");
},
);
final tracks = ref.watch(albumTracksProvider(album));
final tracksNotifier = ref.watch(albumTracksProvider(album).notifier);
final favoriteAlbumsNotifier = ref.watch(favoriteAlbumsProvider.notifier);
final isSavedAlbum = ref.watch(albumsIsSavedProvider(album.id!));
return InheritedTrackView(
collectionId: album.id!,
@ -51,29 +30,33 @@ class AlbumPage extends HookConsumerWidget {
title: album.name!,
description:
"${context.l10n.released}${album.releaseDate}${album.artists!.first.name}",
tracks: tracks,
pagination: PaginationProps.fromQuery(
tracksQuery,
onFetchAll: () {
return tracksQuery.fetchAllTracks(getAllTracks: () async {
final res = await spotify.albums.tracks(album.id!).all();
return res
.map((track) =>
TypeConversionUtils.simpleTrack_X_Track(track, album))
.toList();
});
tracks: tracks.value?.items ?? [],
pagination: PaginationProps(
hasNextPage: tracks.value?.hasMore ?? false,
isLoading: tracks.isLoading,
onFetchMore: () async {
await tracksNotifier.fetchMore();
},
onFetchAll: () async {
return tracksNotifier.fetchAll();
},
onRefresh: () async {
ref.invalidate(albumTracksProvider(album));
},
),
routePath: "/album/${album.id}",
shareUrl: album.externalUrls!.spotify!,
isLiked: isLiked,
onHeart: albumIsSaved.hasData
? () async {
await toggleAlbumLike.mutate(isLiked);
isLiked: isSavedAlbum.value ?? false,
onHeart: isSavedAlbum.value == null
? null
: () async {
if (isSavedAlbum.value!) {
await favoriteAlbumsNotifier.removeFavorites([album.id!]);
} else {
await favoriteAlbumsNotifier.addFavorites([album.id!]);
}
return null;
}
: null,
},
child: const TrackView(),
);
}

View File

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

View File

@ -1,5 +1,4 @@
import 'package:flutter/material.dart' hide Page;
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.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_props.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/infinite_query.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/provider/spotify/spotify.dart';
import 'package:spotube/utils/type_conversion_utils.dart';
class PlaylistPage extends HookConsumerWidget {
@ -22,31 +18,13 @@ class PlaylistPage extends HookConsumerWidget {
@override
Widget build(BuildContext context, ref) {
final spotify = ref.watch(spotifyProvider);
final tracksQuery = useQueries.playlist.tracksOfQuery(ref, playlist.id!);
final tracks = useMemoized(
() {
return tracksQuery.pages.expand((page) => page).toList();
},
[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 tracks = ref.watch(playlistTracksProvider(playlist.id!));
final tracksNotifier =
ref.watch(playlistTracksProvider(playlist.id!).notifier);
final isFavoritePlaylist =
ref.watch(isFavoritePlaylistProvider(playlist.id!));
final favoritePlaylistsNotifier =
ref.watch(favoritePlaylistsProvider.notifier);
final isUserPlaylist = useIsUserPlaylist(ref, playlist.id!);
@ -56,42 +34,42 @@ class PlaylistPage extends HookConsumerWidget {
playlist.images,
placeholder: ImagePlaceholder.collection,
),
pagination: PaginationProps.fromQuery(
tracksQuery,
onFetchAll: () {
return tracksQuery.fetchAllTracks(
getAllTracks: () async {
final res = await spotify.playlists
.getTracksByPlaylistId(playlist.id!)
.all();
return res.toList();
},
);
pagination: PaginationProps(
hasNextPage: tracks.value?.hasMore ?? false,
isLoading: tracks.isLoading,
onFetchMore: tracksNotifier.fetchMore,
onRefresh: () async {
ref.invalidate(playlistTracksProvider(playlist.id!));
},
onFetchAll: () async {
return await tracksNotifier.fetchAll();
},
),
title: playlist.name!,
description: playlist.description,
tracks: tracks,
tracks: tracks.value?.items ?? [],
routePath: '/playlist/${playlist.id}',
isLiked: isLikedQuery.data ?? false,
isLiked: isFavoritePlaylist.value ?? false,
shareUrl: playlist.externalUrls?.spotify ?? "",
onHeart: () async {
if (!isLikedQuery.hasData || togglePlaylistLike.isMutating) {
return false;
}
final confirmed = isUserPlaylist
? await showPromptDialog(
context: context,
title: context.l10n.delete_playlist,
message: context.l10n.delete_playlist_confirmation,
)
: true;
if (confirmed) {
await togglePlaylistLike.mutate(isLikedQuery.data!);
return isUserPlaylist;
}
return null;
},
onHeart: isFavoritePlaylist.value == null
? null
: () async {
final confirmed = isUserPlaylist
? await showPromptDialog(
context: context,
title: context.l10n.delete_playlist,
message: context.l10n.delete_playlist_confirmation,
)
: true;
if (!confirmed) return null;
if (isFavoritePlaylist.value!) {
await favoritePlaylistsNotifier.removeFavorite(playlist);
} else {
await favoritePlaylistsNotifier.addFavorite(playlist);
}
return isUserPlaylist;
},
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 {
@ -69,6 +73,10 @@ class FavoriteAlbumNotifier
.toList(),
);
});
for (final id in ids) {
ref.invalidate(albumsIsSavedProvider(id));
}
}
}

View File

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

View File

@ -52,21 +52,25 @@ class FavoritePlaylistsNotifier
}
Future<void> addFavorite(PlaylistSimple playlist) async {
update((state) async {
await update((state) async {
await spotify.playlists.followPlaylist(playlist.id!);
return state.copyWith(
items: [...state.items, playlist],
);
});
ref.invalidate(isFavoritePlaylistProvider(playlist.id!));
}
Future<void> removeFavorite(PlaylistSimple playlist) async {
update((state) async {
await update((state) async {
await spotify.playlists.unfollowPlaylist(playlist.id!);
return state.copyWith(
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!,
);
ref.refresh(playlistTracksProvider(state.value!.id!));
ref.invalidate(playlistTracksProvider(state.value!.id!));
}
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<
@ -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;
}
}