From d85867a245deb6820977cd36e15ca9b1a4d4f27b Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 24 Feb 2023 13:56:05 +0600 Subject: [PATCH] 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 | 140 ++++------- .../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 | 234 +++++++++--------- 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, 869 insertions(+), 818 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 5fec09f8..5fbed70f 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 7a1a40a1..8e944c61 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 91fe2206..9d22ae34 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 5e3a3139..0f4ed279 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 db58ddd7..8cef3840 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 8c8d95d9..c9dd5672 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 2ff691cd..7ab0020d 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 acb16fc4..b44180e4 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 c12bb5e6..9dad8bc3 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 4f5dff8c..8cc854b5 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 dbb1f4a6..06ed49ba 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 49428c27..42bb117b 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; + final toggleTrackLike = Mutations.track.useToggleFavorite( + ref, + track.id!, + onMutate: (variables) { + return variables; }, - onData: (payload, variables, _) { + onError: (payload, isLiked) { if (!mounted()) return; - savedTracks.refetch(); - }, - onError: (payload, variables, queryContext) { - 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 isLikedQuery = Queries.playlist.useDoesUserFollowQuery( + ref, + playlist.id!, + me.data!.id!, ); - final job = - Queries.playlist.doesUserFollow("${playlist.id}:${me.data?.id}"); - final isLikedQuery = useQuery( - job: job, - externalData: ref.watch(spotifyProvider), - ); - - 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 3be7773a..a0e25a54 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 615d453b..671549c2 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 00000000..0507a9f8 --- /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 00000000..7dd9d84e --- /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 00000000..700698d6 --- /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 674aa6db..255a40ac 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 14d1bb91..569184c8 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 78894945..b1a372f5 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 faeec721..b682d42c 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 d54274ea..810dd00d 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 f915b01d..5f23c126 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 96f84880..670c663d 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()), - ], - ), - 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; - } - }, - ), + 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, + ), + ), + 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()), + ], + ), + 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 06c6244d..02f41e80 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 c93a1aa3..3441ac66 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 b5c3ba39..10b738cf 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 5cbc59b3..06f3af02 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 48ff64eb..90a33f76 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 e0b9e7ec..3ef4d3b2 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 866f8b65..c4bfacb1 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 6823fd43..003646fb 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 ea3ce119..ffecc137 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 2a2a4255..21876ff0 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 29485598..2449ed9e 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 f8f3c3c0..cb3b7f70 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 d4594061..992e73fc 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 f8b92628..b8cf8928 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 13b352c5..24522238 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 dfab40f7..b8a3a01b 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