diff --git a/lib/collections/routes.dart b/lib/collections/routes.dart index 5fec09f8d..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'; @@ -16,7 +17,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'; diff --git a/lib/components/album/album_card.dart b/lib/components/album/album_card.dart index 7a1a40a18..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'; @@ -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 queryClient = useQueryClient(); + final query = queryClient + .getQuery, dynamic>("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( @@ -99,10 +100,14 @@ class AlbumCard extends HookConsumerWidget { updating.value = true; try { final fetchedTracks = - await queryBowl.fetchQuery, SpotifyApi>( - Queries.album.tracksOf(album.id!), - externalData: ref.read(spotifyProvider), - key: ValueKey(const Uuid().v4()), + await queryClient.fetchQuery, SpotifyApi>( + "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..b12dd544d 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 = useQueries.artist.albumsOf(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..5b2bbf9cf 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'; @@ -9,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 { @@ -24,18 +22,14 @@ class CategoryCard extends HookConsumerWidget { @override 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 = useQueries.category.playlistsOf( + ref, + category.id!, ); - final hasNextPage = playlistQuery.pages.isEmpty - ? false - : (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 +43,7 @@ class CategoryCard extends HookConsumerWidget { ], ), ), - playlistQuery.hasError + playlistQuery.hasPageError && !playlistQuery.hasPageData ? PlatformText( "Something Went Wrong\n${playlistQuery.errors.first}") : SizedBox( @@ -67,7 +61,7 @@ class CategoryCard extends HookConsumerWidget { child: Waypoint( controller: scrollController, onTouchEdge: () { - playlistQuery.fetchNextPage(); + playlistQuery.fetchNext(); }, child: ListView( scrollDirection: Axis.horizontal, @@ -76,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/library/user_albums.dart b/lib/components/library/user_albums.dart index db58ddd72..ebcaa9dc5 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 = useQueries.album.ofMine(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..a522b7f42 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 = useQueries.artist.followedByMe(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..d618b3e10 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,22 +33,23 @@ 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 = 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( () { @@ -90,7 +89,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..4065e714d 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 query = queryBowl.getQuery, SpotifyApi>( - Queries.playlist.tracksOf(playlist.id!).queryKey, + final queryBowl = QueryClient.of(context); + final query = queryBowl.getQuery, dynamic>( + "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}", + () => useQueries.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}", + () => useQueries.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/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/root/sidebar.dart b/lib/components/root/sidebar.dart index 4f5dff8ca..e8197b769 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'; @@ -12,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'; @@ -195,11 +193,7 @@ class SidebarFooter extends HookConsumerWidget { width: 256, child: HookBuilder( builder: (context) { - var spotify = ref.watch(spotifyProvider); - final me = useQuery( - job: Queries.user.me, - externalData: spotify, - ); + final me = useQueries.user.me(ref); final data = me.data; final avatarImg = TypeConversionUtils.image_X_UrlString( @@ -208,20 +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.setExternalData(spotify); - me.refetch(); - } - return null; - }, [auth, me.hasError]); - - /// =================================== - return Padding( padding: const EdgeInsets.all(16).copyWith(left: 0), child: Row( diff --git a/lib/components/shared/dialogs/playlist_add_track_dialog.dart b/lib/components/shared/dialogs/playlist_add_track_dialog.dart index dbb1f4a6a..9a8d20034 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 = 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 49428c27c..5c7e6811a 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 = useQueries.user.me(ref); - final savedTracks = useQuery( - job: Queries.playlist.tracksOf("user-liked-tracks"), - externalData: ref.watch(spotifyProvider), - ); + final savedTracks = + useQueries.playlist.tracksOfQuery(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 = useMutations.track.toggleFavorite( + 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 = + useQueries.playlist.tracksOfQuery(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 = useQueries.user.me(ref); - final job = - Queries.playlist.doesUserFollow("${playlist.id}:${me.data?.id}"); - final isLikedQuery = useQuery( - job: job, - externalData: ref.watch(spotifyProvider), + final isLikedQuery = useQueries.playlist.doesUserFollow( + 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 = useMutations.playlist.toggleFavorite( + 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 = useQueries.user.me(ref); - final albumIsSaved = useQuery( - job: Queries.album.isSavedForMe(album.id!), - externalData: spotify, - ); + final albumIsSaved = useQueries.album.isSavedForMe(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 = useMutations.album.toggleFavorite( + 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..f82bb9f1d 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 = useMutations.playlist.removeTrackOf( + 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/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/hooks/use_spotify_infinite_query.dart b/lib/hooks/use_spotify_infinite_query.dart new file mode 100644 index 000000000..d56d3ca7a --- /dev/null +++ b/lib/hooks/use_spotify_infinite_query.dart @@ -0,0 +1,53 @@ +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(() { + return ref.listenManual( + spotifyProvider, + (previous, next) { + if (previous != next) { + query.refreshAll(); + } + }, + ).close; + }, [query]); + + 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..209f3391a --- /dev/null +++ b/lib/hooks/use_spotify_query.dart @@ -0,0 +1,52 @@ +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(() { + return ref.listenManual( + spotifyProvider, + (previous, next) { + if (previous != next) { + query.refresh(); + } + }, + ).close; + }, [query]); + + 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..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,12 +45,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 = 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 788949455..d939fedf6 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 = useQueries.artist.get(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 = useQueries.artist + .doIFollow(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 = useQueries.artist.topTracksOf( + ref, + artistId, ); final isPlaylistPlaying = @@ -391,9 +378,9 @@ class ArtistPage extends HookConsumerWidget { const SizedBox(height: 10), HookBuilder( builder: (context) { - final relatedArtists = useQuery( - job: Queries.artist.relatedArtistsOf(artistId), - externalData: spotify, + final relatedArtists = useQueries.artist.relatedArtistsOf( + ref, + artistId, ); if (relatedArtists.isLoading || !relatedArtists.hasData) { diff --git a/lib/pages/home/genres.dart b/lib/pages/home/genres.dart index faeec721a..3b997e10a 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,103 +19,79 @@ 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 = useQueries.category.list(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]); + 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...", + ); - 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; + final list = RefreshIndicator( + onRefresh: () async { + await categoriesQuery.refreshAll(); + }, + child: Waypoint( + onTouchEdge: () async { + if (categoriesQuery.hasNextPage && isMounted()) { + await categoriesQuery.fetchNext(); } - 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 list = RefreshIndicator( - onRefresh: () async { - await categoriesQuery.refetchPages(); }, - child: Waypoint( - onTouchEdge: () async { - if (categoriesQuery.hasNextPage && isMounted()) { - await categoriesQuery.fetchNextPage(); + 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 d54274eaa..a502103df 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 = useQueries.playlist.featured(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 = useQueries.album.newReleases(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..b158a13b1 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 = useQueries.lyrics.static( + 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..d70813999 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 = useQueries.lyrics.synced(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..82f64dae1 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 = 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..1c4b247b1 100644 --- a/lib/pages/search/search.dart +++ b/lib/pages/search/search.dart @@ -1,4 +1,5 @@ -import 'package:fl_query_hooks/fl_query_hooks.dart'; +import 'dart:async'; + import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart' hide Page; import 'package:flutter_hooks/flutter_hooks.dart'; @@ -17,14 +18,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,49 +36,29 @@ 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: Queries.search.get(SearchType.track.key), - externalData: Tuple2("", spotify), - ); - final searchAlbum = useInfiniteQuery( - job: Queries.search.get(SearchType.album.key), - externalData: Tuple2("", spotify), - ); - final searchPlaylist = useInfiniteQuery( - job: Queries.search.get(SearchType.playlist.key), - externalData: Tuple2("", spotify), - ); - final searchArtist = useInfiniteQuery( - job: Queries.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 [ - searchTrack, - searchAlbum, - searchPlaylist, - searchArtist, - ]) { - query.enabled = false; - query.fetched = false; - query.setExternalData(getVariables()); - query.refetchPages(); - } + Future onSearch() async { + await Future.wait([ + searchTrack.refreshAll(), + searchAlbum.refreshAll(), + searchPlaylist.refreshAll(), + searchArtist.refreshAll(), + ]); } return SafeArea( @@ -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: @@ -107,8 +82,14 @@ class SearchPage extends HookConsumerWidget { other: null, ).resolve(platform!), placeholder: "Search...", - onSubmitted: (value) { - onSearch(); + onSubmitted: (value) async { + ref.read(searchTermStateProvider.notifier).state = + value; + // Fl-Query is too fast, so we need to delay the search + // to prevent spamming the API :) + Timer(const Duration(milliseconds: 50), () { + onSearch(); + }); }, ), ), @@ -127,7 +108,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 +134,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 +183,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 +210,7 @@ class SearchPage extends HookConsumerWidget { controller: playlistController, child: Waypoint( onTouchEdge: () { - searchPlaylist.fetchNextPage(); + searchPlaylist.fetchNext(); }, controller: playlistController, child: SingleChildScrollView( @@ -256,13 +235,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 +260,7 @@ class SearchPage extends HookConsumerWidget { child: Waypoint( controller: artistController, onTouchEdge: () { - searchArtist.fetchNextPage(); + searchArtist.fetchNext(); }, child: SingleChildScrollView( scrollDirection: Axis.horizontal, @@ -311,13 +288,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 +313,7 @@ class SearchPage extends HookConsumerWidget { child: Waypoint( controller: albumController, onTouchEdge: () { - searchAlbum.fetchNextPage(); + searchAlbum.fetchNext(); }, child: SingleChildScrollView( scrollDirection: Axis.horizontal, @@ -364,14 +339,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/mutations/album.dart b/lib/services/mutations/album.dart index c93a1aa3b..920e11c24 100644 --- a/lib/services/mutations/album.dart +++ b/lib/services/mutations/album.dart @@ -1,22 +1,27 @@ 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; + const AlbumMutations(); - if (isLiked) { - await spotify.me.removeAlbums([albumId]); - } else { - await spotify.me.saveAlbums([albumId]); - } - return !isLiked; - }, - ); + Mutation toggleFavorite( + 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/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 b5c3ba396..106f4cdcf 100644 --- a/lib/services/mutations/playlist.dart +++ b/lib/services/mutations/playlist.dart @@ -1,35 +1,42 @@ 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; + const PlaylistMutations(); - if (isLiked) { - await spotify.playlists.unfollowPlaylist(playlistId); - } else { - await spotify.playlists.followPlaylist(playlistId); - } - return !isLiked; - }, - ); + Mutation toggleFavorite( + 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, + ); + } - 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 removeTrackOf( + 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..2245c497a 100644 --- a/lib/services/mutations/track.dart +++ b/lib/services/mutations/track.dart @@ -1,22 +1,32 @@ 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; + const TrackMutations(); - if (isLiked) { - await spotify.tracks.me.removeOne(trackId); - } else { - await spotify.tracks.me.saveOne(trackId); - } - return !isLiked; - }, - ); + Mutation toggleFavorite( + 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..629f732ad 100644 --- a/lib/services/queries/album.dart +++ b/lib/services/queries/album.dart @@ -1,50 +1,78 @@ 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 AlbumQueries { - final ofMine = QueryJob, SpotifyApi>( - queryKey: "current-user-albums", - task: (_, spotify) { - return spotify.me.savedAlbums().all(); - }, - ); + const AlbumQueries(); - 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> ofMine(WidgetRef ref) { + return useSpotifyQuery, dynamic>( + "current-user-albums", + (spotify) { + return spotify.me.savedAlbums().all(); + }, + ref: ref, + ); + } - final isSavedForMe = - QueryJob.withVariableKey(task: (queryKey, spotify) { - return spotify.me - .isSavedAlbums([getVariable(queryKey)]).then((value) => value.first); - }); + Query, dynamic> tracksOf( + WidgetRef ref, + String albumId, + ) { + return useSpotifyQuery, dynamic>( + "album-tracks/$albumId", + (spotify) { + return spotify.albums + .getTracks(albumId) + .all() + .then((value) => value.toList()); + }, + 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; - } - }, - ); + Query isSavedForMe( + WidgetRef ref, + String album, + ) { + return useSpotifyQuery( + "is-saved-for-me/$album", + (spotify) { + return spotify.me.isSavedAlbums([album]).then((value) => value.first); + }, + ref: ref, + ); + } + + InfiniteQuery, dynamic, int> newReleases(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: (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 e0b9e7ec7..fed55429e 100644 --- a/lib/services/queries/artist.dart +++ b/lib/services/queries/artist.dart @@ -1,59 +1,103 @@ 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)), - ); + const ArtistQueries(); - 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); - }, - ); + Query get( + WidgetRef ref, + String artist, + ) { + return useSpotifyQuery( + "artist-profile/$artist", + (spotify) => spotify.artists.get(artist), + 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; - }, - ); + InfiniteQuery, dynamic, String> followedByMe( + WidgetRef ref) { + return useSpotifyInfiniteQuery, dynamic, String>( + "user-following-artists", + (pageParam, spotify) async { + return spotify.me + .following(FollowingType.artist) + .getPage(15, pageParam); + }, + initialPage: "", + nextPage: (lastPage, lastPageData) { + if (lastPageData.isLast || (lastPageData.items ?? []).length < 15) { + return null; + } + return lastPageData.after; + }, + ref: ref, + ); + } - final topTracksOf = QueryJob.withVariableKey, SpotifyApi>( - preQueryKey: "artist-top-track-query", - task: (queryKey, spotify) { - return spotify.artists.getTopTracks(getVariable(queryKey), "US"); - }, - ); + Query doIFollow( + 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 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); - }, - ); + Query, dynamic> topTracksOf( + WidgetRef ref, + String artist, + ) { + return useSpotifyQuery, dynamic>( + "artist-top-track-query/$artist", + (spotify) { + return spotify.artists.getTopTracks(artist, "US"); + }, + ref: ref, + ); + } - final relatedArtistsOf = - QueryJob.withVariableKey, SpotifyApi>( - preQueryKey: "artist-related-artist-query", - task: (queryKey, spotify) { - return spotify.artists.getRelatedArtists(getVariable(queryKey)); - }, - ); + InfiniteQuery, dynamic, int> albumsOf( + 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, lastPageData) { + if (lastPageData.isLast || (lastPageData.items ?? []).length < 5) { + return null; + } + return lastPageData.nextOffset; + }, + ref: ref, + ); + } + + Query, dynamic> relatedArtistsOf( + 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..c4e8d188d 100644 --- a/lib/services/queries/category.dart +++ b/lib/services/queries/category.dart @@ -1,33 +1,65 @@ 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 { - 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); + const CategoryQueries(); - return categories; - }, - ); + InfiniteQuery, dynamic, int> list( + WidgetRef ref, String recommendationMarket) { + return useSpotifyInfiniteQuery, dynamic, int>( + "category-playlists", + (pageParam, spotify) async { + final categories = await spotify.categories + .list(country: recommendationMarket) + .getPage(15, pageParam); - 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); - }, - ); + return categories; + }, + initialPage: 0, + nextPage: (lastPage, lastPageData) { + if (lastPageData.isLast || (lastPageData.items ?? []).length < 15) { + return null; + } + return lastPageData.nextOffset; + }, + jsonConfig: JsonConfig>( + toJson: (page) => page.toJson(), + fromJson: (json) => PageJson.fromJson( + json, + (json) { + return Category.fromJson((json as Map).castKeyDeep()); + }, + ), + ), + ref: ref, + ); + } + + InfiniteQuery, dynamic, int> playlistsOf( + 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, lastPageData) { + if (lastPageData.isLast || (lastPageData.items ?? []).length < 5) { + return null; + } + return lastPageData.nextOffset; + }, + ref: ref, + ); + } } diff --git a/lib/services/queries/lyrics.dart b/lib/services/queries/lyrics.dart index 6823fd430..17bca7899 100644 --- a/lib/services/queries/lyrics.dart +++ b/lib/services/queries/lyrics.dart @@ -1,44 +1,51 @@ 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, - ); + const LyricsQueries(); - if (lyrics == null) throw Exception("Unable find lyrics"); - return lyrics; - }, - ); + Query static( + 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, + ); - final synced = QueryJob.withVariableKey( - preQueryKey: "synced-lyrics", - task: (queryKey, currentTrack) async { - if (currentTrack == null || getVariable(queryKey).isEmpty) { - throw "No track currently"; - } + if (lyrics == null) throw Exception("Unable find lyrics"); + return lyrics; + }, + ); + } - final timedLyrics = await ServiceUtils.getTimedLyrics(currentTrack); - if (timedLyrics == null) throw Exception("Unable to find lyrics"); + Query synced( + 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"); - return timedLyrics; - }, - ); + return timedLyrics; + }, + ); + } } diff --git a/lib/services/queries/playlist.dart b/lib/services/queries/playlist.dart index ea3ce119b..06f8d952b 100644 --- a/lib/services/queries/playlist.dart +++ b/lib/services/queries/playlist.dart @@ -1,56 +1,97 @@ 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'; class PlaylistQueries { - final doesUserFollow = QueryJob.withVariableKey( - preQueryKey: "playlist-is-followed", - task: (queryKey, spotify) { - final idMap = getVariable(queryKey).split(":"); + const PlaylistQueries(); - return spotify.playlists.followedBy(idMap.first, [idMap.last]).then( - (value) => value.first, - ); - }, - ); + Query doesUserFollow( + 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, + ); + } - final ofMine = QueryJob, SpotifyApi>( - queryKey: "current-user-playlists", - task: (_, spotify) { - return spotify.playlists.me.all(); - }, - ); + Query, dynamic> ofMine(WidgetRef ref) { + return useSpotifyQuery, dynamic>( + "current-user-playlists", + (spotify) { + return spotify.playlists.me.all(); + }, + ref: ref, + ); + } - 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(), - ); - }, - ); + 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 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; - } - }, - ); + Query, dynamic> tracksOfQuery( + WidgetRef ref, + String playlistId, + ) { + 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, + ); + } + + InfiniteQuery, dynamic, int> featured( + 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, lastPageData) { + if (lastPageData.isLast || (lastPageData.items ?? []).length < 5) { + return null; + } + return lastPageData.nextOffset; + }, + ref: ref, + ); + } } 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 2a2a42558..7eb3e1392 100644 --- a/lib/services/queries/search.dart +++ b/lib/services/queries/search.dart @@ -1,28 +1,36 @@ 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); - }, - ); + const SearchQueries(); + InfiniteQuery, dynamic, int> query( + WidgetRef ref, + String query, + SearchType searchType, + ) { + return useSpotifyInfiniteQuery, dynamic, int>( + "search-query/${searchType.key}", + (page, spotify) { + if (query.trim().isEmpty) return []; + final queryString = query; + return spotify.search.get( + queryString, + types: [searchType], + ).getPage(10, page); + }, + enabled: false, + ref: ref, + initialPage: 0, + 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/lib/services/queries/user.dart b/lib/services/queries/user.dart index 29485598f..6f94264d2 100644 --- a/lib/services/queries/user.dart +++ b/lib/services/queries/user.dart @@ -1,25 +1,30 @@ 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; - }, - ); + const UserQueries(); + Query me(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..4c4d707f4 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: "0c819d4e11572d592b5334280b8b4f2657f21459" + 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: "0c819d4e11572d592b5334280b8b4f2657f21459" + 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: @@ -1776,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: @@ -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..3ee98abc8 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: @@ -77,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 @@ -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