From d85867a245deb6820977cd36e15ca9b1a4d4f27b Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 24 Feb 2023 13:56:05 +0600 Subject: [PATCH 1/7] feat: initial integration --- lib/collections/routes.dart | 14 +- lib/components/album/album_card.dart | 23 +- lib/components/artist/artist_album_list.dart | 15 +- lib/components/genre/category_card.dart | 15 +- lib/components/library/user_albums.dart | 9 +- lib/components/library/user_artists.dart | 15 +- lib/components/library/user_playlists.dart | 9 +- lib/components/playlist/playlist_card.dart | 19 +- .../playlist/playlist_create_dialog.dart | 13 +- lib/components/root/sidebar.dart | 9 +- .../dialogs/playlist_add_track_dialog.dart | 12 +- lib/components/shared/heart_button.dart | 138 ++++------- .../track_table/track_collection_view.dart | 5 +- .../shared/track_table/track_tile.dart | 23 +- lib/hooks/use_spotify_infinite_query.dart | 47 ++++ lib/hooks/use_spotify_mutation.dart | 36 +++ lib/hooks/use_spotify_query.dart | 46 ++++ lib/main.dart | 6 +- lib/pages/album/album.dart | 7 +- lib/pages/artist/artist.dart | 44 ++-- lib/pages/home/genres.dart | 34 +-- lib/pages/home/personalized.dart | 31 +-- lib/pages/lyrics/genius_lyrics.dart | 23 +- lib/pages/lyrics/synced_lyrics.dart | 230 +++++++++--------- lib/pages/playlist/playlist.dart | 10 +- lib/services/mutations/album.dart | 39 +-- lib/services/mutations/playlist.dart | 65 ++--- lib/services/mutations/track.dart | 44 ++-- lib/services/queries/album.dart | 112 +++++---- lib/services/queries/artist.dart | 140 +++++++---- lib/services/queries/category.dart | 69 ++++-- lib/services/queries/lyrics.dart | 69 +++--- lib/services/queries/playlist.dart | 115 +++++---- lib/services/queries/search.dart | 48 ++-- lib/services/queries/user.dart | 42 ++-- macos/Flutter/GeneratedPluginRegistrant.swift | 2 - pubspec.lock | 80 ++---- pubspec.yaml | 19 +- .../flutter/generated_plugin_registrant.cc | 3 - windows/flutter/generated_plugins.cmake | 1 - 40 files changed, 866 insertions(+), 815 deletions(-) create mode 100644 lib/hooks/use_spotify_infinite_query.dart create mode 100644 lib/hooks/use_spotify_mutation.dart create mode 100644 lib/hooks/use_spotify_query.dart diff --git a/lib/collections/routes.dart b/lib/collections/routes.dart index 5fec09f8d..5fbed70f2 100644 --- a/lib/collections/routes.dart +++ b/lib/collections/routes.dart @@ -16,7 +16,7 @@ import 'package:spotube/pages/lyrics/lyrics.dart'; import 'package:spotube/pages/player/player.dart'; import 'package:spotube/pages/playlist/playlist.dart'; import 'package:spotube/pages/root/root_app.dart'; -import 'package:spotube/pages/search/search.dart'; +// import 'package:spotube/pages/search/search.dart'; import 'package:spotube/pages/settings/settings.dart'; import 'package:spotube/pages/mobile_login/mobile_login.dart'; @@ -33,12 +33,12 @@ final router = GoRouter( path: "/", pageBuilder: (context, state) => SpotubePage(child: const HomePage()), ), - GoRoute( - path: "/search", - name: "Search", - pageBuilder: (context, state) => - SpotubePage(child: const SearchPage()), - ), + // GoRoute( + // path: "/search", + // name: "Search", + // pageBuilder: (context, state) => + // SpotubePage(child: const SearchPage()), + // ), GoRoute( path: "/library", name: "Library", diff --git a/lib/components/album/album_card.dart b/lib/components/album/album_card.dart index 7a1a40a18..8e944c618 100644 --- a/lib/components/album/album_card.dart +++ b/lib/components/album/album_card.dart @@ -7,10 +7,8 @@ import 'package:spotube/components/shared/playbutton_card.dart'; import 'package:spotube/hooks/use_breakpoint_value.dart'; import 'package:spotube/provider/playlist_queue_provider.dart'; import 'package:spotube/provider/spotify_provider.dart'; -import 'package:spotube/services/queries/queries.dart'; import 'package:spotube/utils/service_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; -import 'package:uuid/uuid.dart'; enum AlbumType { album, @@ -48,15 +46,18 @@ class AlbumCard extends HookConsumerWidget { final playing = useStream(PlaylistQueueNotifier.playing).data ?? PlaylistQueueNotifier.isPlaying; final playlistNotifier = ref.watch(PlaylistQueueNotifier.notifier); - final queryBowl = QueryBowl.of(context); - final query = queryBowl.getQuery, SpotifyApi>( - Queries.album.tracksOf(album.id!).queryKey); + final queryBowl = QueryClient.of(context); + final query = queryBowl + .getQuery, SpotifyApi>("album-tracks/${album.id}"); final tracks = useState(query?.data ?? album.tracks ?? []); bool isPlaylistPlaying = playlistNotifier.isPlayingPlaylist(tracks.value); final int marginH = useBreakpointValue(sm: 10, md: 15, lg: 20, xl: 20, xxl: 20); final updating = useState(false); + final spotify = ref.watch(spotifyProvider); + + final scaffold = ScaffoldMessenger.of(context); return PlaybuttonCard( imageUrl: TypeConversionUtils.image_X_UrlString( @@ -100,9 +101,13 @@ class AlbumCard extends HookConsumerWidget { try { final fetchedTracks = await queryBowl.fetchQuery, SpotifyApi>( - Queries.album.tracksOf(album.id!), - externalData: ref.read(spotifyProvider), - key: ValueKey(const Uuid().v4()), + "album-tracks/${album.id}", + () { + return spotify.albums + .getTracks(album.id!) + .all() + .then((value) => value.toList()); + }, ); if (fetchedTracks == null || fetchedTracks.isEmpty) return; @@ -113,7 +118,7 @@ class AlbumCard extends HookConsumerWidget { .toList(), ); tracks.value = fetchedTracks; - ScaffoldMessenger.of(context).showSnackBar( + scaffold.showSnackBar( SnackBar( content: Text("Added ${album.tracks?.length} tracks to queue"), ), diff --git a/lib/components/artist/artist_album_list.dart b/lib/components/artist/artist_album_list.dart index 91fe22065..9d22ae343 100644 --- a/lib/components/artist/artist_album_list.dart +++ b/lib/components/artist/artist_album_list.dart @@ -1,4 +1,3 @@ -import 'package:fl_query_hooks/fl_query_hooks.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart' hide Page; import 'package:flutter_hooks/flutter_hooks.dart'; @@ -8,7 +7,6 @@ import 'package:spotube/components/album/album_card.dart'; import 'package:spotube/components/shared/shimmers/shimmer_playbutton_card.dart'; import 'package:spotube/components/shared/waypoint.dart'; import 'package:spotube/models/logger.dart'; -import 'package:spotube/provider/spotify_provider.dart'; import 'package:spotube/services/queries/queries.dart'; class ArtistAlbumList extends HookConsumerWidget { @@ -23,20 +21,17 @@ class ArtistAlbumList extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { final scrollController = useScrollController(); - final albumsQuery = useInfiniteQuery( - job: Queries.artist.albumsOf(artistId), - externalData: ref.watch(spotifyProvider), - ); + final albumsQuery = Queries.artist.useAlbumsOfQuery(ref, artistId); final albums = useMemoized(() { return albumsQuery.pages - .expand((page) => page?.items ?? const Iterable.empty()) + .expand((page) => page.items ?? const Iterable.empty()) .toList(); }, [albumsQuery.pages]); final hasNextPage = albumsQuery.pages.isEmpty ? false - : (albumsQuery.pages.last?.items?.length ?? 0) == 5; + : (albumsQuery.pages.last.items?.length ?? 0) == 5; return SizedBox( height: 300, @@ -52,9 +47,7 @@ class ArtistAlbumList extends HookConsumerWidget { controller: scrollController, child: Waypoint( controller: scrollController, - onTouchEdge: () { - albumsQuery.fetchNextPage(); - }, + onTouchEdge: albumsQuery.fetchNext, child: ListView.builder( itemCount: albums.length, controller: scrollController, diff --git a/lib/components/genre/category_card.dart b/lib/components/genre/category_card.dart index 5e3a3139d..0f4ed2793 100644 --- a/lib/components/genre/category_card.dart +++ b/lib/components/genre/category_card.dart @@ -1,4 +1,3 @@ -import 'package:fl_query_hooks/fl_query_hooks.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart' hide Page; import 'package:flutter_hooks/flutter_hooks.dart'; @@ -25,17 +24,17 @@ class CategoryCard extends HookConsumerWidget { Widget build(BuildContext context, ref) { final scrollController = useScrollController(); final spotify = ref.watch(spotifyProvider); - final playlistQuery = useInfiniteQuery( - job: Queries.category.playlistsOf(category.id!), - externalData: spotify, + final playlistQuery = Queries.category.usePlaylistsOf( + ref, + category.id!, ); final hasNextPage = playlistQuery.pages.isEmpty ? false - : (playlistQuery.pages.last?.items?.length ?? 0) == 5; + : (playlistQuery.pages.last.items?.length ?? 0) == 5; final playlists = playlistQuery.pages .expand( - (page) => page?.items ?? const Iterable.empty(), + (page) => page.items ?? const Iterable.empty(), ) .toList(); @@ -49,7 +48,7 @@ class CategoryCard extends HookConsumerWidget { ], ), ), - playlistQuery.hasError + playlistQuery.hasErrors ? PlatformText( "Something Went Wrong\n${playlistQuery.errors.first}") : SizedBox( @@ -67,7 +66,7 @@ class CategoryCard extends HookConsumerWidget { child: Waypoint( controller: scrollController, onTouchEdge: () { - playlistQuery.fetchNextPage(); + playlistQuery.fetchNext(); }, child: ListView( scrollDirection: Axis.horizontal, diff --git a/lib/components/library/user_albums.dart b/lib/components/library/user_albums.dart index db58ddd72..8cef38400 100644 --- a/lib/components/library/user_albums.dart +++ b/lib/components/library/user_albums.dart @@ -1,4 +1,3 @@ -import 'package:fl_query_hooks/fl_query_hooks.dart'; import 'package:flutter/material.dart' hide Image; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:collection/collection.dart'; @@ -12,7 +11,6 @@ import 'package:spotube/components/shared/shimmers/shimmer_playbutton_card.dart' import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart'; import 'package:spotube/hooks/use_breakpoint_value.dart'; import 'package:spotube/provider/authentication_provider.dart'; -import 'package:spotube/provider/spotify_provider.dart'; import 'package:spotube/services/queries/queries.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; @@ -24,10 +22,7 @@ class UserAlbums extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { final auth = ref.watch(AuthenticationNotifier.provider); - final albumsQuery = useQuery( - job: Queries.album.ofMine, - externalData: ref.watch(spotifyProvider), - ); + final albumsQuery = Queries.album.useOfMineQuery(ref); final spacing = useBreakpointValue( sm: 0, @@ -64,7 +59,7 @@ class UserAlbums extends HookConsumerWidget { return RefreshIndicator( onRefresh: () async { - await albumsQuery.refetch(); + await albumsQuery.refresh(); }, child: SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics(), diff --git a/lib/components/library/user_artists.dart b/lib/components/library/user_artists.dart index 8c8d95d99..c9dd56723 100644 --- a/lib/components/library/user_artists.dart +++ b/lib/components/library/user_artists.dart @@ -1,4 +1,3 @@ -import 'package:fl_query_hooks/fl_query_hooks.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:collection/collection.dart'; @@ -11,7 +10,6 @@ import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart'; import 'package:spotube/components/shared/waypoint.dart'; import 'package:spotube/components/artist/artist_card.dart'; import 'package:spotube/provider/authentication_provider.dart'; -import 'package:spotube/provider/spotify_provider.dart'; import 'package:spotube/services/queries/queries.dart'; import 'package:tuple/tuple.dart'; @@ -22,20 +20,17 @@ class UserArtists extends HookConsumerWidget { Widget build(BuildContext context, ref) { final auth = ref.watch(AuthenticationNotifier.provider); - final artistQuery = useInfiniteQuery( - job: Queries.artist.followedByMe, - externalData: ref.watch(spotifyProvider), - ); + final artistQuery = Queries.artist.useFollowedByMeQuery(ref); final hasNextPage = artistQuery.pages.isEmpty ? false - : (artistQuery.pages.last?.items?.length ?? 0) == 15; + : (artistQuery.pages.last.items?.length ?? 0) == 15; final searchText = useState(''); final filteredArtists = useMemoized(() { final artists = artistQuery.pages - .expand((page) => page?.items ?? const Iterable.empty()); + .expand((page) => page.items ?? const Iterable.empty()); if (searchText.value.isEmpty) { return artists.toList(); @@ -85,7 +80,7 @@ class UserArtists extends HookConsumerWidget { ) : RefreshIndicator( onRefresh: () async { - await artistQuery.refetchPages(); + await artistQuery.refreshAll(); }, child: GridView.builder( itemCount: filteredArtists.length, @@ -104,7 +99,7 @@ class UserArtists extends HookConsumerWidget { controller: useScrollController(), isGrid: true, onTouchEdge: () { - artistQuery.fetchNextPage(); + artistQuery.fetchNext(); }, child: ArtistCard(filteredArtists[index]), ); diff --git a/lib/components/library/user_playlists.dart b/lib/components/library/user_playlists.dart index 2ff691cdc..7ab0020d3 100644 --- a/lib/components/library/user_playlists.dart +++ b/lib/components/library/user_playlists.dart @@ -1,4 +1,3 @@ -import 'package:fl_query_hooks/fl_query_hooks.dart'; import 'package:flutter/material.dart' hide Image; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:fuzzywuzzy/fuzzywuzzy.dart'; @@ -15,7 +14,6 @@ import 'package:spotube/components/playlist/playlist_card.dart'; import 'package:spotube/hooks/use_breakpoint_value.dart'; import 'package:spotube/hooks/use_breakpoints.dart'; import 'package:spotube/provider/authentication_provider.dart'; -import 'package:spotube/provider/spotify_provider.dart'; import 'package:spotube/services/queries/queries.dart'; import 'package:tuple/tuple.dart'; @@ -35,10 +33,7 @@ class UserPlaylists extends HookConsumerWidget { : PlaybuttonCardViewType.square; final auth = ref.watch(AuthenticationNotifier.provider); - final playlistsQuery = useQuery( - job: Queries.playlist.ofMine, - externalData: ref.watch(spotifyProvider), - ); + final playlistsQuery = Queries.playlist.useOfMineQuery(ref); Image image = Image(); image.height = 300; @@ -90,7 +85,7 @@ class UserPlaylists extends HookConsumerWidget { .toList(), ]; return RefreshIndicator( - onRefresh: () => playlistsQuery.refetch(), + onRefresh: playlistsQuery.refresh, child: SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics(), child: Material( diff --git a/lib/components/playlist/playlist_card.dart b/lib/components/playlist/playlist_card.dart index acb16fc47..b44180e4e 100644 --- a/lib/components/playlist/playlist_card.dart +++ b/lib/components/playlist/playlist_card.dart @@ -10,7 +10,6 @@ import 'package:spotube/provider/spotify_provider.dart'; import 'package:spotube/services/queries/queries.dart'; import 'package:spotube/utils/service_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; -import 'package:uuid/uuid.dart'; class PlaylistCard extends HookConsumerWidget { final PlaylistSimple playlist; @@ -26,9 +25,9 @@ class PlaylistCard extends HookConsumerWidget { final playlistNotifier = ref.watch(PlaylistQueueNotifier.notifier); final playing = useStream(PlaylistQueueNotifier.playing).data ?? PlaylistQueueNotifier.isPlaying; - final queryBowl = QueryBowl.of(context); + final queryBowl = QueryClient.of(context); final query = queryBowl.getQuery, SpotifyApi>( - Queries.playlist.tracksOf(playlist.id!).queryKey, + "playlist-tracks/${playlist.id}", ); final tracks = useState(query?.data ?? []); bool isPlaylistPlaying = playlistNotifier.isPlayingPlaylist(tracks.value); @@ -37,6 +36,8 @@ class PlaylistCard extends HookConsumerWidget { useBreakpointValue(sm: 10, md: 15, lg: 20, xl: 20, xxl: 20); final updating = useState(false); + final spotify = ref.watch(spotifyProvider); + final scaffold = ScaffoldMessenger.of(context); return PlaybuttonCard( viewType: viewType, @@ -66,9 +67,8 @@ class PlaylistCard extends HookConsumerWidget { } List fetchedTracks = await queryBowl.fetchQuery( - key: ValueKey(const Uuid().v4()), - Queries.playlist.tracksOf(playlist.id!), - externalData: ref.read(spotifyProvider), + "playlist-tracks/${playlist.id}", + () => Queries.playlist.tracksOf(playlist.id!, spotify), ) ?? []; @@ -85,9 +85,8 @@ class PlaylistCard extends HookConsumerWidget { try { if (isPlaylistPlaying) return; List fetchedTracks = await queryBowl.fetchQuery( - key: ValueKey(const Uuid().v4()), - Queries.playlist.tracksOf(playlist.id!), - externalData: ref.read(spotifyProvider), + "playlist-tracks/${playlist.id}", + () => Queries.playlist.tracksOf(playlist.id!, spotify), ) ?? []; @@ -95,7 +94,7 @@ class PlaylistCard extends HookConsumerWidget { playlistNotifier.add(fetchedTracks); tracks.value = fetchedTracks; - ScaffoldMessenger.of(context).showSnackBar( + scaffold.showSnackBar( SnackBar( content: Text("Added ${fetchedTracks.length} tracks to queue"), ), diff --git a/lib/components/playlist/playlist_create_dialog.dart b/lib/components/playlist/playlist_create_dialog.dart index c12bb5e6a..9dad8bc39 100644 --- a/lib/components/playlist/playlist_create_dialog.dart +++ b/lib/components/playlist/playlist_create_dialog.dart @@ -1,4 +1,4 @@ -import 'package:fl_query/fl_query.dart'; +import 'package:fl_query_hooks/fl_query_hooks.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; @@ -7,7 +7,6 @@ import 'package:platform_ui/platform_ui.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/root/sidebar.dart'; import 'package:spotube/provider/spotify_provider.dart'; -import 'package:spotube/services/queries/queries.dart'; class PlaylistCreateDialog extends HookConsumerWidget { const PlaylistCreateDialog({Key? key}) : super(key: key); @@ -28,6 +27,8 @@ class PlaylistCreateDialog extends HookConsumerWidget { final description = useTextEditingController(); final public = useState(false); final collaborative = useState(false); + final client = useQueryClient(); + final navigator = Navigator.of(context); onCreate() async { if (playlistName.text.isEmpty) return; @@ -39,12 +40,12 @@ class PlaylistCreateDialog extends HookConsumerWidget { public: public.value, description: description.text, ); - await QueryBowl.of(context) + await client .getQuery( - Queries.playlist.ofMine.queryKey, + "current-user-playlists", ) - ?.refetch(); - Navigator.pop(context); + ?.refresh(); + navigator.pop(); } return PlatformAlertDialog( diff --git a/lib/components/root/sidebar.dart b/lib/components/root/sidebar.dart index 4f5dff8ca..8cc854b5b 100644 --- a/lib/components/root/sidebar.dart +++ b/lib/components/root/sidebar.dart @@ -1,5 +1,4 @@ import 'package:badges/badges.dart'; -import 'package:fl_query_hooks/fl_query_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -196,10 +195,7 @@ class SidebarFooter extends HookConsumerWidget { child: HookBuilder( builder: (context) { var spotify = ref.watch(spotifyProvider); - final me = useQuery( - job: Queries.user.me, - externalData: spotify, - ); + final me = Queries.user.useMe(ref); final data = me.data; final avatarImg = TypeConversionUtils.image_X_UrlString( @@ -214,8 +210,7 @@ class SidebarFooter extends HookConsumerWidget { useEffect(() { if (auth != null && me.hasError) { - me.setExternalData(spotify); - me.refetch(); + me.refresh(); } return null; }, [auth, me.hasError]); diff --git a/lib/components/shared/dialogs/playlist_add_track_dialog.dart b/lib/components/shared/dialogs/playlist_add_track_dialog.dart index dbb1f4a6a..06ed49bae 100644 --- a/lib/components/shared/dialogs/playlist_add_track_dialog.dart +++ b/lib/components/shared/dialogs/playlist_add_track_dialog.dart @@ -1,6 +1,4 @@ -import 'package:fl_query_hooks/fl_query_hooks.dart'; import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:platform_ui/platform_ui.dart'; @@ -18,14 +16,8 @@ class PlaylistAddTrackDialog extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { final spotify = ref.watch(spotifyProvider); - final userPlaylists = useQuery( - job: Queries.playlist.ofMine, - externalData: spotify, - ); - final me = useQuery( - job: Queries.user.me, - externalData: spotify, - ); + final userPlaylists = Queries.playlist.useOfMineQuery(ref); + final me = Queries.user.useMe(ref); final filteredPlaylists = userPlaylists.data?.where( (playlist) => playlist.owner?.id != null && playlist.owner!.id == me.data?.id, diff --git a/lib/components/shared/heart_button.dart b/lib/components/shared/heart_button.dart index 49428c27c..42bb117b6 100644 --- a/lib/components/shared/heart_button.dart +++ b/lib/components/shared/heart_button.dart @@ -1,5 +1,4 @@ import 'package:fl_query/fl_query.dart'; -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'; @@ -8,7 +7,6 @@ import 'package:spotify/spotify.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/hooks/use_palette_color.dart'; import 'package:spotube/provider/authentication_provider.dart'; -import 'package:spotube/provider/spotify_provider.dart'; import 'package:spotube/services/mutations/mutations.dart'; import 'package:spotube/services/queries/queries.dart'; @@ -47,59 +45,36 @@ class HeartButton extends ConsumerWidget { } } -Tuple3>, Query> +Tuple3, Query> useTrackToggleLike(Track track, WidgetRef ref) { - final me = - useQuery(job: Queries.user.me, externalData: ref.watch(spotifyProvider)); + final me = Queries.user.useMe(ref); - final savedTracks = useQuery( - job: Queries.playlist.tracksOf("user-liked-tracks"), - externalData: ref.watch(spotifyProvider), - ); + final savedTracks = + Queries.playlist.useTracksOfQuery(ref, "user-liked-tracks"); final isLiked = savedTracks.data?.map((track) => track.id).contains(track.id) ?? false; final mounted = useIsMounted(); - final toggleTrackLike = useMutation>( - job: Mutations.track.toggleFavorite(track.id!), - onMutate: (variable) { - savedTracks.setQueryData( - (oldData) { - if (!variable.item2) { - return [...(oldData ?? []), track]; - } - - return oldData - ?.where( - (element) => element.id != track.id, - ) - .toList() ?? - []; - }, - ); - return track; - }, - onData: (payload, variables, _) { - if (!mounted()) return; - savedTracks.refetch(); + final toggleTrackLike = Mutations.track.useToggleFavorite( + ref, + track.id!, + onMutate: (variables) { + return variables; }, - onError: (payload, variables, queryContext) { + onError: (payload, isLiked) { if (!mounted()) return; - savedTracks.setQueryData( - (oldData) { - if (variables.item2) { - return [...(oldData ?? []), track]; - } - return oldData - ?.where( - (element) => element.id != track.id, - ) - .toList() ?? - []; - }, + savedTracks.setData( + isLiked == true + ? [...(savedTracks.data ?? []), track] + : savedTracks.data + ?.where( + (element) => element.id != track.id, + ) + .toList() ?? + [], ); }, ); @@ -116,10 +91,8 @@ class TrackHeartButton extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { - final savedTracks = useQuery( - job: Queries.playlist.tracksOf("user-liked-tracks"), - externalData: ref.watch(spotifyProvider), - ); + final savedTracks = + Queries.playlist.useTracksOfQuery(ref, "user-liked-tracks"); final toggler = useTrackToggleLike(track, ref); if (toggler.item3.isLoading || !toggler.item3.hasData) { return const PlatformCircularProgressIndicator(); @@ -130,9 +103,7 @@ class TrackHeartButton extends HookConsumerWidget { isLiked: toggler.item1, onPressed: savedTracks.hasData ? () { - toggler.item2.mutate( - Tuple2(ref.read(spotifyProvider), toggler.item1), - ); + toggler.item2.mutate(toggler.item1); } : null, ); @@ -149,26 +120,21 @@ class PlaylistHeartButton extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { - final me = useQuery( - job: Queries.user.me, - externalData: ref.watch(spotifyProvider), - ); + final me = Queries.user.useMe(ref); - final job = - Queries.playlist.doesUserFollow("${playlist.id}:${me.data?.id}"); - final isLikedQuery = useQuery( - job: job, - externalData: ref.watch(spotifyProvider), + final isLikedQuery = Queries.playlist.useDoesUserFollowQuery( + ref, + playlist.id!, + me.data!.id!, ); - final togglePlaylistLike = useMutation>( - job: Mutations.playlist.toggleFavorite(playlist.id!), - onData: (payload, variables, queryContext) async { - await isLikedQuery.refetch(); - await QueryBowl.of(context) - .getQuery(Queries.playlist.ofMine.queryKey) - ?.refetch(); - }, + final togglePlaylistLike = Mutations.playlist.useToggleFavorite( + ref, + playlist.id!, + refreshQueries: [ + isLikedQuery.key, + "current-user-playlists", + ], ); final titleImage = useMemoized( @@ -195,12 +161,7 @@ class PlaylistHeartButton extends HookConsumerWidget { color: color?.titleTextColor, onPressed: isLikedQuery.hasData ? () { - togglePlaylistLike.mutate( - Tuple2( - ref.read(spotifyProvider), - isLikedQuery.data!, - ), - ); + togglePlaylistLike.mutate(isLikedQuery.data!); } : null, ); @@ -217,26 +178,18 @@ class AlbumHeartButton extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { - final spotify = ref.watch(spotifyProvider); - final me = useQuery( - job: Queries.user.me, - externalData: spotify, - ); + final me = Queries.user.useMe(ref); - final albumIsSaved = useQuery( - job: Queries.album.isSavedForMe(album.id!), - externalData: spotify, - ); + final albumIsSaved = Queries.album.useIsSavedForMeQuery(ref, album.id!); final isLiked = albumIsSaved.data ?? false; - final toggleAlbumLike = useMutation>( - job: Mutations.album.toggleFavorite(album.id!), - onData: (payload, variables, queryContext) { - albumIsSaved.refetch(); - QueryBowl.of(context) - .getQuery(Queries.album.ofMine.queryKey) - ?.refetch(); - }, + final toggleAlbumLike = Mutations.album.useToggleFavorite( + ref, + album.id!, + refreshQueries: [ + albumIsSaved.key, + "current-user-albums", + ], ); if (me.isLoading || !me.hasData) { @@ -248,8 +201,7 @@ class AlbumHeartButton extends HookConsumerWidget { tooltip: isLiked ? "Remove from Favorite" : "Add to Favorite", onPressed: albumIsSaved.hasData ? () { - toggleAlbumLike - .mutate(Tuple2(ref.read(spotifyProvider), isLiked)); + toggleAlbumLike.mutate(isLiked); } : null, ); diff --git a/lib/components/shared/track_table/track_collection_view.dart b/lib/components/shared/track_table/track_collection_view.dart index 3be7773ab..a0e25a548 100644 --- a/lib/components/shared/track_table/track_collection_view.dart +++ b/lib/components/shared/track_table/track_collection_view.dart @@ -218,7 +218,7 @@ class TrackCollectionView extends HookConsumerWidget { : null, body: RefreshIndicator( onRefresh: () async { - await tracksSnapshot.refetch(); + await tracksSnapshot.refresh(); }, child: CustomScrollView( controller: controller, @@ -333,8 +333,7 @@ class TrackCollectionView extends HookConsumerWidget { builder: (context) { if (tracksSnapshot.isLoading || !tracksSnapshot.hasData) { return const ShimmerTrackTile(); - } else if (tracksSnapshot.hasError && - tracksSnapshot.isError) { + } else if (tracksSnapshot.hasError) { return SliverToBoxAdapter( child: PlatformText("Error ${tracksSnapshot.error}")); } diff --git a/lib/components/shared/track_table/track_tile.dart b/lib/components/shared/track_table/track_tile.dart index 615d453ba..671549c26 100644 --- a/lib/components/shared/track_table/track_tile.dart +++ b/lib/components/shared/track_table/track_tile.dart @@ -1,6 +1,4 @@ import 'package:auto_size_text/auto_size_text.dart'; -import 'package:fl_query/fl_query.dart'; -import 'package:fl_query_hooks/fl_query_hooks.dart'; import 'package:flutter/material.dart' hide Action; import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; @@ -21,10 +19,8 @@ import 'package:spotube/provider/blacklist_provider.dart'; import 'package:spotube/provider/spotify_provider.dart'; import 'package:spotube/provider/playlist_queue_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:tuple/tuple.dart'; class TrackTile extends HookConsumerWidget { final PlaylistQueue? playlist; @@ -77,16 +73,9 @@ class TrackTile extends HookConsumerWidget { final playlistQueueNotifier = ref.watch(PlaylistQueueNotifier.notifier); final removingTrack = useState(null); - final removeTrack = useMutation>( - job: Mutations.playlist.removeTrackOf(playlistId ?? ""), - onData: (payload, variables, ctx) { - if (playlistId == null || !payload) return; - QueryBowl.of(context) - .getQuery( - Queries.playlist.tracksOf(playlistId!).queryKey, - ) - ?.refetch(); - }, + final removeTrack = Mutations.playlist.useRemoveTrackOf( + ref, + playlistId ?? "", ); void actionShare(Track track) { @@ -359,7 +348,7 @@ class TrackTile extends HookConsumerWidget { : const Icon(SpotubeIcons.heart), text: const PlatformText("Save as favorite"), onPressed: () { - toggler.item2.mutate(Tuple2(spotify, toggler.item1)); + toggler.item2.mutate(toggler.item1); }, ), if (auth != null) @@ -370,7 +359,7 @@ class TrackTile extends HookConsumerWidget { ), if (userPlaylist && auth != null) Action( - icon: (removeTrack.isLoading || !removeTrack.hasData) && + icon: (removeTrack.isMutating || !removeTrack.hasData) && removingTrack.value == track.value.uri ? const Center( child: PlatformCircularProgressIndicator(), @@ -379,7 +368,7 @@ class TrackTile extends HookConsumerWidget { text: const PlatformText("Remove from playlist"), onPressed: () { removingTrack.value = track.value.uri; - removeTrack.mutate(Tuple2(spotify, track.value.uri!)); + removeTrack.mutate(track.value.uri!); }, ), Action( diff --git a/lib/hooks/use_spotify_infinite_query.dart b/lib/hooks/use_spotify_infinite_query.dart new file mode 100644 index 000000000..0507a9f8f --- /dev/null +++ b/lib/hooks/use_spotify_infinite_query.dart @@ -0,0 +1,47 @@ +import 'dart:async'; + +import 'package:fl_query/fl_query.dart'; +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/provider/spotify_provider.dart'; + +InfiniteQuery + useSpotifyInfiniteQuery( + String queryKey, + FutureOr Function(PageType page, SpotifyApi spotify) queryFn, { + required WidgetRef ref, + required InfiniteQueryNextPage nextPage, + required PageType initialPage, + RetryConfig retryConfig = DefaultConstants.retryConfig, + RefreshConfig refreshConfig = DefaultConstants.refreshConfig, + JsonConfig? jsonConfig, + ValueChanged>? onData, + ValueChanged>? onError, + bool enabled = true, + List? keys, +}) { + final spotify = ref.watch(spotifyProvider); + final query = useInfiniteQuery( + queryKey, + (page) => queryFn(page, spotify), + nextPage: nextPage, + initialPage: initialPage, + retryConfig: retryConfig, + refreshConfig: refreshConfig, + jsonConfig: jsonConfig, + onData: onData, + onError: onError, + enabled: enabled, + keys: keys, + ); + + useEffect(() { + query.refreshAll(); + return null; + }, [spotify]); + + return query; +} diff --git a/lib/hooks/use_spotify_mutation.dart b/lib/hooks/use_spotify_mutation.dart new file mode 100644 index 000000000..7dd9d84e3 --- /dev/null +++ b/lib/hooks/use_spotify_mutation.dart @@ -0,0 +1,36 @@ +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:spotify/spotify.dart'; +import 'package:spotube/provider/spotify_provider.dart'; + +Mutation + useSpotifyMutation( + String mutationKey, + Future Function(VariablesType variables, SpotifyApi spotify) + mutationFn, { + required WidgetRef ref, + RetryConfig retryConfig = DefaultConstants.retryConfig, + MutationOnDataFn? onData, + MutationOnErrorFn? onError, + MutationOnMutationFn? onMutate, + List? refreshQueries, + List? refreshInfiniteQueries, + List? keys, +}) { + final spotify = ref.watch(spotifyProvider); + final mutation = + useMutation( + mutationKey, + (variables) => mutationFn(variables, spotify), + retryConfig: retryConfig, + onData: onData, + onError: onError, + onMutate: onMutate, + refreshQueries: refreshQueries, + refreshInfiniteQueries: refreshInfiniteQueries, + keys: keys, + ); + + return mutation; +} diff --git a/lib/hooks/use_spotify_query.dart b/lib/hooks/use_spotify_query.dart new file mode 100644 index 000000000..700698d6e --- /dev/null +++ b/lib/hooks/use_spotify_query.dart @@ -0,0 +1,46 @@ +import 'dart:async'; + +import 'package:fl_query/fl_query.dart'; +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/provider/spotify_provider.dart'; + +typedef SpotifyQueryFn = FutureOr Function( + SpotifyApi spotify); + +Query useSpotifyQuery( + final String queryKey, + final SpotifyQueryFn queryFn, { + required WidgetRef ref, + final DataType? initial, + final RetryConfig retryConfig = DefaultConstants.retryConfig, + final RefreshConfig refreshConfig = DefaultConstants.refreshConfig, + final JsonConfig? jsonConfig, + final ValueChanged? onData, + final ValueChanged? onError, + final bool enabled = true, +}) { + final spotify = ref.watch(spotifyProvider); + + final query = useQuery( + queryKey, + () => queryFn(spotify), + initial: initial, + retryConfig: retryConfig, + refreshConfig: refreshConfig, + jsonConfig: jsonConfig, + onData: onData, + onError: onError, + enabled: enabled, + ); + + useEffect(() { + query.refresh(); + return null; + }, [spotify]); + + return query; +} diff --git a/lib/main.dart b/lib/main.dart index 674aa6dbc..255a40ac2 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -31,7 +31,6 @@ import 'package:spotube/utils/platform.dart'; import 'package:window_manager/window_manager.dart'; import 'package:window_size/window_size.dart'; -final bowl = QueryBowl(); void main(List rawArgs) async { final parser = ArgParser(); @@ -70,7 +69,7 @@ void main(List rawArgs) async { } WidgetsFlutterBinding.ensureInitialized(); - await Hive.initFlutter(); + await QueryClient.initialize(cachePrefix: "oss.krtirtho.spotube"); Hive.registerAdapter(CacheTrackAdapter()); Hive.registerAdapter(CacheTrackEngagementAdapter()); Hive.registerAdapter(CacheTrackSkipSegmentAdapter()); @@ -173,8 +172,7 @@ void main(List rawArgs) async { }, ) ], - child: QueryBowlScope( - bowl: bowl, + child: QueryClientProvider( child: const Spotube(), ), ); diff --git a/lib/pages/album/album.dart b/lib/pages/album/album.dart index 14d1bb919..569184c8b 100644 --- a/lib/pages/album/album.dart +++ b/lib/pages/album/album.dart @@ -47,12 +47,7 @@ class AlbumPage extends HookConsumerWidget { ref.watch(PlaylistQueueNotifier.provider); final playback = ref.watch(PlaylistQueueNotifier.notifier); - final SpotifyApi spotify = ref.watch(spotifyProvider); - - final tracksSnapshot = useQuery( - job: Queries.album.tracksOf(album.id!), - externalData: spotify, - ); + final tracksSnapshot = Queries.album.useTracksOfQuery(ref, album.id!); final albumArt = useMemoized( () => TypeConversionUtils.image_X_UrlString( diff --git a/lib/pages/artist/artist.dart b/lib/pages/artist/artist.dart index 788949455..b1a372f52 100644 --- a/lib/pages/artist/artist.dart +++ b/lib/pages/artist/artist.dart @@ -1,5 +1,4 @@ import 'package:fl_query/fl_query.dart'; -import 'package:fl_query_hooks/fl_query_hooks.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; @@ -65,10 +64,7 @@ class ArtistPage extends HookConsumerWidget { ), body: HookBuilder( builder: (context) { - final artistsQuery = useQuery( - job: Queries.artist.get(artistId), - externalData: spotify, - ); + final artistsQuery = Queries.artist.useGetArtist(ref, artistId); if (artistsQuery.isLoading || !artistsQuery.hasData) { return const ShimmerArtistProfile(); @@ -166,10 +162,8 @@ class ArtistPage extends HookConsumerWidget { if (auth != null) HookBuilder( builder: (context) { - final isFollowingQuery = useQuery( - job: Queries.artist.doIFollow(artistId), - externalData: spotify, - ); + final isFollowingQuery = Queries.artist + .useDoIFollowQuery(ref, artistId); if (isFollowingQuery.isLoading || !isFollowingQuery.hasData) { @@ -181,7 +175,7 @@ class ArtistPage extends HookConsumerWidget { ); } - final queryBowl = QueryBowl.of(context); + final queryBowl = QueryClient.of(context); return PlatformFilledButton( onPressed: () async { @@ -195,21 +189,14 @@ class ArtistPage extends HookConsumerWidget { FollowingType.artist, [artistId], ); - await isFollowingQuery.refetch(); + await isFollowingQuery.refresh(); queryBowl - .getInfiniteQuery( - Queries.artist.followedByMe - .queryKey, - ) - ?.refetch(); + .refreshInfiniteQueryAllPages( + "user-following-artists"); } finally { - QueryBowl.of(context) - .refetchQueries([ - Queries.artist - .doIFollow(artistId) - .queryKey, - ]); + QueryClient.of(context).refreshQuery( + "user-follows-artists-query/$artistId"); } }, child: PlatformText( @@ -281,9 +268,9 @@ class ArtistPage extends HookConsumerWidget { const SizedBox(height: 50), HookBuilder( builder: (context) { - final topTracksQuery = useQuery( - job: Queries.artist.topTracksOf(artistId), - externalData: spotify, + final topTracksQuery = Queries.artist.useTopTracksOfQuery( + ref, + artistId, ); final isPlaylistPlaying = @@ -391,9 +378,10 @@ class ArtistPage extends HookConsumerWidget { const SizedBox(height: 10), HookBuilder( builder: (context) { - final relatedArtists = useQuery( - job: Queries.artist.relatedArtistsOf(artistId), - externalData: spotify, + final relatedArtists = + Queries.artist.useRelatedArtistsOfQuery( + ref, + artistId, ); if (relatedArtists.isLoading || !relatedArtists.hasData) { diff --git a/lib/pages/home/genres.dart b/lib/pages/home/genres.dart index faeec721a..b682d42cd 100644 --- a/lib/pages/home/genres.dart +++ b/lib/pages/home/genres.dart @@ -1,4 +1,3 @@ -import 'package:fl_query_hooks/fl_query_hooks.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:collection/collection.dart'; @@ -9,8 +8,6 @@ import 'package:spotube/components/genre/category_card.dart'; import 'package:spotube/components/shared/compact_search.dart'; import 'package:spotube/components/shared/shimmers/shimmer_categories.dart'; import 'package:spotube/components/shared/waypoint.dart'; -import 'package:spotube/provider/authentication_provider.dart'; -import 'package:spotube/provider/spotify_provider.dart'; import 'package:spotube/provider/user_preferences_provider.dart'; import 'package:spotube/services/queries/queries.dart'; @@ -22,43 +19,20 @@ class GenrePage extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { final scrollController = useScrollController(); - final spotify = ref.watch(spotifyProvider); final recommendationMarket = ref.watch( userPreferencesProvider.select((s) => s.recommendationMarket), ); - final categoriesQuery = useInfiniteQuery( - job: Queries.category.list, - externalData: { - "spotify": spotify, - "recommendationMarket": recommendationMarket, - }, - ); + final categoriesQuery = Queries.category.useList(ref, recommendationMarket); final isMounted = useIsMounted(); - final auth = ref.watch(AuthenticationNotifier.provider); - - /// Temporary fix before fl-query 0.4.0 - useEffect(() { - if (auth != null && categoriesQuery.hasError) { - categoriesQuery.setExternalData({ - "spotify": spotify, - "recommendationMarket": recommendationMarket, - }); - categoriesQuery.refetchPages(); - } - return null; - }, [auth, categoriesQuery.hasError]); - - /// =================================== - return HookBuilder(builder: (context) { final searchText = useState(""); final categories = useMemoized( () { final categories = categoriesQuery.pages .expand( - (page) => page?.items ?? const Iterable.empty(), + (page) => page.items ?? const Iterable.empty(), ) .toList(); if (searchText.value.isEmpty) { @@ -86,12 +60,12 @@ class GenrePage extends HookConsumerWidget { final list = RefreshIndicator( onRefresh: () async { - await categoriesQuery.refetchPages(); + await categoriesQuery.refreshAll(); }, child: Waypoint( onTouchEdge: () async { if (categoriesQuery.hasNextPage && isMounted()) { - await categoriesQuery.fetchNextPage(); + await categoriesQuery.fetchNext(); } }, controller: scrollController, diff --git a/lib/pages/home/personalized.dart b/lib/pages/home/personalized.dart index d54274eaa..810dd00d8 100644 --- a/lib/pages/home/personalized.dart +++ b/lib/pages/home/personalized.dart @@ -1,4 +1,3 @@ -import 'package:fl_query_hooks/fl_query_hooks.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart' hide Page; import 'package:flutter_hooks/flutter_hooks.dart'; @@ -10,7 +9,6 @@ import 'package:spotube/components/playlist/playlist_card.dart'; import 'package:spotube/components/shared/shimmers/shimmer_playbutton_card.dart'; import 'package:spotube/components/shared/waypoint.dart'; import 'package:spotube/models/logger.dart'; -import 'package:spotube/provider/spotify_provider.dart'; import 'package:spotube/services/queries/queries.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; @@ -104,30 +102,9 @@ class PersonalizedPage extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { - final spotify = ref.watch(spotifyProvider); + final featuredPlaylistsQuery = Queries.playlist.useFeaturedQuery(ref); - final featuredPlaylistsQuery = useInfiniteQuery( - job: Queries.playlist.featured, - externalData: spotify, - ); - - final newReleases = useInfiniteQuery( - job: Queries.album.newReleases, - externalData: spotify, - ); - - useEffect(() { - if (featuredPlaylistsQuery.hasError && - featuredPlaylistsQuery.pages.first == null) { - featuredPlaylistsQuery.setExternalData(spotify); - featuredPlaylistsQuery.refetch(); - } - if (newReleases.hasError && newReleases.pages.first == null) { - newReleases.setExternalData(spotify); - newReleases.refetch(); - } - return null; - }, [spotify]); + final newReleases = Queries.album.useNewReleasesQuery(ref); return ListView( children: [ @@ -136,13 +113,13 @@ class PersonalizedPage extends HookConsumerWidget { featuredPlaylistsQuery.pages.whereType>(), title: 'Featured', hasNextPage: featuredPlaylistsQuery.hasNextPage, - onFetchMore: featuredPlaylistsQuery.fetchNextPage, + onFetchMore: featuredPlaylistsQuery.fetchNext, ), PersonalizedItemCard( albums: newReleases.pages.whereType>(), title: 'New Releases', hasNextPage: newReleases.hasNextPage, - onFetchMore: newReleases.fetchNextPage, + onFetchMore: newReleases.fetchNext, ), ], ); diff --git a/lib/pages/lyrics/genius_lyrics.dart b/lib/pages/lyrics/genius_lyrics.dart index f915b01df..5f23c1261 100644 --- a/lib/pages/lyrics/genius_lyrics.dart +++ b/lib/pages/lyrics/genius_lyrics.dart @@ -1,4 +1,3 @@ -import 'package:fl_query_hooks/fl_query_hooks.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:palette_generator/palette_generator.dart'; @@ -10,7 +9,6 @@ import 'package:spotube/provider/playlist_queue_provider.dart'; import 'package:spotube/provider/user_preferences_provider.dart'; import 'package:spotube/services/queries/queries.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; -import 'package:tuple/tuple.dart'; class GeniusLyrics extends HookConsumerWidget { final PaletteColor palette; @@ -24,12 +22,9 @@ class GeniusLyrics extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { final playlist = ref.watch(PlaylistQueueNotifier.provider); - final geniusLyricsQuery = useQuery( - job: Queries.lyrics.static(playlist?.activeTrack.id ?? ""), - externalData: Tuple2( - playlist?.activeTrack, - ref.watch(userPreferencesProvider).geniusAccessToken, - ), + final geniusLyricsQuery = Queries.lyrics.useStatic( + playlist?.activeTrack, + ref.watch(userPreferencesProvider).geniusAccessToken, ); final breakpoint = useBreakpoints(); final textTheme = Theme.of(context).textTheme; @@ -42,8 +37,8 @@ class GeniusLyrics extends HookConsumerWidget { child: Text( playlist?.activeTrack.name ?? "", style: breakpoint >= Breakpoints.md - ? textTheme.headline3 - : textTheme.headline4?.copyWith( + ? textTheme.displaySmall + : textTheme.headlineMedium?.copyWith( fontSize: 25, color: palette.titleTextColor, ), @@ -54,8 +49,8 @@ class GeniusLyrics extends HookConsumerWidget { TypeConversionUtils.artists_X_String( playlist?.activeTrack.artists ?? []), style: (breakpoint >= Breakpoints.md - ? textTheme.headline5 - : textTheme.headline6) + ? textTheme.headlineSmall + : textTheme.titleLarge) ?.copyWith(color: palette.bodyTextColor), ), ) @@ -68,12 +63,12 @@ class GeniusLyrics extends HookConsumerWidget { child: Builder( builder: (context) { if (geniusLyricsQuery.isLoading || - geniusLyricsQuery.isRefetching) { + geniusLyricsQuery.isRefreshing) { return const ShimmerLyrics(); } else if (geniusLyricsQuery.hasError) { return Text( "Sorry, no Lyrics were found for `${playlist?.activeTrack.name}` :'(\n${geniusLyricsQuery.error.toString()}", - style: textTheme.bodyText1?.copyWith( + style: textTheme.bodyLarge?.copyWith( color: palette.bodyTextColor, ), ); diff --git a/lib/pages/lyrics/synced_lyrics.dart b/lib/pages/lyrics/synced_lyrics.dart index 96f848802..670c663dd 100644 --- a/lib/pages/lyrics/synced_lyrics.dart +++ b/lib/pages/lyrics/synced_lyrics.dart @@ -44,6 +44,19 @@ class SyncedLyrics extends HookConsumerWidget { final breakpoint = useBreakpoints(); final controller = useAutoScrollController(); + final timedLyricsQuery = Queries.lyrics.useSynced(playlist?.activeTrack); + final lyricValue = timedLyricsQuery.data; + final lyricsMap = useMemoized( + () => + lyricValue?.lyrics + .map((lyric) => {lyric.time.inSeconds: lyric.text}) + .reduce((accumulator, lyricSlice) => + {...accumulator, ...lyricSlice}) ?? + {}, + [lyricValue], + ); + final currentTime = useSyncedLyrics(ref, lyricsMap, lyricDelay); + final textTheme = Theme.of(context).textTheme; useEffect(() { @@ -55,130 +68,109 @@ class SyncedLyrics extends HookConsumerWidget { }, [playlist?.activeTrack]); final headlineTextStyle = (breakpoint >= Breakpoints.md - ? textTheme.headline3 - : textTheme.headline4?.copyWith(fontSize: 25)) + ? textTheme.displaySmall + : textTheme.headlineMedium?.copyWith(fontSize: 25)) ?.copyWith(color: palette.titleTextColor); - return QueryBuilder( - job: Queries.lyrics.synced(playlist?.activeTrack.id ?? ""), - externalData: playlist?.isLoading == true - ? playlist?.activeTrack as SpotubeTrack - : null, - builder: (context, timedLyricsQuery) { - return HookBuilder(builder: (context) { - final lyricValue = timedLyricsQuery.data; - final lyricsMap = useMemoized( - () => - lyricValue?.lyrics - .map((lyric) => {lyric.time.inSeconds: lyric.text}) - .reduce((accumulator, lyricSlice) => - {...accumulator, ...lyricSlice}) ?? - {}, - [lyricValue], - ); - final currentTime = useSyncedLyrics(ref, lyricsMap, lyricDelay); - return Stack( - children: [ - Column( - children: [ - if (isModal != true) - Center( - child: SpotubeMarqueeText( - text: playlist?.activeTrack.name ?? "Not Playing", - style: headlineTextStyle, - isHovering: true, - ), - ), - if (isModal != true) - Center( - child: Text( - TypeConversionUtils.artists_X_String( - playlist?.activeTrack.artists ?? []), - style: breakpoint >= Breakpoints.md - ? textTheme.headline5 - : textTheme.headline6, - ), - ), - if (lyricValue != null && lyricValue.lyrics.isNotEmpty) - Expanded( - child: ListView.builder( - controller: controller, - itemCount: lyricValue.lyrics.length, - itemBuilder: (context, index) { - final lyricSlice = lyricValue.lyrics[index]; - final isActive = - lyricSlice.time.inSeconds == currentTime; - - if (isActive) { - controller.scrollToIndex( - index, - preferPosition: AutoScrollPosition.middle, - ); - } - return AutoScrollTag( - key: ValueKey(index), - index: index, - controller: controller, - child: lyricSlice.text.isEmpty - ? Container() - : Center( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: AnimatedDefaultTextStyle( - duration: - const Duration(milliseconds: 250), - style: TextStyle( - color: isActive - ? Colors.white - : palette.bodyTextColor, - fontWeight: isActive - ? FontWeight.bold - : FontWeight.normal, - fontSize: isActive ? 30 : 26, - ), - child: Text( - lyricSlice.text, - maxLines: 2, - textAlign: TextAlign.center, - ), - ), - ), - ), - ); - }, - ), - ), - if (playlist?.activeTrack != null && - (lyricValue == null || - lyricValue.lyrics.isEmpty == true)) - const Expanded(child: ShimmerLyrics()), - ], + return HookBuilder(builder: (context) { + return Stack( + children: [ + Column( + children: [ + if (isModal != true) + Center( + child: SpotubeMarqueeText( + text: playlist?.activeTrack.name ?? "Not Playing", + style: headlineTextStyle, + isHovering: true, + ), + ), + if (isModal != true) + Center( + child: Text( + TypeConversionUtils.artists_X_String( + playlist?.activeTrack.artists ?? []), + style: breakpoint >= Breakpoints.md + ? textTheme.headlineSmall + : textTheme.titleLarge, + ), ), - Positioned( - top: 10, - right: 10, - child: Align( - alignment: Alignment.centerRight, - child: PlatformFilledButton( - child: const Icon( - SpotubeIcons.clock, - size: 16, - ), - onPressed: () async { - final delay = await showPlatformAlertDialog( - context, - builder: (context) => const LyricDelayAdjustDialog(), + if (lyricValue != null && lyricValue.lyrics.isNotEmpty) + Expanded( + child: ListView.builder( + controller: controller, + itemCount: lyricValue.lyrics.length, + itemBuilder: (context, index) { + final lyricSlice = lyricValue.lyrics[index]; + final isActive = lyricSlice.time.inSeconds == currentTime; + + if (isActive) { + controller.scrollToIndex( + index, + preferPosition: AutoScrollPosition.middle, ); - if (delay != null) { - ref.read(lyricDelayState.notifier).state = delay; - } - }, - ), + } + return AutoScrollTag( + key: ValueKey(index), + index: index, + controller: controller, + child: lyricSlice.text.isEmpty + ? Container() + : Center( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: AnimatedDefaultTextStyle( + duration: const Duration(milliseconds: 250), + style: TextStyle( + color: isActive + ? Colors.white + : palette.bodyTextColor, + fontWeight: isActive + ? FontWeight.bold + : FontWeight.normal, + fontSize: isActive ? 30 : 26, + ), + child: Text( + lyricSlice.text, + maxLines: 2, + textAlign: TextAlign.center, + ), + ), + ), + ), + ); + }, ), ), - ], - ); - }); - }); + if (playlist?.activeTrack != null && + (lyricValue == null || lyricValue.lyrics.isEmpty == true)) + const Expanded(child: ShimmerLyrics()), + ], + ), + Positioned( + top: 10, + right: 10, + child: Align( + alignment: Alignment.centerRight, + child: PlatformFilledButton( + child: const Icon( + SpotubeIcons.clock, + size: 16, + ), + onPressed: () async { + final delay = await showPlatformAlertDialog( + context, + builder: (context) => const LyricDelayAdjustDialog(), + ); + if (delay != null) { + ref.read(lyricDelayState.notifier).state = delay; + } + }, + ), + ), + ), + ], + ); + }); } } diff --git a/lib/pages/playlist/playlist.dart b/lib/pages/playlist/playlist.dart index 06c6244d8..02f41e805 100644 --- a/lib/pages/playlist/playlist.dart +++ b/lib/pages/playlist/playlist.dart @@ -1,4 +1,3 @@ -import 'package:fl_query_hooks/fl_query_hooks.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -9,7 +8,6 @@ import 'package:spotube/hooks/use_breakpoints.dart'; import 'package:spotube/models/logger.dart'; import 'package:flutter/material.dart'; import 'package:spotify/spotify.dart'; -import 'package:spotube/provider/spotify_provider.dart'; import 'package:spotube/provider/playlist_queue_provider.dart'; import 'package:spotube/services/queries/queries.dart'; @@ -46,15 +44,11 @@ class PlaylistView extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { final playlistNotifier = ref.watch(PlaylistQueueNotifier.notifier); - SpotifyApi spotify = ref.watch(spotifyProvider); final breakpoint = useBreakpoints(); - final meSnapshot = useQuery(job: Queries.user.me, externalData: spotify); - final tracksSnapshot = useQuery( - job: Queries.playlist.tracksOf(playlist.id!), - externalData: spotify, - ); + final meSnapshot = Queries.user.useMe(ref); + final tracksSnapshot = Queries.playlist.useTracksOfQuery(ref, playlist.id!); final isPlaylistPlaying = playlistNotifier.isPlayingPlaylist(tracksSnapshot.data ?? []); diff --git a/lib/services/mutations/album.dart b/lib/services/mutations/album.dart index c93a1aa3b..3441ac663 100644 --- a/lib/services/mutations/album.dart +++ b/lib/services/mutations/album.dart @@ -1,22 +1,25 @@ import 'package:fl_query/fl_query.dart'; -import 'package:spotify/spotify.dart'; -import 'package:tuple/tuple.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotube/hooks/use_spotify_mutation.dart'; class AlbumMutations { - final toggleFavorite = - MutationJob.withVariableKey>( - preMutationKey: "toggle-album-like", - task: (queryKey, externalData) async { - final albumId = getVariable(queryKey); - final spotify = externalData.item1; - final isLiked = externalData.item2; - - if (isLiked) { - await spotify.me.removeAlbums([albumId]); - } else { - await spotify.me.saveAlbums([albumId]); - } - return !isLiked; - }, - ); + Mutation useToggleFavorite( + WidgetRef ref, + String albumId, { + List? refreshQueries, + }) { + return useSpotifyMutation( + "toggle-album-like/$albumId", + (isLiked, spotify) async { + if (isLiked) { + await spotify.me.removeAlbums([albumId]); + } else { + await spotify.me.saveAlbums([albumId]); + } + return !isLiked; + }, + ref: ref, + refreshQueries: refreshQueries, + ); + } } diff --git a/lib/services/mutations/playlist.dart b/lib/services/mutations/playlist.dart index b5c3ba396..10b738cfc 100644 --- a/lib/services/mutations/playlist.dart +++ b/lib/services/mutations/playlist.dart @@ -1,35 +1,40 @@ import 'package:fl_query/fl_query.dart'; -import 'package:spotify/spotify.dart'; -import 'package:tuple/tuple.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotube/hooks/use_spotify_mutation.dart'; class PlaylistMutations { - final toggleFavorite = - MutationJob.withVariableKey>( - preMutationKey: "toggle-playlist-like", - task: (queryKey, externalData) async { - final playlistId = getVariable(queryKey); - final spotify = externalData.item1; - final isLiked = externalData.item2; + Mutation useToggleFavorite( + WidgetRef ref, + String playlistId, { + List? refreshQueries, + }) { + return useSpotifyMutation( + "toggle-playlist-like/$playlistId", + (isLiked, spotify) async { + if (isLiked) { + await spotify.playlists.unfollowPlaylist(playlistId); + } else { + await spotify.playlists.followPlaylist(playlistId); + } + return !isLiked; + }, + ref: ref, + refreshQueries: refreshQueries, + ); + } - if (isLiked) { - await spotify.playlists.unfollowPlaylist(playlistId); - } else { - await spotify.playlists.followPlaylist(playlistId); - } - return !isLiked; - }, - ); - - final removeTrackOf = - MutationJob.withVariableKey>( - preMutationKey: "remove-track-from-playlist", - task: (queryKey, externalData) async { - final spotify = externalData.item1; - final playlistId = getVariable(queryKey); - final trackId = externalData.item2; - - await spotify.playlists.removeTracks([trackId], playlistId); - return true; - }, - ); + Mutation useRemoveTrackOf( + WidgetRef ref, + String playlistId, + ) { + return useSpotifyMutation( + "remove-track-from-playlist/$playlistId", + (trackId, spotify) async { + await spotify.playlists.removeTracks([trackId], playlistId); + return true; + }, + ref: ref, + refreshQueries: ["playlist-tracks/$playlistId"], + ); + } } diff --git a/lib/services/mutations/track.dart b/lib/services/mutations/track.dart index 5cbc59b38..06f3af023 100644 --- a/lib/services/mutations/track.dart +++ b/lib/services/mutations/track.dart @@ -1,22 +1,30 @@ import 'package:fl_query/fl_query.dart'; -import 'package:spotify/spotify.dart'; -import 'package:tuple/tuple.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotube/hooks/use_spotify_mutation.dart'; class TrackMutations { - final toggleFavorite = - MutationJob.withVariableKey>( - preMutationKey: "toggle-track-like", - task: (queryKey, externalData) async { - final trackId = getVariable(queryKey); - final spotify = externalData.item1; - final isLiked = externalData.item2; - - if (isLiked) { - await spotify.tracks.me.removeOne(trackId); - } else { - await spotify.tracks.me.saveOne(trackId); - } - return !isLiked; - }, - ); + Mutation useToggleFavorite( + WidgetRef ref, + String trackId, { + MutationOnMutationFn? onMutate, + MutationOnDataFn? onData, + MutationOnErrorFn? onError, + }) { + return useSpotifyMutation( + 'toggle-track-like/$trackId', + (isLiked, spotify) async { + if (isLiked) { + await spotify.tracks.me.removeOne(trackId); + } else { + await spotify.tracks.me.saveOne(trackId); + } + return !isLiked; + }, + ref: ref, + onData: onData, + onMutate: onMutate, + refreshQueries: ["playlist-tracks/user-liked-tracks"], + onError: onError, + ); + } } diff --git a/lib/services/queries/album.dart b/lib/services/queries/album.dart index 48ff64ebd..90a33f762 100644 --- a/lib/services/queries/album.dart +++ b/lib/services/queries/album.dart @@ -1,50 +1,80 @@ import 'package:catcher/catcher.dart'; +import 'package:collection/collection.dart'; import 'package:fl_query/fl_query.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; +import 'package:spotube/hooks/use_spotify_infinite_query.dart'; +import 'package:spotube/hooks/use_spotify_query.dart'; class AlbumQueries { - final ofMine = QueryJob, SpotifyApi>( - queryKey: "current-user-albums", - task: (_, spotify) { - return spotify.me.savedAlbums().all(); - }, - ); + Query, dynamic> useOfMineQuery(WidgetRef ref) { + return useSpotifyQuery, dynamic>( + "current-user-albums", + (spotify) { + return spotify.me.savedAlbums().all(); + }, + ref: ref, + ); + } - final tracksOf = QueryJob.withVariableKey, SpotifyApi>( - preQueryKey: "album-tracks", - task: (queryKey, spotify) { - final id = getVariable(queryKey); - return spotify.albums.getTracks(id).all().then((value) => value.toList()); - }, - ); + Query, dynamic> useTracksOfQuery( + WidgetRef ref, + String albumId, + ) { + return useSpotifyQuery, dynamic>( + "album-tracks/$albumId", + (spotify) { + return spotify.albums + .getTracks(albumId) + .all() + .then((value) => value.toList()); + }, + ref: ref, + ); + } - final isSavedForMe = - QueryJob.withVariableKey(task: (queryKey, spotify) { - return spotify.me - .isSavedAlbums([getVariable(queryKey)]).then((value) => value.first); - }); + Query useIsSavedForMeQuery( + WidgetRef ref, + String album, + ) { + return useSpotifyQuery( + "is-saved-for-me/$album", + (spotify) { + return spotify.me.isSavedAlbums([album]).then((value) => value.first); + }, + ref: ref, + ); + } - final newReleases = InfiniteQueryJob, SpotifyApi, int>( - queryKey: "new-releases", - initialParam: 0, - getNextPageParam: (lastPage, lastParam) => - lastPage.items?.length == 5 ? lastPage.nextOffset : null, - getPreviousPageParam: (firstPage, firstParam) => firstPage.nextOffset - 6, - refetchOnExternalDataChange: true, - task: (_, pageParam, spotify) async { - try { - final albums = await Pages( - spotify, - 'v1/browse/new-releases', - (json) => AlbumSimple.fromJson(json), - 'albums', - (json) => AlbumSimple.fromJson(json), - ).getPage(5, pageParam); - return albums; - } catch (e, stack) { - Catcher.reportCheckedError(e, stack); - rethrow; - } - }, - ); + InfiniteQuery, dynamic, int> useNewReleasesQuery( + WidgetRef ref) { + return useSpotifyInfiniteQuery, dynamic, int>( + "new-releases", + (pageParam, spotify) async { + try { + final albums = await Pages( + spotify, + 'v1/browse/new-releases', + (json) => AlbumSimple.fromJson(json), + 'albums', + (json) => AlbumSimple.fromJson(json), + ).getPage(5, pageParam); + return albums; + } catch (e, stack) { + Catcher.reportCheckedError(e, stack); + rethrow; + } + }, + ref: ref, + initialPage: 0, + nextPage: (lastParam, pages) { + final lastPage = pages.elementAtOrNull(lastParam); + if (lastPage == null || + lastPage.isLast || + (lastPage.items ?? []).length < 5) return null; + + return lastPage.nextOffset; + }, + ); + } } diff --git a/lib/services/queries/artist.dart b/lib/services/queries/artist.dart index e0b9e7ec7..3ef4d3b2b 100644 --- a/lib/services/queries/artist.dart +++ b/lib/services/queries/artist.dart @@ -1,59 +1,101 @@ +import 'package:collection/collection.dart'; import 'package:fl_query/fl_query.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; +import 'package:spotube/hooks/use_spotify_infinite_query.dart'; +import 'package:spotube/hooks/use_spotify_query.dart'; class ArtistQueries { - final get = QueryJob.withVariableKey( - preQueryKey: "artist-profile", - task: (queryKey, externalData) => - externalData.artists.get(getVariable(queryKey)), - ); + Query useGetArtist( + WidgetRef ref, + String artist, + ) { + return useSpotifyQuery( + "artist-profile/$artist", + (spotify) => spotify.artists.get(artist), + ref: ref, + ); + } - final followedByMe = InfiniteQueryJob, SpotifyApi, String>( - queryKey: "user-following-artists", - initialParam: "", - getNextPageParam: (lastPage, lastParam) => lastPage.after, - getPreviousPageParam: (lastPage, lastParam) => - lastPage.metadata.previous ?? "", - task: (queryKey, pageKey, spotify) { - return spotify.me.following(FollowingType.artist).getPage(15, pageKey); - }, - ); + InfiniteQuery, dynamic, String> useFollowedByMeQuery( + WidgetRef ref) { + return useSpotifyInfiniteQuery, dynamic, String>( + "user-following-artists", + (pageParam, spotify) async { + return spotify.me + .following(FollowingType.artist) + .getPage(15, pageParam); + }, + initialPage: "", + nextPage: (lastPage, pages) => + pages.last.isLast || (pages.last.items?.length ?? 0) < 15 + ? null + : pages.last.after, + ref: ref, + ); + } - final doIFollow = QueryJob.withVariableKey( - preQueryKey: "user-follows-artists-query", - task: (artistId, spotify) async { - final result = await spotify.me.isFollowing( - FollowingType.artist, - [getVariable(artistId)], - ); - return result.first; - }, - ); + Query useDoIFollowQuery( + WidgetRef ref, + String artist, + ) { + return useSpotifyQuery( + "user-follows-artists-query/$artist", + (spotify) async { + final result = await spotify.me.isFollowing( + FollowingType.artist, + [artist], + ); + return result.first; + }, + ref: ref, + ); + } - final topTracksOf = QueryJob.withVariableKey, SpotifyApi>( - preQueryKey: "artist-top-track-query", - task: (queryKey, spotify) { - return spotify.artists.getTopTracks(getVariable(queryKey), "US"); - }, - ); + Query, dynamic> useTopTracksOfQuery( + WidgetRef ref, + String artist, + ) { + return useSpotifyQuery, dynamic>( + "artist-top-track-query/$artist", + (spotify) { + return spotify.artists.getTopTracks(artist, "US"); + }, + ref: ref, + ); + } - final albumsOf = - InfiniteQueryJob.withVariableKey, SpotifyApi, int>( - preQueryKey: "artist-albums", - initialParam: 0, - getNextPageParam: (lastPage, lastParam) => lastPage.nextOffset, - getPreviousPageParam: (lastPage, lastParam) => lastPage.nextOffset - 6, - task: (queryKey, pageKey, spotify) { - final id = getVariable(queryKey); - return spotify.artists.albums(id).getPage(5, pageKey); - }, - ); + InfiniteQuery, dynamic, int> useAlbumsOfQuery( + WidgetRef ref, + String artist, + ) { + return useSpotifyInfiniteQuery, dynamic, int>( + "artist-albums/$artist", + (pageParam, spotify) async { + return spotify.artists.albums(artist).getPage(5, pageParam); + }, + initialPage: 0, + nextPage: (lastPage, pages) { + final page = pages.elementAtOrNull(lastPage); + if (page == null || page.isLast || (page.items ?? []).length < 5) { + return null; + } + return page.nextOffset; + }, + ref: ref, + ); + } - final relatedArtistsOf = - QueryJob.withVariableKey, SpotifyApi>( - preQueryKey: "artist-related-artist-query", - task: (queryKey, spotify) { - return spotify.artists.getRelatedArtists(getVariable(queryKey)); - }, - ); + Query, dynamic> useRelatedArtistsOfQuery( + WidgetRef ref, + String artist, + ) { + return useSpotifyQuery, dynamic>( + "artist-related-artist-query/$artist", + (spotify) { + return spotify.artists.getRelatedArtists(artist); + }, + ref: ref, + ); + } } diff --git a/lib/services/queries/category.dart b/lib/services/queries/category.dart index 866f8b65d..c4bfacb12 100644 --- a/lib/services/queries/category.dart +++ b/lib/services/queries/category.dart @@ -1,33 +1,50 @@ import 'package:fl_query/fl_query.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; +import 'package:spotube/hooks/use_spotify_infinite_query.dart'; class CategoryQueries { - final list = InfiniteQueryJob, Map, int>( - queryKey: "categories-query", - initialParam: 0, - getNextPageParam: (lastPage, lastParam) => lastPage.nextOffset, - getPreviousPageParam: (lastPage, lastParam) => lastPage.nextOffset - 16, - refetchOnExternalDataChange: true, - task: (queryKey, pageParam, data) async { - final SpotifyApi spotify = data["spotify"] as SpotifyApi; - final String recommendationMarket = data["recommendationMarket"]; - final categories = await spotify.categories - .list(country: recommendationMarket) - .getPage(15, pageParam); + InfiniteQuery, dynamic, int> useList( + WidgetRef ref, String recommendationMarket) { + return useSpotifyInfiniteQuery, dynamic, int>( + "category-playlists", + (pageParam, spotify) async { + final categories = await spotify.categories + .list(country: recommendationMarket) + .getPage(15, pageParam); - return categories; - }, - ); + return categories; + }, + initialPage: 0, + nextPage: (lastPage, pages) { + if (pages.isEmpty) return lastPage + 1; + return pages.last.isLast || (pages.last.items?.length ?? 0) < 15 + ? null + : pages.last.nextOffset; + }, + ref: ref, + ); + } - final playlistsOf = - InfiniteQueryJob.withVariableKey, SpotifyApi, int>( - preQueryKey: "category-playlists", - initialParam: 0, - getNextPageParam: (lastPage, lastParam) => lastPage.nextOffset, - getPreviousPageParam: (lastPage, lastParam) => lastPage.nextOffset - 6, - task: (queryKey, pageKey, spotify) { - final id = getVariable(queryKey); - return spotify.playlists.getByCategoryId(id).getPage(5, pageKey); - }, - ); + InfiniteQuery, dynamic, int> usePlaylistsOf( + WidgetRef ref, + String category, + ) { + return useSpotifyInfiniteQuery, dynamic, int>( + "category-playlists/$category", + (pageParam, spotify) async { + final playlists = await spotify.playlists + .getByCategoryId(category) + .getPage(5, pageParam); + + return playlists; + }, + initialPage: 0, + nextPage: (lastPage, pages) => + pages.last.isLast || (pages.last.items?.length ?? 0) < 5 + ? null + : pages.last.nextOffset, + ref: ref, + ); + } } diff --git a/lib/services/queries/lyrics.dart b/lib/services/queries/lyrics.dart index 6823fd430..003646fb2 100644 --- a/lib/services/queries/lyrics.dart +++ b/lib/services/queries/lyrics.dart @@ -1,44 +1,49 @@ import 'package:collection/collection.dart'; import 'package:fl_query/fl_query.dart'; +import 'package:fl_query_hooks/fl_query_hooks.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/models/lyrics.dart'; import 'package:spotube/models/spotube_track.dart'; import 'package:spotube/utils/service_utils.dart'; -import 'package:tuple/tuple.dart'; class LyricsQueries { - final static = QueryJob.withVariableKey>( - preQueryKey: "genius-lyrics-query", - refetchOnExternalDataChange: true, - task: (queryKey, externalData) async { - final currentTrack = externalData.item1; - final geniusAccessToken = externalData.item2; - if (currentTrack == null || getVariable(queryKey).isEmpty) { - return "“Give this player a track to play”\n- S'Challa"; - } - final lyrics = await ServiceUtils.getLyrics( - currentTrack.name!, - currentTrack.artists?.map((s) => s.name).whereNotNull().toList() ?? [], - apiKey: geniusAccessToken, - optimizeQuery: true, - ); + Query useStatic( + Track? track, + String geniusAccessToken, + ) { + return useQuery( + "genius-lyrics-query/${track?.id}", + () async { + if (track == null) { + return "“Give this player a track to play”\n- S'Challa"; + } + final lyrics = await ServiceUtils.getLyrics( + track.name!, + track.artists?.map((s) => s.name).whereNotNull().toList() ?? [], + apiKey: geniusAccessToken, + optimizeQuery: true, + ); - if (lyrics == null) throw Exception("Unable find lyrics"); - return lyrics; - }, - ); + if (lyrics == null) throw Exception("Unable find lyrics"); + return lyrics; + }, + ); + } - final synced = QueryJob.withVariableKey( - preQueryKey: "synced-lyrics", - task: (queryKey, currentTrack) async { - if (currentTrack == null || getVariable(queryKey).isEmpty) { - throw "No track currently"; - } + Query useSynced( + Track? track, + ) { + return useQuery( + "synced-lyrics/${track?.id}}", + () async { + if (track == null || track is! SpotubeTrack) { + throw "No track currently"; + } + final timedLyrics = await ServiceUtils.getTimedLyrics(track); + if (timedLyrics == null) throw Exception("Unable to find lyrics"); - final timedLyrics = await ServiceUtils.getTimedLyrics(currentTrack); - if (timedLyrics == null) throw Exception("Unable to find lyrics"); - - return timedLyrics; - }, - ); + return timedLyrics; + }, + ); + } } diff --git a/lib/services/queries/playlist.dart b/lib/services/queries/playlist.dart index ea3ce119b..ffecc137b 100644 --- a/lib/services/queries/playlist.dart +++ b/lib/services/queries/playlist.dart @@ -1,56 +1,79 @@ import 'package:catcher/catcher.dart'; import 'package:fl_query/fl_query.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; +import 'package:spotube/hooks/use_spotify_infinite_query.dart'; +import 'package:spotube/hooks/use_spotify_query.dart'; class PlaylistQueries { - final doesUserFollow = QueryJob.withVariableKey( - preQueryKey: "playlist-is-followed", - task: (queryKey, spotify) { - final idMap = getVariable(queryKey).split(":"); + Query useDoesUserFollowQuery( + WidgetRef ref, + String playlistId, + String userId, + ) { + return useSpotifyQuery( + "playlist-is-followed/$playlistId/$userId", + (spotify) async { + final result = await spotify.playlists.followedBy(playlistId, [userId]); + return result.first; + }, + ref: ref, + ); + } - return spotify.playlists.followedBy(idMap.first, [idMap.last]).then( - (value) => value.first, - ); - }, - ); + Query, dynamic> useOfMineQuery(WidgetRef ref) { + return useSpotifyQuery, dynamic>( + "current-user-playlists", + (spotify) { + return spotify.playlists.me.all(); + }, + ref: ref, + ); + } - final ofMine = QueryJob, SpotifyApi>( - queryKey: "current-user-playlists", - task: (_, spotify) { - return spotify.playlists.me.all(); - }, - ); + Future> tracksOf(String playlistId, SpotifyApi spotify) { + if (playlistId == "user-liked-tracks") { + return spotify.tracks.me.saved.all().then( + (tracks) => tracks.map((e) => e.track!).toList(), + ); + } + return spotify.playlists.getTracksByPlaylistId(playlistId).all().then( + (value) => value.toList(), + ); + } - final tracksOf = QueryJob.withVariableKey, SpotifyApi>( - preQueryKey: "playlist-tracks", - task: (queryKey, spotify) { - final id = getVariable(queryKey); - return id != "user-liked-tracks" - ? spotify.playlists.getTracksByPlaylistId(id).all().then( - (value) => value.toList(), - ) - : spotify.tracks.me.saved.all().then( - (tracks) => tracks.map((e) => e.track!).toList(), - ); - }, - ); + Query, dynamic> useTracksOfQuery( + WidgetRef ref, + String playlistId, + ) { + return useSpotifyQuery, dynamic>( + "playlist-tracks/$playlistId", + (spotify) => tracksOf(playlistId, spotify), + ref: ref, + ); + } - final featured = InfiniteQueryJob, SpotifyApi, int>( - queryKey: "featured-playlists", - initialParam: 0, - getNextPageParam: (lastPage, lastParam) => - lastPage.items?.length == 5 ? lastPage.nextOffset : null, - getPreviousPageParam: (firstPage, firstParam) => firstPage.nextOffset - 6, - refetchOnExternalDataChange: true, - task: (_, pageParam, spotify) async { - try { - final playlists = - await spotify.playlists.featured.getPage(5, pageParam); - return playlists; - } catch (e, stack) { - Catcher.reportCheckedError(e, stack); - rethrow; - } - }, - ); + InfiniteQuery, dynamic, int> useFeaturedQuery( + WidgetRef ref, + ) { + return useSpotifyInfiniteQuery, dynamic, int>( + "featured-playlists", + (pageParam, spotify) async { + try { + final playlists = + await spotify.playlists.featured.getPage(5, pageParam); + return playlists; + } catch (e, stack) { + Catcher.reportCheckedError(e, stack); + rethrow; + } + }, + initialPage: 0, + nextPage: (lastPage, pages) => + pages.last.isLast || (pages.last.items?.length ?? 0) < 5 + ? null + : pages.last.nextOffset, + ref: ref, + ); + } } diff --git a/lib/services/queries/search.dart b/lib/services/queries/search.dart index 2a2a42558..21876ff0e 100644 --- a/lib/services/queries/search.dart +++ b/lib/services/queries/search.dart @@ -1,28 +1,30 @@ import 'package:fl_query/fl_query.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; -import 'package:tuple/tuple.dart'; +import 'package:spotube/hooks/use_spotify_infinite_query.dart'; class SearchQueries { - final get = InfiniteQueryJob.withVariableKey, - Tuple2, int>( - preQueryKey: "search-query", - refetchOnExternalDataChange: true, - initialParam: 0, - enabled: false, - getNextPageParam: (lastPage, lastParam) => - lastPage.isNotEmpty && (lastPage.first.items?.length ?? 0) < 10 - ? null - : lastParam + 10, - getPreviousPageParam: (lastPage, lastParam) => lastParam - 10, - task: (queryKey, pageParam, variables) { - if (variables.item1.trim().isEmpty) return []; - final queryString = variables.item1; - final spotify = variables.item2; - final searchType = getVariable(queryKey); - return spotify.search.get( - queryString, - types: [SearchType(searchType)], - ).getPage(10, pageParam); - }, - ); + InfiniteQuery, dynamic, int> useSearchQuery( + WidgetRef ref, + String query, + SearchType searchType, + ) { + return useSpotifyInfiniteQuery, dynamic, int>( + "search-query/$query", + (page, spotify) { + if (query.trim().isEmpty) return []; + final queryString = query; + return spotify.search.get( + queryString, + types: [searchType], + ).getPage(10, page); + }, + ref: ref, + initialPage: 0, + nextPage: (lastPage, pages) => + pages.last.isNotEmpty && (pages.last.first.items?.length ?? 0) < 10 + ? null + : pages.last.last.nextOffset, + ); + } } diff --git a/lib/services/queries/user.dart b/lib/services/queries/user.dart index 29485598f..2449ed9ea 100644 --- a/lib/services/queries/user.dart +++ b/lib/services/queries/user.dart @@ -1,25 +1,29 @@ import 'package:fl_query/fl_query.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; +import 'package:spotube/hooks/use_spotify_query.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; class UserQueries { - final me = QueryJob( - queryKey: "current-user", - refetchOnExternalDataChange: true, - task: (_, spotify) async { - final me = await spotify.me.get(); - if (me.images == null || me.images?.isEmpty == true) { - me.images = [ - Image() - ..height = 50 - ..width = 50 - ..url = TypeConversionUtils.image_X_UrlString( - me.images, - placeholder: ImagePlaceholder.artist, - ), - ]; - } - return me; - }, - ); + Query useMe(WidgetRef ref) { + return useSpotifyQuery( + "current-user", + (spotify) async { + final me = await spotify.me.get(); + if (me.images == null || me.images?.isEmpty == true) { + me.images = [ + Image() + ..height = 50 + ..width = 50 + ..url = TypeConversionUtils.image_X_UrlString( + me.images, + placeholder: ImagePlaceholder.artist, + ), + ]; + } + return me; + }, + ref: ref, + ); + } } diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index f8f3c3c00..cb3b7f705 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -9,7 +9,6 @@ import audio_service import audio_session import audioplayers_darwin import catcher -import connectivity_plus_macos import device_info_plus import macos_ui import metadata_god @@ -27,7 +26,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin")) AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin")) CatcherPlugin.register(with: registry.registrar(forPlugin: "CatcherPlugin")) - ConnectivityPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) MacOSUiPlugin.register(with: registry.registrar(forPlugin: "MacOSUiPlugin")) MetadataGodPlugin.register(with: registry.registrar(forPlugin: "MetadataGodPlugin")) diff --git a/pubspec.lock b/pubspec.lock index d45940615..992e73fcf 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -394,54 +394,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.0" - connectivity_plus: - dependency: transitive - description: - name: connectivity_plus - sha256: "3f8fe4e504c2d33696dac671a54909743bc6a902a9bb0902306f7a2aed7e528e" - url: "https://pub.dev" - source: hosted - version: "2.3.9" - connectivity_plus_linux: - dependency: transitive - description: - name: connectivity_plus_linux - sha256: "3caf859d001f10407b8e48134c761483e4495ae38094ffcca97193f6c271f5e2" - url: "https://pub.dev" - source: hosted - version: "1.3.1" - connectivity_plus_macos: - dependency: transitive - description: - name: connectivity_plus_macos - sha256: "488d2de1e47e1224ad486e501b20b088686ba1f4ee9c4420ecbc3b9824f0b920" - url: "https://pub.dev" - source: hosted - version: "1.2.6" - connectivity_plus_platform_interface: - dependency: transitive - description: - name: connectivity_plus_platform_interface - sha256: cf1d1c28f4416f8c654d7dc3cd638ec586076255d407cef3ddbdaf178272a71a - url: "https://pub.dev" - source: hosted - version: "1.2.4" - connectivity_plus_web: - dependency: transitive - description: - name: connectivity_plus_web - sha256: "81332be1b4baf8898fed17bb4fdef27abb7c6fd990bf98c54fd978478adf2f1a" - url: "https://pub.dev" - source: hosted - version: "1.2.5" - connectivity_plus_windows: - dependency: transitive - description: - name: connectivity_plus_windows - sha256: "535b0404b4d5605c4dd8453d67e5d6d2ea0dd36e3b477f50f31af51b0aeab9dd" - url: "https://pub.dev" - source: hosted - version: "1.2.2" convert: dependency: transitive description: @@ -581,18 +533,20 @@ packages: fl_query: dependency: "direct main" description: - name: fl_query - sha256: "9d55b025d672aaf27766923817a7b458b5fb78631c83e6ce958faaef8c9ac61d" - url: "https://pub.dev" - source: hosted + path: "packages/fl_query" + ref: new-architecture + resolved-ref: d964216ee17e600f79c33f1811080877c8c1b510 + url: "https://github.com/KRTirtho/fl-query.git" + source: git version: "0.3.1" fl_query_hooks: dependency: "direct main" description: - name: fl_query_hooks - sha256: "052b50587794ca6e0d0d4cb6591efcd91308c299a3779087632754a09282562e" - url: "https://pub.dev" - source: hosted + path: "packages/fl_query_hooks" + ref: new-architecture + resolved-ref: d964216ee17e600f79c33f1811080877c8c1b510 + url: "https://github.com/KRTirtho/fl-query.git" + source: git version: "0.3.1" fluent_ui: dependency: "direct main" @@ -691,10 +645,10 @@ packages: dependency: "direct main" description: name: flutter_hooks - sha256: "2b202559a4ed3656bbb7aae9d8b335fb0037b23acc7ae3f377d1ba0b95c21aec" + sha256: "6a126f703b89499818d73305e4ce1e3de33b4ae1c5512e3b8eab4b986f46774c" url: "https://pub.dev" source: hosted - version: "0.18.5+1" + version: "0.18.6" flutter_inappwebview: dependency: "direct main" description: @@ -1062,14 +1016,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" - nm: + mutex: dependency: transitive description: - name: nm - sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254" + name: mutex + sha256: "03116a4e46282a671b46c12de649d72c0ed18188ffe12a8d0fc63e83f4ad88f4" url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "3.0.1" oauth2: dependency: transitive description: @@ -1822,5 +1776,5 @@ packages: source: hosted version: "1.12.3" sdks: - dart: ">=2.19.0 <4.0.0" + dart: ">=2.19.0 <3.0.0" flutter: ">=3.7.0" diff --git a/pubspec.yaml b/pubspec.yaml index f8b926282..b8cf89289 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -27,8 +27,16 @@ dependencies: cupertino_icons: ^1.0.5 dbus: ^0.7.8 file_picker: ^5.2.2 - fl_query: ^0.3.1 - fl_query_hooks: ^0.3.1 + fl_query: + git: + url: https://github.com/KRTirtho/fl-query.git + path: packages/fl_query + ref: new-architecture + fl_query_hooks: + git: + url: https://github.com/KRTirtho/fl-query.git + path: packages/fl_query_hooks + ref: new-architecture fluent_ui: ^4.3.0 fluentui_system_icons: ^1.1.189 flutter: @@ -95,8 +103,13 @@ dev_dependencies: sdk: flutter hive_generator: ^2.0.0 -dependency_overrides: +dependency_overrides: package_info_plus: ^3.0.2 + fl_query: + git: + url: https://github.com/KRTirtho/fl-query.git + path: packages/fl_query + ref: new-architecture flutter: uses-material-design: true diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 13b352c59..245222383 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -8,7 +8,6 @@ #include #include -#include #include #include #include @@ -21,8 +20,6 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("AudioplayersWindowsPlugin")); CatcherPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("CatcherPlugin")); - ConnectivityPlusWindowsPluginRegisterWithRegistrar( - registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); MetadataGodPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("MetadataGodPluginCApi")); PermissionHandlerWindowsPluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index dfab40f7e..b8a3a01b4 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -5,7 +5,6 @@ list(APPEND FLUTTER_PLUGIN_LIST audioplayers_windows catcher - connectivity_plus_windows metadata_god permission_handler_windows screen_retriever From 3d9da8b4e3731b8376d9a5ad6055457fc9480525 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 24 Feb 2023 15:27:13 +0600 Subject: [PATCH 2/7] refactor: use singleton for Queries and Mutations service classes --- lib/components/artist/artist_album_list.dart | 2 +- lib/components/genre/category_card.dart | 2 +- lib/components/library/user_albums.dart | 2 +- lib/components/library/user_artists.dart | 2 +- lib/components/library/user_playlists.dart | 2 +- lib/components/playlist/playlist_card.dart | 4 +- lib/components/root/sidebar.dart | 4 +- .../dialogs/playlist_add_track_dialog.dart | 4 +- lib/components/shared/heart_button.dart | 20 +-- .../shared/track_table/track_tile.dart | 2 +- lib/pages/album/album.dart | 4 +- lib/pages/artist/artist.dart | 11 +- lib/pages/home/genres.dart | 125 +++++++++--------- lib/pages/home/personalized.dart | 4 +- lib/pages/lyrics/genius_lyrics.dart | 2 +- lib/pages/lyrics/synced_lyrics.dart | 2 +- lib/pages/playlist/playlist.dart | 4 +- lib/pages/root/root_app.dart | 2 - lib/pages/search/search.dart | 8 +- lib/services/mutations/album.dart | 4 +- lib/services/mutations/mutations.dart | 11 +- lib/services/mutations/playlist.dart | 6 +- lib/services/mutations/track.dart | 4 +- lib/services/queries/album.dart | 11 +- lib/services/queries/artist.dart | 14 +- lib/services/queries/category.dart | 6 +- lib/services/queries/lyrics.dart | 6 +- lib/services/queries/playlist.dart | 10 +- lib/services/queries/queries.dart | 19 +-- lib/services/queries/search.dart | 3 +- lib/services/queries/user.dart | 3 +- 31 files changed, 159 insertions(+), 144 deletions(-) diff --git a/lib/components/artist/artist_album_list.dart b/lib/components/artist/artist_album_list.dart index 9d22ae343..b12dd544d 100644 --- a/lib/components/artist/artist_album_list.dart +++ b/lib/components/artist/artist_album_list.dart @@ -21,7 +21,7 @@ class ArtistAlbumList extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { final scrollController = useScrollController(); - final albumsQuery = Queries.artist.useAlbumsOfQuery(ref, artistId); + final albumsQuery = useQueries.artist.albumsOf(ref, artistId); final albums = useMemoized(() { return albumsQuery.pages diff --git a/lib/components/genre/category_card.dart b/lib/components/genre/category_card.dart index 0f4ed2793..02c66e62f 100644 --- a/lib/components/genre/category_card.dart +++ b/lib/components/genre/category_card.dart @@ -24,7 +24,7 @@ class CategoryCard extends HookConsumerWidget { Widget build(BuildContext context, ref) { final scrollController = useScrollController(); final spotify = ref.watch(spotifyProvider); - final playlistQuery = Queries.category.usePlaylistsOf( + final playlistQuery = useQueries.category.playlistsOf( ref, category.id!, ); diff --git a/lib/components/library/user_albums.dart b/lib/components/library/user_albums.dart index 8cef38400..ebcaa9dc5 100644 --- a/lib/components/library/user_albums.dart +++ b/lib/components/library/user_albums.dart @@ -22,7 +22,7 @@ class UserAlbums extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { final auth = ref.watch(AuthenticationNotifier.provider); - final albumsQuery = Queries.album.useOfMineQuery(ref); + final albumsQuery = useQueries.album.ofMine(ref); final spacing = useBreakpointValue( sm: 0, diff --git a/lib/components/library/user_artists.dart b/lib/components/library/user_artists.dart index c9dd56723..a522b7f42 100644 --- a/lib/components/library/user_artists.dart +++ b/lib/components/library/user_artists.dart @@ -20,7 +20,7 @@ class UserArtists extends HookConsumerWidget { Widget build(BuildContext context, ref) { final auth = ref.watch(AuthenticationNotifier.provider); - final artistQuery = Queries.artist.useFollowedByMeQuery(ref); + final artistQuery = useQueries.artist.followedByMe(ref); final hasNextPage = artistQuery.pages.isEmpty ? false diff --git a/lib/components/library/user_playlists.dart b/lib/components/library/user_playlists.dart index 7ab0020d3..b0eef87d7 100644 --- a/lib/components/library/user_playlists.dart +++ b/lib/components/library/user_playlists.dart @@ -33,7 +33,7 @@ class UserPlaylists extends HookConsumerWidget { : PlaybuttonCardViewType.square; final auth = ref.watch(AuthenticationNotifier.provider); - final playlistsQuery = Queries.playlist.useOfMineQuery(ref); + final playlistsQuery = useQueries.playlist.ofMine(ref); Image image = Image(); image.height = 300; diff --git a/lib/components/playlist/playlist_card.dart b/lib/components/playlist/playlist_card.dart index b44180e4e..8d0c92150 100644 --- a/lib/components/playlist/playlist_card.dart +++ b/lib/components/playlist/playlist_card.dart @@ -68,7 +68,7 @@ class PlaylistCard extends HookConsumerWidget { List fetchedTracks = await queryBowl.fetchQuery( "playlist-tracks/${playlist.id}", - () => Queries.playlist.tracksOf(playlist.id!, spotify), + () => useQueries.playlist.tracksOf(playlist.id!, spotify), ) ?? []; @@ -86,7 +86,7 @@ class PlaylistCard extends HookConsumerWidget { if (isPlaylistPlaying) return; List fetchedTracks = await queryBowl.fetchQuery( "playlist-tracks/${playlist.id}", - () => Queries.playlist.tracksOf(playlist.id!, spotify), + () => useQueries.playlist.tracksOf(playlist.id!, spotify), ) ?? []; diff --git a/lib/components/root/sidebar.dart b/lib/components/root/sidebar.dart index 8cc854b5b..451dfa783 100644 --- a/lib/components/root/sidebar.dart +++ b/lib/components/root/sidebar.dart @@ -11,7 +11,6 @@ import 'package:spotube/components/shared/image/universal_image.dart'; import 'package:spotube/hooks/use_breakpoints.dart'; import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/downloader_provider.dart'; -import 'package:spotube/provider/spotify_provider.dart'; import 'package:spotube/provider/user_preferences_provider.dart'; import 'package:spotube/services/queries/queries.dart'; @@ -194,8 +193,7 @@ class SidebarFooter extends HookConsumerWidget { width: 256, child: HookBuilder( builder: (context) { - var spotify = ref.watch(spotifyProvider); - final me = Queries.user.useMe(ref); + final me = useQueries.user.me(ref); final data = me.data; final avatarImg = TypeConversionUtils.image_X_UrlString( diff --git a/lib/components/shared/dialogs/playlist_add_track_dialog.dart b/lib/components/shared/dialogs/playlist_add_track_dialog.dart index 06ed49bae..9a8d20034 100644 --- a/lib/components/shared/dialogs/playlist_add_track_dialog.dart +++ b/lib/components/shared/dialogs/playlist_add_track_dialog.dart @@ -16,8 +16,8 @@ class PlaylistAddTrackDialog extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { final spotify = ref.watch(spotifyProvider); - final userPlaylists = Queries.playlist.useOfMineQuery(ref); - final me = Queries.user.useMe(ref); + final userPlaylists = useQueries.playlist.ofMine(ref); + final me = useQueries.user.me(ref); final filteredPlaylists = userPlaylists.data?.where( (playlist) => playlist.owner?.id != null && playlist.owner!.id == me.data?.id, diff --git a/lib/components/shared/heart_button.dart b/lib/components/shared/heart_button.dart index 42bb117b6..0a547d26a 100644 --- a/lib/components/shared/heart_button.dart +++ b/lib/components/shared/heart_button.dart @@ -47,17 +47,17 @@ class HeartButton extends ConsumerWidget { Tuple3, Query> useTrackToggleLike(Track track, WidgetRef ref) { - final me = Queries.user.useMe(ref); + final me = useQueries.user.me(ref); final savedTracks = - Queries.playlist.useTracksOfQuery(ref, "user-liked-tracks"); + useQueries.playlist.tracksOfQuery(ref, "user-liked-tracks"); final isLiked = savedTracks.data?.map((track) => track.id).contains(track.id) ?? false; final mounted = useIsMounted(); - final toggleTrackLike = Mutations.track.useToggleFavorite( + final toggleTrackLike = useMutations.track.toggleFavorite( ref, track.id!, onMutate: (variables) { @@ -92,7 +92,7 @@ class TrackHeartButton extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { final savedTracks = - Queries.playlist.useTracksOfQuery(ref, "user-liked-tracks"); + useQueries.playlist.tracksOfQuery(ref, "user-liked-tracks"); final toggler = useTrackToggleLike(track, ref); if (toggler.item3.isLoading || !toggler.item3.hasData) { return const PlatformCircularProgressIndicator(); @@ -120,15 +120,15 @@ class PlaylistHeartButton extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { - final me = Queries.user.useMe(ref); + final me = useQueries.user.me(ref); - final isLikedQuery = Queries.playlist.useDoesUserFollowQuery( + final isLikedQuery = useQueries.playlist.doesUserFollow( ref, playlist.id!, me.data!.id!, ); - final togglePlaylistLike = Mutations.playlist.useToggleFavorite( + final togglePlaylistLike = useMutations.playlist.toggleFavorite( ref, playlist.id!, refreshQueries: [ @@ -178,12 +178,12 @@ class AlbumHeartButton extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { - final me = Queries.user.useMe(ref); + final me = useQueries.user.me(ref); - final albumIsSaved = Queries.album.useIsSavedForMeQuery(ref, album.id!); + final albumIsSaved = useQueries.album.isSavedForMe(ref, album.id!); final isLiked = albumIsSaved.data ?? false; - final toggleAlbumLike = Mutations.album.useToggleFavorite( + final toggleAlbumLike = useMutations.album.toggleFavorite( ref, album.id!, refreshQueries: [ diff --git a/lib/components/shared/track_table/track_tile.dart b/lib/components/shared/track_table/track_tile.dart index 671549c26..f82bb9f1d 100644 --- a/lib/components/shared/track_table/track_tile.dart +++ b/lib/components/shared/track_table/track_tile.dart @@ -73,7 +73,7 @@ class TrackTile extends HookConsumerWidget { final playlistQueueNotifier = ref.watch(PlaylistQueueNotifier.notifier); final removingTrack = useState(null); - final removeTrack = Mutations.playlist.useRemoveTrackOf( + final removeTrack = useMutations.playlist.removeTrackOf( ref, playlistId ?? "", ); diff --git a/lib/pages/album/album.dart b/lib/pages/album/album.dart index 569184c8b..a6399bf04 100644 --- a/lib/pages/album/album.dart +++ b/lib/pages/album/album.dart @@ -1,4 +1,3 @@ -import 'package:fl_query_hooks/fl_query_hooks.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; @@ -12,7 +11,6 @@ import 'package:spotube/provider/playlist_queue_provider.dart'; import 'package:spotube/services/queries/queries.dart'; import 'package:spotube/utils/service_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; -import 'package:spotube/provider/spotify_provider.dart'; class AlbumPage extends HookConsumerWidget { final AlbumSimple album; @@ -47,7 +45,7 @@ class AlbumPage extends HookConsumerWidget { ref.watch(PlaylistQueueNotifier.provider); final playback = ref.watch(PlaylistQueueNotifier.notifier); - final tracksSnapshot = Queries.album.useTracksOfQuery(ref, album.id!); + final tracksSnapshot = useQueries.album.tracksOf(ref, album.id!); final albumArt = useMemoized( () => TypeConversionUtils.image_X_UrlString( diff --git a/lib/pages/artist/artist.dart b/lib/pages/artist/artist.dart index b1a372f52..d939fedf6 100644 --- a/lib/pages/artist/artist.dart +++ b/lib/pages/artist/artist.dart @@ -64,7 +64,7 @@ class ArtistPage extends HookConsumerWidget { ), body: HookBuilder( builder: (context) { - final artistsQuery = Queries.artist.useGetArtist(ref, artistId); + final artistsQuery = useQueries.artist.get(ref, artistId); if (artistsQuery.isLoading || !artistsQuery.hasData) { return const ShimmerArtistProfile(); @@ -162,8 +162,8 @@ class ArtistPage extends HookConsumerWidget { if (auth != null) HookBuilder( builder: (context) { - final isFollowingQuery = Queries.artist - .useDoIFollowQuery(ref, artistId); + final isFollowingQuery = useQueries.artist + .doIFollow(ref, artistId); if (isFollowingQuery.isLoading || !isFollowingQuery.hasData) { @@ -268,7 +268,7 @@ class ArtistPage extends HookConsumerWidget { const SizedBox(height: 50), HookBuilder( builder: (context) { - final topTracksQuery = Queries.artist.useTopTracksOfQuery( + final topTracksQuery = useQueries.artist.topTracksOf( ref, artistId, ); @@ -378,8 +378,7 @@ class ArtistPage extends HookConsumerWidget { const SizedBox(height: 10), HookBuilder( builder: (context) { - final relatedArtists = - Queries.artist.useRelatedArtistsOfQuery( + final relatedArtists = useQueries.artist.relatedArtistsOf( ref, artistId, ); diff --git a/lib/pages/home/genres.dart b/lib/pages/home/genres.dart index b682d42cd..3b997e10a 100644 --- a/lib/pages/home/genres.dart +++ b/lib/pages/home/genres.dart @@ -22,77 +22,76 @@ class GenrePage extends HookConsumerWidget { final recommendationMarket = ref.watch( userPreferencesProvider.select((s) => s.recommendationMarket), ); - final categoriesQuery = Queries.category.useList(ref, recommendationMarket); + final categoriesQuery = useQueries.category.list(ref, recommendationMarket); final isMounted = useIsMounted(); - return HookBuilder(builder: (context) { - final searchText = useState(""); - final categories = useMemoized( - () { - final categories = categoriesQuery.pages - .expand( - (page) => page.items ?? const Iterable.empty(), - ) - .toList(); - if (searchText.value.isEmpty) { - return categories; - } - return categories - .map((e) => Tuple2( - weightedRatio(e.name!, searchText.value), - e, - )) - .sorted((a, b) => b.item1.compareTo(a.item1)) - .where((e) => e.item1 > 50) - .map((e) => e.item2) - .toList(); - }, - [categoriesQuery.pages, searchText.value], - ); + final searchText = useState(""); + final categories = useMemoized( + () { + final categories = categoriesQuery.pages + .expand( + (page) => page.items ?? const Iterable.empty(), + ) + .toList(); + if (searchText.value.isEmpty) { + return categories; + } + return categories + .map((e) => Tuple2( + weightedRatio(e.name!, searchText.value), + e, + )) + .sorted((a, b) => b.item1.compareTo(a.item1)) + .where((e) => e.item1 > 50) + .map((e) => e.item2) + .toList(); + }, + [categoriesQuery.pages, searchText.value], + ); - final searchbar = CompactSearch( - onChanged: (value) { - searchText.value = value; - }, - placeholder: "Filter categories or genres...", - ); + final searchbar = CompactSearch( + onChanged: (value) { + searchText.value = value; + }, + placeholder: "Filter categories or genres...", + ); - final list = RefreshIndicator( - onRefresh: () async { - await categoriesQuery.refreshAll(); + final list = RefreshIndicator( + onRefresh: () async { + await categoriesQuery.refreshAll(); + }, + child: Waypoint( + onTouchEdge: () async { + if (categoriesQuery.hasNextPage && isMounted()) { + await categoriesQuery.fetchNext(); + } }, - child: Waypoint( - onTouchEdge: () async { - if (categoriesQuery.hasNextPage && isMounted()) { - await categoriesQuery.fetchNext(); + controller: scrollController, + child: ListView.builder( + physics: const AlwaysScrollableScrollPhysics(), + controller: scrollController, + itemCount: categories.length, + itemBuilder: (context, index) { + final category = categories[index]; + if (searchText.value.isEmpty && index == categories.length - 1) { + return const ShimmerCategories(); } + return CategoryCard(category); }, - controller: scrollController, - child: ListView.builder( - physics: const AlwaysScrollableScrollPhysics(), - controller: scrollController, - itemCount: categories.length, - itemBuilder: (context, index) { - final category = categories[index]; - if (searchText.value.isEmpty && index == categories.length - 1) { - return const ShimmerCategories(); - } - return CategoryCard(category); - }, - ), ), - ); - return Stack( - children: [ - Positioned.fill(child: list), - Positioned( - top: 0, - right: 10, - child: searchbar, - ), - ], - ); - }); + ), + ); + + return Stack( + children: [ + Positioned.fill(child: list), + Positioned( + top: 0, + right: 10, + child: searchbar, + ), + ], + ); } } diff --git a/lib/pages/home/personalized.dart b/lib/pages/home/personalized.dart index 810dd00d8..a502103df 100644 --- a/lib/pages/home/personalized.dart +++ b/lib/pages/home/personalized.dart @@ -102,9 +102,9 @@ class PersonalizedPage extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { - final featuredPlaylistsQuery = Queries.playlist.useFeaturedQuery(ref); + final featuredPlaylistsQuery = useQueries.playlist.featured(ref); - final newReleases = Queries.album.useNewReleasesQuery(ref); + final newReleases = useQueries.album.newReleases(ref); return ListView( children: [ diff --git a/lib/pages/lyrics/genius_lyrics.dart b/lib/pages/lyrics/genius_lyrics.dart index 5f23c1261..b158a13b1 100644 --- a/lib/pages/lyrics/genius_lyrics.dart +++ b/lib/pages/lyrics/genius_lyrics.dart @@ -22,7 +22,7 @@ class GeniusLyrics extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { final playlist = ref.watch(PlaylistQueueNotifier.provider); - final geniusLyricsQuery = Queries.lyrics.useStatic( + final geniusLyricsQuery = useQueries.lyrics.static( playlist?.activeTrack, ref.watch(userPreferencesProvider).geniusAccessToken, ); diff --git a/lib/pages/lyrics/synced_lyrics.dart b/lib/pages/lyrics/synced_lyrics.dart index 670c663dd..d70813999 100644 --- a/lib/pages/lyrics/synced_lyrics.dart +++ b/lib/pages/lyrics/synced_lyrics.dart @@ -44,7 +44,7 @@ class SyncedLyrics extends HookConsumerWidget { final breakpoint = useBreakpoints(); final controller = useAutoScrollController(); - final timedLyricsQuery = Queries.lyrics.useSynced(playlist?.activeTrack); + final timedLyricsQuery = useQueries.lyrics.synced(playlist?.activeTrack); final lyricValue = timedLyricsQuery.data; final lyricsMap = useMemoized( () => diff --git a/lib/pages/playlist/playlist.dart b/lib/pages/playlist/playlist.dart index 02f41e805..82f64dae1 100644 --- a/lib/pages/playlist/playlist.dart +++ b/lib/pages/playlist/playlist.dart @@ -47,8 +47,8 @@ class PlaylistView extends HookConsumerWidget { final breakpoint = useBreakpoints(); - final meSnapshot = Queries.user.useMe(ref); - final tracksSnapshot = Queries.playlist.useTracksOfQuery(ref, playlist.id!); + final meSnapshot = useQueries.user.me(ref); + final tracksSnapshot = useQueries.playlist.tracksOfQuery(ref, playlist.id!); final isPlaylistPlaying = playlistNotifier.isPlayingPlaylist(tracksSnapshot.data ?? []); diff --git a/lib/pages/root/root_app.dart b/lib/pages/root/root_app.dart index ddcd2c14d..b696a5f33 100644 --- a/lib/pages/root/root_app.dart +++ b/lib/pages/root/root_app.dart @@ -4,13 +4,11 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:platform_ui/platform_ui.dart'; -import 'package:spotube/collections/side_bar_tiles.dart'; import 'package:spotube/components/shared/dialogs/replace_downloaded_dialog.dart'; import 'package:spotube/components/root/bottom_player.dart'; import 'package:spotube/components/root/sidebar.dart'; import 'package:spotube/components/root/spotube_navigation_bar.dart'; import 'package:spotube/components/shared/page_window_title_bar.dart'; -import 'package:spotube/hooks/use_breakpoints.dart'; import 'package:spotube/hooks/use_update_checker.dart'; import 'package:spotube/provider/downloader_provider.dart'; diff --git a/lib/pages/search/search.dart b/lib/pages/search/search.dart index 1d5de6bed..6cbcefc87 100644 --- a/lib/pages/search/search.dart +++ b/lib/pages/search/search.dart @@ -52,19 +52,19 @@ class SearchPage extends HookConsumerWidget { ); final searchTrack = useInfiniteQuery( - job: Queries.search.get(SearchType.track.key), + job: useQueries.search.get(SearchType.track.key), externalData: Tuple2("", spotify), ); final searchAlbum = useInfiniteQuery( - job: Queries.search.get(SearchType.album.key), + job: useQueries.search.get(SearchType.album.key), externalData: Tuple2("", spotify), ); final searchPlaylist = useInfiniteQuery( - job: Queries.search.get(SearchType.playlist.key), + job: useQueries.search.get(SearchType.playlist.key), externalData: Tuple2("", spotify), ); final searchArtist = useInfiniteQuery( - job: Queries.search.get(SearchType.artist.key), + job: useQueries.search.get(SearchType.artist.key), externalData: Tuple2("", spotify), ); diff --git a/lib/services/mutations/album.dart b/lib/services/mutations/album.dart index 3441ac663..920e11c24 100644 --- a/lib/services/mutations/album.dart +++ b/lib/services/mutations/album.dart @@ -3,7 +3,9 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/hooks/use_spotify_mutation.dart'; class AlbumMutations { - Mutation useToggleFavorite( + const AlbumMutations(); + + Mutation toggleFavorite( WidgetRef ref, String albumId, { List? refreshQueries, diff --git a/lib/services/mutations/mutations.dart b/lib/services/mutations/mutations.dart index b61f1209c..286704865 100644 --- a/lib/services/mutations/mutations.dart +++ b/lib/services/mutations/mutations.dart @@ -2,8 +2,11 @@ import 'package:spotube/services/mutations/album.dart'; import 'package:spotube/services/mutations/playlist.dart'; import 'package:spotube/services/mutations/track.dart'; -abstract class Mutations { - static final playlist = PlaylistMutations(); - static final album = AlbumMutations(); - static final track = TrackMutations(); +class _UseMutations { + const _UseMutations._(); + final playlist = const PlaylistMutations(); + final album = const AlbumMutations(); + final track = const TrackMutations(); } + +const useMutations = _UseMutations._(); diff --git a/lib/services/mutations/playlist.dart b/lib/services/mutations/playlist.dart index 10b738cfc..106f4cdcf 100644 --- a/lib/services/mutations/playlist.dart +++ b/lib/services/mutations/playlist.dart @@ -3,7 +3,9 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/hooks/use_spotify_mutation.dart'; class PlaylistMutations { - Mutation useToggleFavorite( + const PlaylistMutations(); + + Mutation toggleFavorite( WidgetRef ref, String playlistId, { List? refreshQueries, @@ -23,7 +25,7 @@ class PlaylistMutations { ); } - Mutation useRemoveTrackOf( + Mutation removeTrackOf( WidgetRef ref, String playlistId, ) { diff --git a/lib/services/mutations/track.dart b/lib/services/mutations/track.dart index 06f3af023..2245c497a 100644 --- a/lib/services/mutations/track.dart +++ b/lib/services/mutations/track.dart @@ -3,7 +3,9 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/hooks/use_spotify_mutation.dart'; class TrackMutations { - Mutation useToggleFavorite( + const TrackMutations(); + + Mutation toggleFavorite( WidgetRef ref, String trackId, { MutationOnMutationFn? onMutate, diff --git a/lib/services/queries/album.dart b/lib/services/queries/album.dart index 90a33f762..c7860ab41 100644 --- a/lib/services/queries/album.dart +++ b/lib/services/queries/album.dart @@ -7,7 +7,9 @@ import 'package:spotube/hooks/use_spotify_infinite_query.dart'; import 'package:spotube/hooks/use_spotify_query.dart'; class AlbumQueries { - Query, dynamic> useOfMineQuery(WidgetRef ref) { + const AlbumQueries(); + + Query, dynamic> ofMine(WidgetRef ref) { return useSpotifyQuery, dynamic>( "current-user-albums", (spotify) { @@ -17,7 +19,7 @@ class AlbumQueries { ); } - Query, dynamic> useTracksOfQuery( + Query, dynamic> tracksOf( WidgetRef ref, String albumId, ) { @@ -33,7 +35,7 @@ class AlbumQueries { ); } - Query useIsSavedForMeQuery( + Query isSavedForMe( WidgetRef ref, String album, ) { @@ -46,8 +48,7 @@ class AlbumQueries { ); } - InfiniteQuery, dynamic, int> useNewReleasesQuery( - WidgetRef ref) { + InfiniteQuery, dynamic, int> newReleases(WidgetRef ref) { return useSpotifyInfiniteQuery, dynamic, int>( "new-releases", (pageParam, spotify) async { diff --git a/lib/services/queries/artist.dart b/lib/services/queries/artist.dart index 3ef4d3b2b..e183d702a 100644 --- a/lib/services/queries/artist.dart +++ b/lib/services/queries/artist.dart @@ -6,7 +6,9 @@ import 'package:spotube/hooks/use_spotify_infinite_query.dart'; import 'package:spotube/hooks/use_spotify_query.dart'; class ArtistQueries { - Query useGetArtist( + const ArtistQueries(); + + Query get( WidgetRef ref, String artist, ) { @@ -17,7 +19,7 @@ class ArtistQueries { ); } - InfiniteQuery, dynamic, String> useFollowedByMeQuery( + InfiniteQuery, dynamic, String> followedByMe( WidgetRef ref) { return useSpotifyInfiniteQuery, dynamic, String>( "user-following-artists", @@ -35,7 +37,7 @@ class ArtistQueries { ); } - Query useDoIFollowQuery( + Query doIFollow( WidgetRef ref, String artist, ) { @@ -52,7 +54,7 @@ class ArtistQueries { ); } - Query, dynamic> useTopTracksOfQuery( + Query, dynamic> topTracksOf( WidgetRef ref, String artist, ) { @@ -65,7 +67,7 @@ class ArtistQueries { ); } - InfiniteQuery, dynamic, int> useAlbumsOfQuery( + InfiniteQuery, dynamic, int> albumsOf( WidgetRef ref, String artist, ) { @@ -86,7 +88,7 @@ class ArtistQueries { ); } - Query, dynamic> useRelatedArtistsOfQuery( + Query, dynamic> relatedArtistsOf( WidgetRef ref, String artist, ) { diff --git a/lib/services/queries/category.dart b/lib/services/queries/category.dart index c4bfacb12..2cfa61267 100644 --- a/lib/services/queries/category.dart +++ b/lib/services/queries/category.dart @@ -4,7 +4,9 @@ import 'package:spotify/spotify.dart'; import 'package:spotube/hooks/use_spotify_infinite_query.dart'; class CategoryQueries { - InfiniteQuery, dynamic, int> useList( + const CategoryQueries(); + + InfiniteQuery, dynamic, int> list( WidgetRef ref, String recommendationMarket) { return useSpotifyInfiniteQuery, dynamic, int>( "category-playlists", @@ -26,7 +28,7 @@ class CategoryQueries { ); } - InfiniteQuery, dynamic, int> usePlaylistsOf( + InfiniteQuery, dynamic, int> playlistsOf( WidgetRef ref, String category, ) { diff --git a/lib/services/queries/lyrics.dart b/lib/services/queries/lyrics.dart index 003646fb2..17bca7899 100644 --- a/lib/services/queries/lyrics.dart +++ b/lib/services/queries/lyrics.dart @@ -7,7 +7,9 @@ import 'package:spotube/models/spotube_track.dart'; import 'package:spotube/utils/service_utils.dart'; class LyricsQueries { - Query useStatic( + const LyricsQueries(); + + Query static( Track? track, String geniusAccessToken, ) { @@ -30,7 +32,7 @@ class LyricsQueries { ); } - Query useSynced( + Query synced( Track? track, ) { return useQuery( diff --git a/lib/services/queries/playlist.dart b/lib/services/queries/playlist.dart index ffecc137b..e927779bf 100644 --- a/lib/services/queries/playlist.dart +++ b/lib/services/queries/playlist.dart @@ -6,7 +6,9 @@ import 'package:spotube/hooks/use_spotify_infinite_query.dart'; import 'package:spotube/hooks/use_spotify_query.dart'; class PlaylistQueries { - Query useDoesUserFollowQuery( + const PlaylistQueries(); + + Query doesUserFollow( WidgetRef ref, String playlistId, String userId, @@ -21,7 +23,7 @@ class PlaylistQueries { ); } - Query, dynamic> useOfMineQuery(WidgetRef ref) { + Query, dynamic> ofMine(WidgetRef ref) { return useSpotifyQuery, dynamic>( "current-user-playlists", (spotify) { @@ -42,7 +44,7 @@ class PlaylistQueries { ); } - Query, dynamic> useTracksOfQuery( + Query, dynamic> tracksOfQuery( WidgetRef ref, String playlistId, ) { @@ -53,7 +55,7 @@ class PlaylistQueries { ); } - InfiniteQuery, dynamic, int> useFeaturedQuery( + InfiniteQuery, dynamic, int> featured( WidgetRef ref, ) { return useSpotifyInfiniteQuery, dynamic, int>( diff --git a/lib/services/queries/queries.dart b/lib/services/queries/queries.dart index 25ae9c814..d6f59f4f9 100644 --- a/lib/services/queries/queries.dart +++ b/lib/services/queries/queries.dart @@ -6,12 +6,15 @@ import 'package:spotube/services/queries/playlist.dart'; import 'package:spotube/services/queries/search.dart'; import 'package:spotube/services/queries/user.dart'; -abstract class Queries { - static final album = AlbumQueries(); - static final artist = ArtistQueries(); - static final category = CategoryQueries(); - static final lyrics = LyricsQueries(); - static final playlist = PlaylistQueries(); - static final search = SearchQueries(); - static final user = UserQueries(); +class Queries { + const Queries._(); + final album = const AlbumQueries(); + final artist = const ArtistQueries(); + final category = const CategoryQueries(); + final lyrics = const LyricsQueries(); + final playlist = const PlaylistQueries(); + final search = const SearchQueries(); + final user = const UserQueries(); } + +const useQueries = Queries._(); diff --git a/lib/services/queries/search.dart b/lib/services/queries/search.dart index 21876ff0e..4717a412d 100644 --- a/lib/services/queries/search.dart +++ b/lib/services/queries/search.dart @@ -4,7 +4,8 @@ import 'package:spotify/spotify.dart'; import 'package:spotube/hooks/use_spotify_infinite_query.dart'; class SearchQueries { - InfiniteQuery, dynamic, int> useSearchQuery( + const SearchQueries(); + InfiniteQuery, dynamic, int> query( WidgetRef ref, String query, SearchType searchType, diff --git a/lib/services/queries/user.dart b/lib/services/queries/user.dart index 2449ed9ea..6f94264d2 100644 --- a/lib/services/queries/user.dart +++ b/lib/services/queries/user.dart @@ -5,7 +5,8 @@ import 'package:spotube/hooks/use_spotify_query.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; class UserQueries { - Query useMe(WidgetRef ref) { + const UserQueries(); + Query me(WidgetRef ref) { return useSpotifyQuery( "current-user", (spotify) async { From 761743991520609dd2b2dcb12cd6e4e75a8f6925 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 26 Feb 2023 14:01:53 +0600 Subject: [PATCH 3/7] feat: compatibility with fl-query nextPage method change --- lib/components/album/album_card.dart | 2 +- lib/components/genre/category_card.dart | 9 ++------- lib/components/playlist/playlist_card.dart | 2 +- lib/components/root/sidebar.dart | 11 ----------- lib/hooks/use_spotify_infinite_query.dart | 12 +++++++++--- lib/hooks/use_spotify_query.dart | 12 +++++++++--- lib/services/queries/album.dart | 13 +++++-------- lib/services/queries/artist.dart | 18 +++++++++--------- lib/services/queries/category.dart | 20 +++++++++++--------- lib/services/queries/playlist.dart | 10 ++++++---- lib/services/queries/search.dart | 12 ++++++++---- pubspec.lock | 4 ++-- 12 files changed, 63 insertions(+), 62 deletions(-) diff --git a/lib/components/album/album_card.dart b/lib/components/album/album_card.dart index 8e944c618..e06b559ef 100644 --- a/lib/components/album/album_card.dart +++ b/lib/components/album/album_card.dart @@ -48,7 +48,7 @@ class AlbumCard extends HookConsumerWidget { final playlistNotifier = ref.watch(PlaylistQueueNotifier.notifier); final queryBowl = QueryClient.of(context); final query = queryBowl - .getQuery, SpotifyApi>("album-tracks/${album.id}"); + .getQuery, dynamic>("album-tracks/${album.id}"); final tracks = useState(query?.data ?? album.tracks ?? []); bool isPlaylistPlaying = playlistNotifier.isPlayingPlaylist(tracks.value); final int marginH = diff --git a/lib/components/genre/category_card.dart b/lib/components/genre/category_card.dart index 02c66e62f..5b2bbf9cf 100644 --- a/lib/components/genre/category_card.dart +++ b/lib/components/genre/category_card.dart @@ -8,7 +8,6 @@ import 'package:spotube/components/shared/shimmers/shimmer_playbutton_card.dart' import 'package:spotube/components/shared/waypoint.dart'; import 'package:spotube/components/playlist/playlist_card.dart'; import 'package:spotube/models/logger.dart'; -import 'package:spotube/provider/spotify_provider.dart'; import 'package:spotube/services/queries/queries.dart'; class CategoryCard extends HookConsumerWidget { @@ -23,14 +22,10 @@ class CategoryCard extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { final scrollController = useScrollController(); - final spotify = ref.watch(spotifyProvider); final playlistQuery = useQueries.category.playlistsOf( ref, category.id!, ); - final hasNextPage = playlistQuery.pages.isEmpty - ? false - : (playlistQuery.pages.last.items?.length ?? 0) == 5; final playlists = playlistQuery.pages .expand( @@ -48,7 +43,7 @@ class CategoryCard extends HookConsumerWidget { ], ), ), - playlistQuery.hasErrors + playlistQuery.hasPageError && !playlistQuery.hasPageData ? PlatformText( "Something Went Wrong\n${playlistQuery.errors.first}") : SizedBox( @@ -75,7 +70,7 @@ class CategoryCard extends HookConsumerWidget { children: [ ...playlists .map((playlist) => PlaylistCard(playlist)), - if (hasNextPage) + if (playlistQuery.hasNextPage) const ShimmerPlaybuttonCard(count: 1), ], ), diff --git a/lib/components/playlist/playlist_card.dart b/lib/components/playlist/playlist_card.dart index 8d0c92150..4065e714d 100644 --- a/lib/components/playlist/playlist_card.dart +++ b/lib/components/playlist/playlist_card.dart @@ -26,7 +26,7 @@ class PlaylistCard extends HookConsumerWidget { final playing = useStream(PlaylistQueueNotifier.playing).data ?? PlaylistQueueNotifier.isPlaying; final queryBowl = QueryClient.of(context); - final query = queryBowl.getQuery, SpotifyApi>( + final query = queryBowl.getQuery, dynamic>( "playlist-tracks/${playlist.id}", ); final tracks = useState(query?.data ?? []); diff --git a/lib/components/root/sidebar.dart b/lib/components/root/sidebar.dart index 451dfa783..e8197b769 100644 --- a/lib/components/root/sidebar.dart +++ b/lib/components/root/sidebar.dart @@ -202,19 +202,8 @@ class SidebarFooter extends HookConsumerWidget { placeholder: ImagePlaceholder.artist, ); - // TODO: Remove below code after fl-query ^0.4.0 - /// Temporary fix before fl-query 0.4.0 final auth = ref.watch(AuthenticationNotifier.provider); - useEffect(() { - if (auth != null && me.hasError) { - me.refresh(); - } - return null; - }, [auth, me.hasError]); - - /// =================================== - return Padding( padding: const EdgeInsets.all(16).copyWith(left: 0), child: Row( diff --git a/lib/hooks/use_spotify_infinite_query.dart b/lib/hooks/use_spotify_infinite_query.dart index 0507a9f8f..d56d3ca7a 100644 --- a/lib/hooks/use_spotify_infinite_query.dart +++ b/lib/hooks/use_spotify_infinite_query.dart @@ -39,9 +39,15 @@ InfiniteQuery ); useEffect(() { - query.refreshAll(); - return null; - }, [spotify]); + return ref.listenManual( + spotifyProvider, + (previous, next) { + if (previous != next) { + query.refreshAll(); + } + }, + ).close; + }, [query]); return query; } diff --git a/lib/hooks/use_spotify_query.dart b/lib/hooks/use_spotify_query.dart index 700698d6e..209f3391a 100644 --- a/lib/hooks/use_spotify_query.dart +++ b/lib/hooks/use_spotify_query.dart @@ -38,9 +38,15 @@ Query useSpotifyQuery( ); useEffect(() { - query.refresh(); - return null; - }, [spotify]); + return ref.listenManual( + spotifyProvider, + (previous, next) { + if (previous != next) { + query.refresh(); + } + }, + ).close; + }, [query]); return query; } diff --git a/lib/services/queries/album.dart b/lib/services/queries/album.dart index c7860ab41..629f732ad 100644 --- a/lib/services/queries/album.dart +++ b/lib/services/queries/album.dart @@ -1,5 +1,4 @@ import 'package:catcher/catcher.dart'; -import 'package:collection/collection.dart'; import 'package:fl_query/fl_query.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; @@ -68,13 +67,11 @@ class AlbumQueries { }, ref: ref, initialPage: 0, - nextPage: (lastParam, pages) { - final lastPage = pages.elementAtOrNull(lastParam); - if (lastPage == null || - lastPage.isLast || - (lastPage.items ?? []).length < 5) return null; - - return lastPage.nextOffset; + nextPage: (lastPage, lastPageData) { + if (lastPageData.isLast || (lastPageData.items ?? []).length < 5) { + return null; + } + return lastPageData.nextOffset; }, ); } diff --git a/lib/services/queries/artist.dart b/lib/services/queries/artist.dart index e183d702a..fed55429e 100644 --- a/lib/services/queries/artist.dart +++ b/lib/services/queries/artist.dart @@ -1,4 +1,3 @@ -import 'package:collection/collection.dart'; import 'package:fl_query/fl_query.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; @@ -29,10 +28,12 @@ class ArtistQueries { .getPage(15, pageParam); }, initialPage: "", - nextPage: (lastPage, pages) => - pages.last.isLast || (pages.last.items?.length ?? 0) < 15 - ? null - : pages.last.after, + nextPage: (lastPage, lastPageData) { + if (lastPageData.isLast || (lastPageData.items ?? []).length < 15) { + return null; + } + return lastPageData.after; + }, ref: ref, ); } @@ -77,12 +78,11 @@ class ArtistQueries { return spotify.artists.albums(artist).getPage(5, pageParam); }, initialPage: 0, - nextPage: (lastPage, pages) { - final page = pages.elementAtOrNull(lastPage); - if (page == null || page.isLast || (page.items ?? []).length < 5) { + nextPage: (lastPage, lastPageData) { + if (lastPageData.isLast || (lastPageData.items ?? []).length < 5) { return null; } - return page.nextOffset; + return lastPageData.nextOffset; }, ref: ref, ); diff --git a/lib/services/queries/category.dart b/lib/services/queries/category.dart index 2cfa61267..4f646b13d 100644 --- a/lib/services/queries/category.dart +++ b/lib/services/queries/category.dart @@ -18,11 +18,11 @@ class CategoryQueries { return categories; }, initialPage: 0, - nextPage: (lastPage, pages) { - if (pages.isEmpty) return lastPage + 1; - return pages.last.isLast || (pages.last.items?.length ?? 0) < 15 - ? null - : pages.last.nextOffset; + nextPage: (lastPage, lastPageData) { + if (lastPageData.isLast || (lastPageData.items ?? []).length < 15) { + return null; + } + return lastPageData.nextOffset; }, ref: ref, ); @@ -42,10 +42,12 @@ class CategoryQueries { return playlists; }, initialPage: 0, - nextPage: (lastPage, pages) => - pages.last.isLast || (pages.last.items?.length ?? 0) < 5 - ? null - : pages.last.nextOffset, + nextPage: (lastPage, lastPageData) { + if (lastPageData.isLast || (lastPageData.items ?? []).length < 5) { + return null; + } + return lastPageData.nextOffset; + }, ref: ref, ); } diff --git a/lib/services/queries/playlist.dart b/lib/services/queries/playlist.dart index e927779bf..55e4ed6f4 100644 --- a/lib/services/queries/playlist.dart +++ b/lib/services/queries/playlist.dart @@ -71,10 +71,12 @@ class PlaylistQueries { } }, initialPage: 0, - nextPage: (lastPage, pages) => - pages.last.isLast || (pages.last.items?.length ?? 0) < 5 - ? null - : pages.last.nextOffset, + nextPage: (lastPage, lastPageData) { + if (lastPageData.isLast || (lastPageData.items ?? []).length < 5) { + return null; + } + return lastPageData.nextOffset; + }, ref: ref, ); } diff --git a/lib/services/queries/search.dart b/lib/services/queries/search.dart index 4717a412d..e68546202 100644 --- a/lib/services/queries/search.dart +++ b/lib/services/queries/search.dart @@ -22,10 +22,14 @@ class SearchQueries { }, ref: ref, initialPage: 0, - nextPage: (lastPage, pages) => - pages.last.isNotEmpty && (pages.last.first.items?.length ?? 0) < 10 - ? null - : pages.last.last.nextOffset, + nextPage: (lastPage, lastPageData) { + if (lastPageData.isEmpty) return null; + if ((lastPageData.first.isLast || + (lastPageData.first.items ?? []).length < 10)) { + return null; + } + return lastPageData.first.nextOffset; + }, ); } } diff --git a/pubspec.lock b/pubspec.lock index 992e73fcf..a95d9301d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -535,7 +535,7 @@ packages: description: path: "packages/fl_query" ref: new-architecture - resolved-ref: d964216ee17e600f79c33f1811080877c8c1b510 + resolved-ref: f2a23b085cd657a1612d87749f6592b4d67814c5 url: "https://github.com/KRTirtho/fl-query.git" source: git version: "0.3.1" @@ -544,7 +544,7 @@ packages: description: path: "packages/fl_query_hooks" ref: new-architecture - resolved-ref: d964216ee17e600f79c33f1811080877c8c1b510 + resolved-ref: f2a23b085cd657a1612d87749f6592b4d67814c5 url: "https://github.com/KRTirtho/fl-query.git" source: git version: "0.3.1" From b78b65737781a0519cb0d9658aab1fd3378c0a0f Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 26 Feb 2023 22:07:02 +0600 Subject: [PATCH 4/7] chore: enable search again --- lib/collections/routes.dart | 13 +++-- lib/pages/search/search.dart | 94 +++++++++++--------------------- lib/services/queries/search.dart | 3 +- 3 files changed, 40 insertions(+), 70 deletions(-) diff --git a/lib/collections/routes.dart b/lib/collections/routes.dart index 5fbed70f2..353d4e5c8 100644 --- a/lib/collections/routes.dart +++ b/lib/collections/routes.dart @@ -3,6 +3,7 @@ import 'package:flutter/widgets.dart'; import 'package:go_router/go_router.dart'; import 'package:spotify/spotify.dart' hide Search; import 'package:spotube/pages/home/home.dart'; +import 'package:spotube/pages/search/search.dart'; import 'package:spotube/pages/settings/blacklist.dart'; import 'package:spotube/pages/settings/about.dart'; import 'package:spotube/utils/platform.dart'; @@ -33,12 +34,12 @@ final router = GoRouter( path: "/", pageBuilder: (context, state) => SpotubePage(child: const HomePage()), ), - // GoRoute( - // path: "/search", - // name: "Search", - // pageBuilder: (context, state) => - // SpotubePage(child: const SearchPage()), - // ), + GoRoute( + path: "/search", + name: "Search", + pageBuilder: (context, state) => + SpotubePage(child: const SearchPage()), + ), GoRoute( path: "/library", name: "Library", diff --git a/lib/pages/search/search.dart b/lib/pages/search/search.dart index 6cbcefc87..f7dddaefc 100644 --- a/lib/pages/search/search.dart +++ b/lib/pages/search/search.dart @@ -1,4 +1,3 @@ -import 'package:fl_query_hooks/fl_query_hooks.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart' hide Page; import 'package:flutter_hooks/flutter_hooks.dart'; @@ -17,14 +16,12 @@ import 'package:spotube/components/artist/artist_card.dart'; import 'package:spotube/components/playlist/playlist_card.dart'; import 'package:spotube/hooks/use_breakpoints.dart'; import 'package:spotube/provider/authentication_provider.dart'; -import 'package:spotube/provider/spotify_provider.dart'; import 'package:spotube/provider/playlist_queue_provider.dart'; import 'package:spotube/services/queries/queries.dart'; import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/primitive_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; -import 'package:tuple/tuple.dart'; import 'package:collection/collection.dart'; final searchTermStateProvider = StateProvider((ref) => ""); @@ -37,36 +34,21 @@ class SearchPage extends HookConsumerWidget { ref.watch(AuthenticationNotifier.provider); final authenticationNotifier = ref.watch(AuthenticationNotifier.provider.notifier); - final spotify = ref.watch(spotifyProvider); final albumController = useScrollController(); final playlistController = useScrollController(); final artistController = useScrollController(); final breakpoint = useBreakpoints(); - final getVariables = useCallback( - () => Tuple2( - ref.read(searchTermStateProvider), - spotify, - ), - [], - ); + final searchTerm = ref.watch(searchTermStateProvider); - final searchTrack = useInfiniteQuery( - job: useQueries.search.get(SearchType.track.key), - externalData: Tuple2("", spotify), - ); - final searchAlbum = useInfiniteQuery( - job: useQueries.search.get(SearchType.album.key), - externalData: Tuple2("", spotify), - ); - final searchPlaylist = useInfiniteQuery( - job: useQueries.search.get(SearchType.playlist.key), - externalData: Tuple2("", spotify), - ); - final searchArtist = useInfiniteQuery( - job: useQueries.search.get(SearchType.artist.key), - externalData: Tuple2("", spotify), - ); + final searchTrack = + useQueries.search.query(ref, searchTerm, SearchType.track); + final searchAlbum = + useQueries.search.query(ref, searchTerm, SearchType.album); + final searchPlaylist = + useQueries.search.query(ref, searchTerm, SearchType.playlist); + final searchArtist = + useQueries.search.query(ref, searchTerm, SearchType.artist); void onSearch() { for (final query in [ @@ -75,10 +57,7 @@ class SearchPage extends HookConsumerWidget { searchPlaylist, searchArtist, ]) { - query.enabled = false; - query.fetched = false; - query.setExternalData(getVariables()); - query.refetchPages(); + query.refreshAll(); } } @@ -96,10 +75,6 @@ class SearchPage extends HookConsumerWidget { ), color: PlatformTheme.of(context).scaffoldBackgroundColor, child: PlatformTextField( - onChanged: (value) { - ref.read(searchTermStateProvider.notifier).state = - value; - }, prefixIcon: SpotubeIcons.search, prefixIconColor: PlatformProperty.only( ios: @@ -108,6 +83,8 @@ class SearchPage extends HookConsumerWidget { ).resolve(platform!), placeholder: "Search...", onSubmitted: (value) { + ref.read(searchTermStateProvider.notifier).state = + value; onSearch(); }, ), @@ -127,7 +104,7 @@ class SearchPage extends HookConsumerWidget { ...searchAlbum.pages, ...searchPlaylist.pages, ...searchArtist.pages, - ].expand((page) => page ?? []).toList(); + ].expand((page) => page).toList(); for (MapEntry page in pages.asMap().entries) { for (var item in page.value.items ?? []) { if (item is AlbumSimple) { @@ -153,12 +130,10 @@ class SearchPage extends HookConsumerWidget { children: [ if (tracks.isNotEmpty) PlatformText.headline("Songs"), - if (searchTrack.isLoading && - !searchTrack.isFetchingNextPage) + if (searchTrack.isLoadingPage) const PlatformCircularProgressIndicator() - else if (searchTrack.hasError) - PlatformText(searchTrack - .error?[searchTrack.pageParams.last] + else if (searchTrack.hasPageError) + PlatformText(searchTrack.errors.lastOrNull ?.toString() ?? "") else @@ -204,10 +179,10 @@ class SearchPage extends HookConsumerWidget { tracks.isNotEmpty) Center( child: PlatformTextButton( - onPressed: searchTrack.isFetchingNextPage + onPressed: searchTrack.isRefreshingPage ? null - : () => searchTrack.fetchNextPage(), - child: searchTrack.isFetchingNextPage + : () => searchTrack.fetchNext(), + child: searchTrack.isRefreshingPage ? const PlatformCircularProgressIndicator() : const PlatformText("Load more"), ), @@ -231,7 +206,7 @@ class SearchPage extends HookConsumerWidget { controller: playlistController, child: Waypoint( onTouchEdge: () { - searchPlaylist.fetchNextPage(); + searchPlaylist.fetchNext(); }, controller: playlistController, child: SingleChildScrollView( @@ -256,13 +231,11 @@ class SearchPage extends HookConsumerWidget { ), ), ), - if (searchPlaylist.isLoading && - !searchPlaylist.isFetchingNextPage) + if (searchPlaylist.isLoadingPage) const PlatformCircularProgressIndicator(), - if (searchPlaylist.hasError) + if (searchPlaylist.hasPageError) PlatformText( - searchPlaylist.error?[ - searchPlaylist.pageParams.last] + searchPlaylist.errors.lastOrNull ?.toString() ?? "", ), @@ -283,7 +256,7 @@ class SearchPage extends HookConsumerWidget { child: Waypoint( controller: artistController, onTouchEdge: () { - searchArtist.fetchNextPage(); + searchArtist.fetchNext(); }, child: SingleChildScrollView( scrollDirection: Axis.horizontal, @@ -311,13 +284,11 @@ class SearchPage extends HookConsumerWidget { ), ), ), - if (searchArtist.isLoading && - !searchArtist.isFetchingNextPage) + if (searchArtist.isLoadingPage) const PlatformCircularProgressIndicator(), - if (searchArtist.hasError) + if (searchArtist.hasPageError) PlatformText( - searchArtist.error?[ - searchArtist.pageParams.last] + searchArtist.errors.lastOrNull ?.toString() ?? "", ), @@ -338,7 +309,7 @@ class SearchPage extends HookConsumerWidget { child: Waypoint( controller: albumController, onTouchEdge: () { - searchAlbum.fetchNextPage(); + searchAlbum.fetchNext(); }, child: SingleChildScrollView( scrollDirection: Axis.horizontal, @@ -364,14 +335,11 @@ class SearchPage extends HookConsumerWidget { ), ), ), - if (searchAlbum.isLoading && - !searchAlbum.isFetchingNextPage) + if (searchAlbum.isLoadingPage) const PlatformCircularProgressIndicator(), - if (searchAlbum.hasError) + if (searchAlbum.hasPageError) PlatformText( - searchAlbum - .error?[searchAlbum.pageParams.last] - ?.toString() ?? + searchAlbum.errors.lastOrNull?.toString() ?? "", ), ], diff --git a/lib/services/queries/search.dart b/lib/services/queries/search.dart index e68546202..7eb3e1392 100644 --- a/lib/services/queries/search.dart +++ b/lib/services/queries/search.dart @@ -11,7 +11,7 @@ class SearchQueries { SearchType searchType, ) { return useSpotifyInfiniteQuery, dynamic, int>( - "search-query/$query", + "search-query/${searchType.key}", (page, spotify) { if (query.trim().isEmpty) return []; final queryString = query; @@ -20,6 +20,7 @@ class SearchQueries { types: [searchType], ).getPage(10, page); }, + enabled: false, ref: ref, initialPage: 0, nextPage: (lastPage, lastPageData) { From f5dc76a98f55f0f032a6fe4208465899f932355a Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Thu, 2 Mar 2023 11:59:00 +0600 Subject: [PATCH 5/7] fix(search): has to submit twice for search results --- lib/components/album/album_card.dart | 8 +- lib/components/library/user_playlists.dart | 26 ++++--- .../playlist/playlist_genre_view.dart | 73 ------------------- lib/components/shared/heart_button.dart | 2 +- lib/pages/search/search.dart | 26 ++++--- pubspec.lock | 4 +- 6 files changed, 37 insertions(+), 102 deletions(-) delete mode 100644 lib/components/playlist/playlist_genre_view.dart diff --git a/lib/components/album/album_card.dart b/lib/components/album/album_card.dart index e06b559ef..eade617fb 100644 --- a/lib/components/album/album_card.dart +++ b/lib/components/album/album_card.dart @@ -1,4 +1,4 @@ -import 'package:fl_query/fl_query.dart'; +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'; @@ -46,8 +46,8 @@ class AlbumCard extends HookConsumerWidget { final playing = useStream(PlaylistQueueNotifier.playing).data ?? PlaylistQueueNotifier.isPlaying; final playlistNotifier = ref.watch(PlaylistQueueNotifier.notifier); - final queryBowl = QueryClient.of(context); - final query = queryBowl + final queryClient = useQueryClient(); + final query = queryClient .getQuery, dynamic>("album-tracks/${album.id}"); final tracks = useState(query?.data ?? album.tracks ?? []); bool isPlaylistPlaying = playlistNotifier.isPlayingPlaylist(tracks.value); @@ -100,7 +100,7 @@ class AlbumCard extends HookConsumerWidget { updating.value = true; try { final fetchedTracks = - await queryBowl.fetchQuery, SpotifyApi>( + await queryClient.fetchQuery, SpotifyApi>( "album-tracks/${album.id}", () { return spotify.albums diff --git a/lib/components/library/user_playlists.dart b/lib/components/library/user_playlists.dart index b0eef87d7..d618b3e10 100644 --- a/lib/components/library/user_playlists.dart +++ b/lib/components/library/user_playlists.dart @@ -35,17 +35,21 @@ class UserPlaylists extends HookConsumerWidget { final playlistsQuery = useQueries.playlist.ofMine(ref); - Image image = Image(); - image.height = 300; - image.width = 300; - PlaylistSimple likedTracksPlaylist = PlaylistSimple(); - likedTracksPlaylist.name = "Liked Tracks"; - likedTracksPlaylist.type = "playlist"; - likedTracksPlaylist.collaborative = false; - likedTracksPlaylist.public = false; - likedTracksPlaylist.id = "user-liked-tracks"; - image.url = "https://t.scdn.co/images/3099b3803ad9496896c43f22fe9be8c4.png"; - likedTracksPlaylist.images = [image]; + final likedTracksPlaylist = useMemoized( + () => PlaylistSimple() + ..name = "Liked Tracks" + ..type = "playlist" + ..collaborative = false + ..public = false + ..id = "user-liked-tracks" + ..images = [ + Image() + ..height = 300 + ..width = 300 + ..url = + "https://t.scdn.co/images/3099b3803ad9496896c43f22fe9be8c4.png" + ], + []); final playlists = useMemoized( () { diff --git a/lib/components/playlist/playlist_genre_view.dart b/lib/components/playlist/playlist_genre_view.dart deleted file mode 100644 index 347089f58..000000000 --- a/lib/components/playlist/playlist_genre_view.dart +++ /dev/null @@ -1,73 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:platform_ui/platform_ui.dart'; -import 'package:spotify/spotify.dart'; -import 'package:spotube/components/shared/page_window_title_bar.dart'; -import 'package:spotube/components/playlist/playlist_card.dart'; -import 'package:spotube/provider/spotify_provider.dart'; - -class PlaylistGenreView extends ConsumerWidget { - final String genreId; - final String genreName; - final Iterable? playlists; - const PlaylistGenreView( - this.genreId, - this.genreName, { - this.playlists, - Key? key, - }) : super(key: key); - - @override - Widget build(BuildContext context, ref) { - return Scaffold( - appBar: PageWindowTitleBar( - leading: const PlatformBackButton(), - ), - body: Column( - children: [ - PlatformText.subheading( - genreName, - textAlign: TextAlign.center, - ), - Consumer( - builder: (context, ref, child) { - SpotifyApi spotifyApi = ref.watch(spotifyProvider); - return Expanded( - child: SingleChildScrollView( - child: FutureBuilder>( - future: playlists == null - ? (genreId != "user-featured-playlists" - ? spotifyApi.playlists - .getByCategoryId(genreId) - .all() - : spotifyApi.playlists.featured.all()) - : Future.value(playlists), - builder: (context, snapshot) { - if (snapshot.hasError) { - return const Center(child: Text("Error occurred")); - } - if (!snapshot.hasData) { - return const PlatformCircularProgressIndicator(); - } - return Center( - child: Wrap( - children: snapshot.data! - .map( - (playlist) => Padding( - padding: const EdgeInsets.all(8.0), - child: PlaylistCard(playlist), - ), - ) - .toList(), - ), - ); - }), - ), - ); - }, - ) - ], - ), - ); - } -} diff --git a/lib/components/shared/heart_button.dart b/lib/components/shared/heart_button.dart index 0a547d26a..5c7e6811a 100644 --- a/lib/components/shared/heart_button.dart +++ b/lib/components/shared/heart_button.dart @@ -125,7 +125,7 @@ class PlaylistHeartButton extends HookConsumerWidget { final isLikedQuery = useQueries.playlist.doesUserFollow( ref, playlist.id!, - me.data!.id!, + me.data?.id ?? '', ); final togglePlaylistLike = useMutations.playlist.toggleFavorite( diff --git a/lib/pages/search/search.dart b/lib/pages/search/search.dart index f7dddaefc..1c4b247b1 100644 --- a/lib/pages/search/search.dart +++ b/lib/pages/search/search.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart' hide Page; import 'package:flutter_hooks/flutter_hooks.dart'; @@ -50,15 +52,13 @@ class SearchPage extends HookConsumerWidget { final searchArtist = useQueries.search.query(ref, searchTerm, SearchType.artist); - void onSearch() { - for (final query in [ - searchTrack, - searchAlbum, - searchPlaylist, - searchArtist, - ]) { - query.refreshAll(); - } + Future onSearch() async { + await Future.wait([ + searchTrack.refreshAll(), + searchAlbum.refreshAll(), + searchPlaylist.refreshAll(), + searchArtist.refreshAll(), + ]); } return SafeArea( @@ -82,10 +82,14 @@ class SearchPage extends HookConsumerWidget { other: null, ).resolve(platform!), placeholder: "Search...", - onSubmitted: (value) { + onSubmitted: (value) async { ref.read(searchTermStateProvider.notifier).state = value; - onSearch(); + // Fl-Query is too fast, so we need to delay the search + // to prevent spamming the API :) + Timer(const Duration(milliseconds: 50), () { + onSearch(); + }); }, ), ), diff --git a/pubspec.lock b/pubspec.lock index a95d9301d..10fbc3442 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -535,7 +535,7 @@ packages: description: path: "packages/fl_query" ref: new-architecture - resolved-ref: f2a23b085cd657a1612d87749f6592b4d67814c5 + resolved-ref: cf2550a2909d0cb957324fe114acacb431a5f33a url: "https://github.com/KRTirtho/fl-query.git" source: git version: "0.3.1" @@ -544,7 +544,7 @@ packages: description: path: "packages/fl_query_hooks" ref: new-architecture - resolved-ref: f2a23b085cd657a1612d87749f6592b4d67814c5 + resolved-ref: cf2550a2909d0cb957324fe114acacb431a5f33a url: "https://github.com/KRTirtho/fl-query.git" source: git version: "0.3.1" From bdd70984e6670813e508786e74cd2ea4a1fe1d53 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Thu, 2 Mar 2023 13:20:07 +0600 Subject: [PATCH 6/7] fix(playbutton_card): play and add to queue needs 2 clicks work feat: add disk caching to liked tracks and categories query --- lib/extensions/map.dart | 15 ++++++++ lib/extensions/page.dart | 61 ++++++++++++++++++++++++++++++ lib/services/queries/category.dart | 11 ++++++ lib/services/queries/playlist.dart | 14 +++++++ pubspec.lock | 4 +- 5 files changed, 103 insertions(+), 2 deletions(-) create mode 100644 lib/extensions/map.dart create mode 100644 lib/extensions/page.dart diff --git a/lib/extensions/map.dart b/lib/extensions/map.dart new file mode 100644 index 000000000..48f2935c2 --- /dev/null +++ b/lib/extensions/map.dart @@ -0,0 +1,15 @@ +extension CastDeepMaps on Map { + Map castKeyDeep() { + return cast().map((key, value) { + if (value is Map) { + return MapEntry(key, value.castKeyDeep()); + } else if (value is List) { + return MapEntry( + key, + value.map((e) => e is Map ? e.castKeyDeep() : e).toList(), + ); + } + return MapEntry(key, value); + }); + } +} diff --git a/lib/extensions/page.dart b/lib/extensions/page.dart new file mode 100644 index 000000000..34343fb5c --- /dev/null +++ b/lib/extensions/page.dart @@ -0,0 +1,61 @@ +import 'package:spotify/spotify.dart'; + +extension CursorPageJson on CursorPage { + static CursorPage fromJson( + Map json, + T Function(dynamic json) itemFromJson, + ) { + final metadata = Paging.fromJson(json["metadata"]); + final paging = CursorPaging(); + paging.cursors = Cursor.fromJson(json["metadata"])..after = json["after"]; + paging.href = metadata.href; + paging.itemsNative = paging.itemsNative; + paging.limit = metadata.limit; + paging.next = metadata.next; + return CursorPage( + paging, + itemFromJson, + ); + } + + Map toJson() { + return { + "after": after, + "metadata": metadata.toJson(), + }; + } +} + +extension PagingToJson on Paging { + Map toJson() { + return { + "items": itemsNative, + "total": total, + "next": next, + "previous": previous, + "limit": limit, + "offset": offset, + "href": href, + }; + } +} + +extension PageJson on Page { + static Page fromJson( + Map json, + T Function(dynamic json) itemFromJson, + ) { + return Page( + Paging.fromJson( + Map.castFrom(json["metadata"]), + ), + itemFromJson, + ); + } + + Map toJson() { + return { + "metadata": metadata.toJson(), + }; + } +} diff --git a/lib/services/queries/category.dart b/lib/services/queries/category.dart index 4f646b13d..c4e8d188d 100644 --- a/lib/services/queries/category.dart +++ b/lib/services/queries/category.dart @@ -1,6 +1,8 @@ import 'package:fl_query/fl_query.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; +import 'package:spotube/extensions/map.dart'; +import 'package:spotube/extensions/page.dart'; import 'package:spotube/hooks/use_spotify_infinite_query.dart'; class CategoryQueries { @@ -24,6 +26,15 @@ class CategoryQueries { } return lastPageData.nextOffset; }, + jsonConfig: JsonConfig>( + toJson: (page) => page.toJson(), + fromJson: (json) => PageJson.fromJson( + json, + (json) { + return Category.fromJson((json as Map).castKeyDeep()); + }, + ), + ), ref: ref, ); } diff --git a/lib/services/queries/playlist.dart b/lib/services/queries/playlist.dart index 55e4ed6f4..06f8d952b 100644 --- a/lib/services/queries/playlist.dart +++ b/lib/services/queries/playlist.dart @@ -2,6 +2,8 @@ import 'package:catcher/catcher.dart'; import 'package:fl_query/fl_query.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; +import 'package:spotube/extensions/map.dart'; +import 'package:spotube/extensions/track.dart'; import 'package:spotube/hooks/use_spotify_infinite_query.dart'; import 'package:spotube/hooks/use_spotify_query.dart'; @@ -51,6 +53,18 @@ class PlaylistQueries { return useSpotifyQuery, dynamic>( "playlist-tracks/$playlistId", (spotify) => tracksOf(playlistId, spotify), + jsonConfig: playlistId == "user-liked-tracks" + ? JsonConfig( + toJson: (tracks) => { + 'tracks': tracks.map((e) => e.toJson()).toList() + }, + fromJson: (json) => (json['tracks'] as List) + .map((e) => Track.fromJson( + (e as Map).castKeyDeep(), + )) + .toList(), + ) + : null, ref: ref, ); } diff --git a/pubspec.lock b/pubspec.lock index 10fbc3442..709564da0 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -535,7 +535,7 @@ packages: description: path: "packages/fl_query" ref: new-architecture - resolved-ref: cf2550a2909d0cb957324fe114acacb431a5f33a + resolved-ref: "64d661779a88c845e5280f3da99a2bd90bdc586b" url: "https://github.com/KRTirtho/fl-query.git" source: git version: "0.3.1" @@ -544,7 +544,7 @@ packages: description: path: "packages/fl_query_hooks" ref: new-architecture - resolved-ref: cf2550a2909d0cb957324fe114acacb431a5f33a + resolved-ref: "64d661779a88c845e5280f3da99a2bd90bdc586b" url: "https://github.com/KRTirtho/fl-query.git" source: git version: "0.3.1" From 1e5a4ef40265ebef6b5a798ce439b990527d1a7e Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Thu, 2 Mar 2023 19:07:14 +0600 Subject: [PATCH 7/7] chore: update deps --- pubspec.lock | 8 ++++---- pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 709564da0..4c4d707f4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -535,7 +535,7 @@ packages: description: path: "packages/fl_query" ref: new-architecture - resolved-ref: "64d661779a88c845e5280f3da99a2bd90bdc586b" + resolved-ref: "0c819d4e11572d592b5334280b8b4f2657f21459" url: "https://github.com/KRTirtho/fl-query.git" source: git version: "0.3.1" @@ -544,7 +544,7 @@ packages: description: path: "packages/fl_query_hooks" ref: new-architecture - resolved-ref: "64d661779a88c845e5280f3da99a2bd90bdc586b" + resolved-ref: "0c819d4e11572d592b5334280b8b4f2657f21459" url: "https://github.com/KRTirtho/fl-query.git" source: git version: "0.3.1" @@ -1730,10 +1730,10 @@ packages: dependency: "direct main" description: name: window_manager - sha256: "5a7bda81b8b8e5feb733d07f4bd174e64dfb412031aff5314a375118e42b498e" + sha256: "492806c69879f0d28e95472bbe5e8d5940ac8c6e99cc07052fe14946974555ba" url: "https://pub.dev" source: hosted - version: "0.2.8" + version: "0.3.1" window_size: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index b8cf89289..3ee98abc8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -85,7 +85,7 @@ dependencies: uuid: ^3.0.7 version: ^3.0.2 visibility_detector: ^0.3.3 - window_manager: 0.2.8 + window_manager: ^0.3.1 window_size: git: url: https://github.com/google/flutter-desktop-embedding.git