From b3ac5ca3bbb6d5af154f4b5d715d1f19ca2f46e2 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 23 Oct 2022 16:06:03 +0600 Subject: [PATCH] fix(auth): refresh access token timer not working refactor: use fl_query for spotify API instead of riverpod --- lib/components/Album/AlbumView.dart | 48 +--- lib/components/Artist/ArtistProfile.dart | 160 +++++++----- lib/components/Home/Sidebar.dart | 21 +- lib/components/Lyrics/Lyrics.dart | 28 +- lib/components/Lyrics/SyncedLyrics.dart | 10 +- lib/components/Player/PlayerActions.dart | 37 +-- lib/components/Playlist/PlaylistView.dart | 71 +---- lib/components/Search/Search.dart | 45 +++- .../Shared/AdaptivePopupMenuButton.dart | 2 +- lib/components/Shared/HeartButton.dart | 222 +++++++++++++++- .../Shared/TrackCollectionView.dart | 42 ++- lib/components/Shared/TrackTile.dart | 41 +-- lib/provider/Auth.dart | 10 +- lib/provider/SpotifyRequests.dart | 243 ++++++++++-------- lib/utils/type_conversion_utils.dart | 1 - pubspec.lock | 4 +- pubspec.yaml | 4 +- 17 files changed, 615 insertions(+), 374 deletions(-) diff --git a/lib/components/Album/AlbumView.dart b/lib/components/Album/AlbumView.dart index 71d551f7c..4397e55e0 100644 --- a/lib/components/Album/AlbumView.dart +++ b/lib/components/Album/AlbumView.dart @@ -1,4 +1,5 @@ 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'; @@ -57,9 +58,14 @@ class AlbumView extends HookConsumerWidget { final SpotifyApi spotify = ref.watch(spotifyProvider); final Auth auth = ref.watch(authProvider); - final tracksSnapshot = ref.watch(albumTracksQuery(album.id!)); - final albumSavedSnapshot = - ref.watch(albumIsSavedForCurrentUserQuery(album.id!)); + final tracksSnapshot = useQuery( + job: albumTracksQueryJob(album.id!), + externalData: spotify, + ); + final albumSavedSnapshot = useQuery( + job: albumIsSavedForCurrentUserQueryJob(album.id!), + externalData: spotify, + ); final albumArt = useMemoized( () => TypeConversionUtils.image_X_UrlString( @@ -82,11 +88,11 @@ class AlbumView extends HookConsumerWidget { routePath: "/album/${album.id}", bottomSpace: breakpoint.isLessThanOrEqualTo(Breakpoints.md), onPlay: ([track]) { - if (tracksSnapshot.asData?.value != null) { + if (tracksSnapshot.hasData) { if (!isAlbumPlaying) { playPlaylist( playback, - tracksSnapshot.asData!.value + tracksSnapshot.data! .map((track) => TypeConversionUtils.simpleTrack_X_Track(track, album)) .toList(), @@ -95,7 +101,7 @@ class AlbumView extends HookConsumerWidget { } else if (isAlbumPlaying && track != null) { playPlaylist( playback, - tracksSnapshot.asData!.value + tracksSnapshot.data! .map((track) => TypeConversionUtils.simpleTrack_X_Track(track, album)) .toList(), @@ -112,35 +118,7 @@ class AlbumView extends HookConsumerWidget { ClipboardData(text: "https://open.spotify.com/album/${album.id}"), ); }, - heartBtn: auth.isLoggedIn - ? albumSavedSnapshot.when( - data: (isSaved) { - return HeartButton( - isLiked: isSaved, - onPressed: () { - (isSaved - ? spotify.me.removeAlbums( - [album.id!], - ) - : spotify.me.saveAlbums( - [album.id!], - )) - .whenComplete(() { - ref.refresh( - albumIsSavedForCurrentUserQuery( - album.id!, - ), - ); - QueryBowl.of(context).refetchQueries( - [currentUserAlbumsQueryJob.queryKey], - ); - }); - }, - ); - }, - error: (error, _) => Text("Error $error"), - loading: () => const CircularProgressIndicator()) - : null, + heartBtn: AlbumHeartButton(album: album), ); } } diff --git a/lib/components/Artist/ArtistProfile.dart b/lib/components/Artist/ArtistProfile.dart index 0830a5088..266137fed 100644 --- a/lib/components/Artist/ArtistProfile.dart +++ b/lib/components/Artist/ArtistProfile.dart @@ -1,3 +1,5 @@ +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'; @@ -49,20 +51,28 @@ class ArtistProfile extends HookConsumerWidget { final Playback playback = ref.watch(playbackProvider); - final artistsSnapshot = ref.watch(artistProfileQuery(artistId)); - final isFollowingSnapshot = - ref.watch(currentUserFollowsArtistQuery(artistId)); - final topTracksSnapshot = ref.watch(artistTopTracksQuery(artistId)); - - final relatedArtists = ref.watch(artistRelatedArtistsQuery(artistId)); - return SafeArea( child: Scaffold( appBar: const PageWindowTitleBar( leading: BackButton(), ), - body: artistsSnapshot.when( - data: (data) { + body: HookBuilder( + builder: (context) { + final artistsQuery = useQuery( + job: artistProfileQueryJob(artistId), + externalData: spotify, + ); + + if (artistsQuery.isLoading || !artistsQuery.hasData) { + return const ShimmerArtistProfile(); + } else if (artistsQuery.hasError) { + return Center( + child: Text(artistsQuery.error.toString()), + ); + } + + final data = artistsQuery.data!; + return SingleChildScrollView( controller: parentScrollController, padding: const EdgeInsets.all(20), @@ -115,42 +125,57 @@ class ArtistProfile extends HookConsumerWidget { Row( mainAxisSize: MainAxisSize.min, children: [ - isFollowingSnapshot.when( - data: (isFollowing) { - return OutlinedButton( - onPressed: () async { - try { - isFollowing - ? await spotify.me.unfollow( - FollowingType.artist, - [artistId], - ) - : await spotify.me.follow( - FollowingType.artist, - [artistId], - ); - } catch (e, stack) { - logger.e( - "FollowButton.onPressed", - e, - stack, - ); - } finally { - ref.refresh( - currentUserFollowsArtistQuery( - artistId), - ); - } - }, - child: Text( - isFollowing ? "Following" : "Follow", - ), + HookBuilder( + builder: (context) { + final isFollowingQuery = useQuery( + job: currentUserFollowsArtistQueryJob( + artistId), + externalData: spotify, + ); + + if (isFollowingQuery.isLoading || + !isFollowingQuery.hasData) { + return const SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator(), ); - }, - error: (error, stackTrace) => Container(), - loading: () => - const CircularProgressIndicator - .adaptive()), + } + + return OutlinedButton( + onPressed: () async { + try { + isFollowingQuery.data! + ? await spotify.me.unfollow( + FollowingType.artist, + [artistId], + ) + : await spotify.me.follow( + FollowingType.artist, + [artistId], + ); + } catch (e, stack) { + logger.e( + "FollowButton.onPressed", + e, + stack, + ); + } finally { + QueryBowl.of(context).refetchQueries([ + currentUserFollowsArtistQueryJob( + artistId) + .queryKey, + ]); + } + }, + child: Text( + isFollowingQuery.data! + ? "Following" + : "Follow", + ), + ); + }, + ), IconButton( icon: const Icon(Icons.share_rounded), onPressed: () { @@ -180,8 +205,23 @@ class ArtistProfile extends HookConsumerWidget { ], ), const SizedBox(height: 50), - topTracksSnapshot.when( - data: (topTracks) { + HookBuilder( + builder: (context) { + final topTracksQuery = useQuery( + job: artistTopTracksQueryJob(artistId), + externalData: spotify, + ); + + if (topTracksQuery.isLoading || !topTracksQuery.hasData) { + return const CircularProgressIndicator.adaptive(); + } else if (topTracksQuery.hasError) { + return Center( + child: Text(topTracksQuery.error.toString()), + ); + } + + final topTracks = topTracksQuery.data!; + final isPlaylistPlaying = playback.playlist?.id == data.id; playPlaylist(List tracks, @@ -248,10 +288,6 @@ class ArtistProfile extends HookConsumerWidget { }), ]); }, - error: (error, stack) => - Text("Failed to find top tracks $error"), - loading: () => const Center( - child: CircularProgressIndicator.adaptive()), ), const SizedBox(height: 50), Text( @@ -266,28 +302,36 @@ class ArtistProfile extends HookConsumerWidget { style: Theme.of(context).textTheme.headline4, ), const SizedBox(height: 10), - relatedArtists.when( - data: (artists) { + HookBuilder( + builder: (context) { + final relatedArtists = useQuery( + job: artistRelatedArtistsQueryJob(artistId), + externalData: spotify, + ); + + if (relatedArtists.isLoading || !relatedArtists.hasData) { + return const CircularProgressIndicator.adaptive(); + } else if (relatedArtists.hasError) { + return Center( + child: Text(relatedArtists.error.toString()), + ); + } + return Center( child: Wrap( spacing: 20, runSpacing: 20, - children: artists + children: relatedArtists.data! .map((artist) => ArtistCard(artist)) .toList(), ), ); }, - error: (error, stackTrack) => - Text("Failed to get Artist albums $error"), - loading: () => const CircularProgressIndicator.adaptive(), ), ], ), ); }, - error: (_, __) => const Text("Life's miserable"), - loading: () => const ShimmerArtistProfile(), ), ), ); diff --git a/lib/components/Home/Sidebar.dart b/lib/components/Home/Sidebar.dart index b045af9af..5903d44d3 100644 --- a/lib/components/Home/Sidebar.dart +++ b/lib/components/Home/Sidebar.dart @@ -1,5 +1,6 @@ import 'package:badges/badges.dart'; import 'package:bitsdojo_window/bitsdojo_window.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'; @@ -9,6 +10,7 @@ import 'package:spotube/hooks/useBreakpoints.dart'; import 'package:spotube/models/sideBarTiles.dart'; import 'package:spotube/provider/Auth.dart'; import 'package:spotube/provider/Downloader.dart'; +import 'package:spotube/provider/SpotifyDI.dart'; import 'package:spotube/provider/SpotifyRequests.dart'; import 'package:spotube/provider/UserPreferences.dart'; import 'package:spotube/utils/platform.dart'; @@ -42,7 +44,7 @@ class Sidebar extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final breakpoints = useBreakpoints(); final extended = useState(false); - final meSnapshot = ref.watch(currentUserQuery); + final auth = ref.watch(authProvider); final downloadCount = ref.watch( downloaderProvider.select((s) => s.currentlyRunning), @@ -161,15 +163,28 @@ class Sidebar extends HookConsumerWidget { ), SizedBox( width: extended.value ? 256 : 80, - child: Builder( + child: HookBuilder( builder: (context) { - final data = meSnapshot.asData?.value; + final me = useQuery( + job: currentUserQueryJob, + externalData: ref.watch(spotifyProvider), + ); + final data = me.data; final avatarImg = TypeConversionUtils.image_X_UrlString( data?.images, index: (data?.images?.length ?? 1) - 1, placeholder: ImagePlaceholder.artist, ); + + useEffect(() { + if (auth.isLoggedIn && !me.hasData) { + me.setExternalData(ref.read(spotifyProvider)); + me.refetch(); + } + return; + }, [auth.isLoggedIn, me.hasData]); + if (extended.value) { return Padding( padding: const EdgeInsets.all(16).copyWith(left: 0), diff --git a/lib/components/Lyrics/Lyrics.dart b/lib/components/Lyrics/Lyrics.dart index a699592ac..aebe37ece 100644 --- a/lib/components/Lyrics/Lyrics.dart +++ b/lib/components/Lyrics/Lyrics.dart @@ -1,3 +1,4 @@ +import 'package:fl_query_hooks/fl_query_hooks.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; @@ -5,7 +6,9 @@ import 'package:spotube/components/LoaderShimmers/ShimmerLyrics.dart'; import 'package:spotube/hooks/useBreakpoints.dart'; import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/SpotifyRequests.dart'; +import 'package:spotube/provider/UserPreferences.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; +import 'package:tuple/tuple.dart'; class Lyrics extends HookConsumerWidget { final Color? titleBarForegroundColor; @@ -17,7 +20,13 @@ class Lyrics extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { Playback playback = ref.watch(playbackProvider); - final geniusLyricsSnapshot = ref.watch(geniusLyricsQuery); + final geniusLyricsQuery = useQuery( + job: geniusLyricsQueryJob, + externalData: Tuple2( + playback.track, + ref.watch(userPreferencesProvider).geniusAccessToken, + ), + ); final breakpoint = useBreakpoints(); final textTheme = Theme.of(context).textTheme; @@ -46,8 +55,18 @@ class Lyrics extends HookConsumerWidget { child: Center( child: Padding( padding: const EdgeInsets.all(8.0), - child: geniusLyricsSnapshot.when( - data: (lyrics) { + child: Builder( + builder: (context) { + if (geniusLyricsQuery.isLoading) { + return const ShimmerLyrics(); + } else if (geniusLyricsQuery.hasError) { + return Text( + "Sorry, no Lyrics were found for `${playback.track?.name}` :'(\n${geniusLyricsQuery.error.toString()}", + ); + } + + final lyrics = geniusLyricsQuery.data; + return Text( lyrics == null && playback.track == null ? "No Track being played currently" @@ -56,9 +75,6 @@ class Lyrics extends HookConsumerWidget { ?.copyWith(color: textTheme.headline1?.color), ); }, - error: (error, __) => Text( - "Sorry, no Lyrics were found for `${playback.track?.name}` :'("), - loading: () => const ShimmerLyrics(), ), ), ), diff --git a/lib/components/Lyrics/SyncedLyrics.dart b/lib/components/Lyrics/SyncedLyrics.dart index 2b774b9fc..707bc2f98 100644 --- a/lib/components/Lyrics/SyncedLyrics.dart +++ b/lib/components/Lyrics/SyncedLyrics.dart @@ -1,5 +1,6 @@ import 'dart:ui'; +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'; @@ -30,14 +31,17 @@ class SyncedLyrics extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { - final timedLyricsSnapshot = ref.watch(rentanadviserLyricsQuery); + Playback playback = ref.watch(playbackProvider); + final timedLyricsQuery = useQuery( + job: rentanadviserLyricsQueryJob, + externalData: playback.track, + ); final lyricDelay = ref.watch(lyricDelayState); - Playback playback = ref.watch(playbackProvider); final breakpoint = useBreakpoints(); final controller = useAutoScrollController(); final failed = useState(false); - final lyricValue = timedLyricsSnapshot.asData?.value; + final lyricValue = timedLyricsQuery.data; final lyricsMap = useMemoized( () => lyricValue?.lyrics diff --git a/lib/components/Player/PlayerActions.dart b/lib/components/Player/PlayerActions.dart index b8e76b3d7..deec48c14 100644 --- a/lib/components/Player/PlayerActions.dart +++ b/lib/components/Player/PlayerActions.dart @@ -6,13 +6,9 @@ import 'package:spotify/spotify.dart'; import 'package:spotube/components/Library/UserLocalTracks.dart'; import 'package:spotube/components/Player/PlayerQueue.dart'; import 'package:spotube/components/Shared/HeartButton.dart'; -import 'package:spotube/hooks/useForceUpdate.dart'; import 'package:spotube/models/Logger.dart'; -import 'package:spotube/provider/Auth.dart'; import 'package:spotube/provider/Downloader.dart'; import 'package:spotube/provider/Playback.dart'; -import 'package:spotube/provider/SpotifyDI.dart'; -import 'package:spotube/provider/SpotifyRequests.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; class PlayerActions extends HookConsumerWidget { @@ -27,11 +23,8 @@ class PlayerActions extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { - final SpotifyApi spotifyApi = ref.watch(spotifyProvider); final Playback playback = ref.watch(playbackProvider); - final Auth auth = ref.watch(authProvider); final downloader = ref.watch(downloaderProvider); - final update = useForceUpdate(); final isInQueue = downloader.inQueue.any((element) => element.id == playback.track?.id); final localTracks = ref.watch(localTracksProvider).value; @@ -96,35 +89,7 @@ class PlayerActions extends HookConsumerWidget { ? () => downloader.addToQueue(playback.track!) : null, ), - if (auth.isLoggedIn) - FutureBuilder( - future: playback.track?.id != null - ? spotifyApi.tracks.me.containsOne(playback.track!.id!) - : Future.value(false), - initialData: false, - builder: (context, snapshot) { - bool isLiked = snapshot.data ?? false; - return HeartButton( - isLiked: isLiked, - onPressed: () async { - try { - if (playback.track?.id == null) return; - isLiked - ? await spotifyApi.tracks.me - .removeOne(playback.track!.id!) - : await spotifyApi.tracks.me - .saveOne(playback.track!.id!); - } catch (e, stack) { - logger.e("FavoriteButton.onPressed", e, stack); - } finally { - update(); - ref.refresh(currentUserSavedTracksQuery); - ref.refresh( - playlistTracksQuery("user-liked-tracks"), - ); - } - }); - }), + if (playback.track != null) TrackHeartButton(track: playback.track!), ], ); } diff --git a/lib/components/Playlist/PlaylistView.dart b/lib/components/Playlist/PlaylistView.dart index f66223e06..fdf460515 100644 --- a/lib/components/Playlist/PlaylistView.dart +++ b/lib/components/Playlist/PlaylistView.dart @@ -1,6 +1,4 @@ -import 'dart:convert'; - -import 'package:fl_query/fl_query.dart'; +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'; @@ -8,10 +6,8 @@ import 'package:spotube/components/Shared/HeartButton.dart'; import 'package:spotube/components/Shared/TrackCollectionView.dart'; import 'package:spotube/components/Shared/TracksTableView.dart'; import 'package:spotube/hooks/useBreakpoints.dart'; -import 'package:spotube/hooks/usePaletteColor.dart'; import 'package:spotube/models/CurrentPlaylist.dart'; import 'package:spotube/models/Logger.dart'; -import 'package:spotube/provider/Auth.dart'; import 'package:spotube/provider/Playback.dart'; import 'package:flutter/material.dart'; import 'package:spotify/spotify.dart'; @@ -59,15 +55,18 @@ class PlaylistView extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { Playback playback = ref.watch(playbackProvider); - final Auth auth = ref.watch(authProvider); SpotifyApi spotify = ref.watch(spotifyProvider); final isPlaylistPlaying = playback.playlist?.id != null && playback.playlist?.id == playlist.id; final breakpoint = useBreakpoints(); - final meSnapshot = ref.watch(currentUserQuery); - final tracksSnapshot = ref.watch(playlistTracksQuery(playlist.id!)); + final meSnapshot = + useQuery(job: currentUserQueryJob, externalData: spotify); + final tracksSnapshot = useQuery( + job: playlistTracksQueryJob(playlist.id!), + externalData: spotify, + ); final titleImage = useMemoized( () => TypeConversionUtils.image_X_UrlString( @@ -76,11 +75,6 @@ class PlaylistView extends HookConsumerWidget { ), [playlist.images]); - final color = usePaletteGenerator( - context, - titleImage, - ).dominantColor; - return TrackCollectionView( id: playlist.id!, isPlaying: isPlaylistPlaying, @@ -89,15 +83,15 @@ class PlaylistView extends HookConsumerWidget { tracksSnapshot: tracksSnapshot, description: playlist.description, isOwned: playlist.owner?.id != null && - playlist.owner!.id == meSnapshot.asData?.value.id, + playlist.owner!.id == meSnapshot.data?.id, onPlay: ([track]) { - if (tracksSnapshot.asData?.value != null) { + if (tracksSnapshot.hasData) { if (!isPlaylistPlaying) { - playPlaylist(playback, tracksSnapshot.asData!.value, ref); + playPlaylist(playback, tracksSnapshot.data!, ref); } else if (isPlaylistPlaying && track != null) { playPlaylist( playback, - tracksSnapshot.asData!.value, + tracksSnapshot.data!, ref, currentTrack: track, ); @@ -126,48 +120,7 @@ class PlaylistView extends HookConsumerWidget { ); }); }, - heartBtn: (auth.isLoggedIn && playlist.id != "user-liked-tracks" - ? meSnapshot.when( - data: (me) { - final query = playlistIsFollowedQuery( - jsonEncode({"playlistId": playlist.id, "userId": me.id!})); - final followingSnapshot = ref.watch(query); - - return followingSnapshot.when( - data: (isFollowing) { - return HeartButton( - isLiked: isFollowing, - color: color?.titleTextColor, - icon: playlist.owner?.id != null && - me.id == playlist.owner?.id - ? Icons.delete_outline_rounded - : null, - onPressed: () async { - try { - isFollowing - ? await spotify.playlists - .unfollowPlaylist(playlist.id!) - : await spotify.playlists - .followPlaylist(playlist.id!); - } catch (e, stack) { - logger.e("FollowButton.onPressed", e, stack); - } finally { - ref.refresh(query); - QueryBowl.of(context).refetchQueries([ - currentUserPlaylistsQueryJob.queryKey, - ]); - } - }, - ); - }, - error: (error, _) => Text("Error $error"), - loading: () => const CircularProgressIndicator(), - ); - }, - error: (error, _) => Text("Error $error"), - loading: () => const CircularProgressIndicator(), - ) - : null), + heartBtn: PlaylistHeartButton(playlist: playlist), ); } } diff --git a/lib/components/Search/Search.dart b/lib/components/Search/Search.dart index f44a81c6c..fca9c28af 100644 --- a/lib/components/Search/Search.dart +++ b/lib/components/Search/Search.dart @@ -1,3 +1,4 @@ +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'; @@ -12,9 +13,11 @@ import 'package:spotube/hooks/useBreakpoints.dart'; import 'package:spotube/models/CurrentPlaylist.dart'; import 'package:spotube/provider/Auth.dart'; import 'package:spotube/provider/Playback.dart'; +import 'package:spotube/provider/SpotifyDI.dart'; import 'package:spotube/provider/SpotifyRequests.dart'; import 'package:spotube/utils/primitive_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; +import 'package:tuple/tuple.dart'; final searchTermStateProvider = StateProvider((ref) => ""); @@ -24,18 +27,26 @@ class Search extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { final Auth auth = ref.watch(authProvider); - final searchTerm = ref.watch(searchTermStateProvider); - final controller = - useTextEditingController(text: ref.read(searchTermStateProvider)); final albumController = useScrollController(); final playlistController = useScrollController(); final artistController = useScrollController(); final breakpoint = useBreakpoints(); + final searchMutation = useMutation( + job: searchMutationJob, + ); + if (auth.isAnonymous) { return const AnonymousFallback(); } - final searchSnapshot = ref.watch(searchQuery(searchTerm)); + + final getVariables = useCallback( + () => Tuple2( + ref.read(searchTermStateProvider), + ref.read(spotifyProvider), + ), + [], + ); return SafeArea( child: Material( @@ -49,14 +60,15 @@ class Search extends HookConsumerWidget { ), color: Theme.of(context).backgroundColor, child: TextField( - controller: controller, + onChanged: (value) { + ref.read(searchTermStateProvider.notifier).state = value; + }, decoration: InputDecoration( isDense: true, suffix: ElevatedButton( child: const Icon(Icons.search_rounded), onPressed: () { - ref.read(searchTermStateProvider.notifier).state = - controller.value.text; + searchMutation.mutate(getVariables()); }, ), contentPadding: const EdgeInsets.symmetric( @@ -67,19 +79,26 @@ class Search extends HookConsumerWidget { hintText: "Search...", ), onSubmitted: (value) { - ref.read(searchTermStateProvider.notifier).state = - controller.value.text; + searchMutation.mutate(getVariables()); }, ), ), - searchSnapshot.when( - data: (data) { + HookBuilder( + builder: (context) { + if (searchMutation.hasError && searchMutation.isError) { + return Text("Alas! Error=${searchMutation.error}"); + } + if (searchMutation.isLoading) { + return const CircularProgressIndicator(); + } + Playback playback = ref.watch(playbackProvider); List albums = []; List artists = []; List tracks = []; List playlists = []; - for (MapEntry page in data.asMap().entries) { + for (MapEntry page + in (searchMutation.data ?? []).asMap().entries) { for (var item in page.value.items ?? []) { if (item is AlbumSimple) { albums.add(item); @@ -241,8 +260,6 @@ class Search extends HookConsumerWidget { ), ); }, - error: (error, __) => Text("Error $error"), - loading: () => const CircularProgressIndicator(), ) ], ), diff --git a/lib/components/Shared/AdaptivePopupMenuButton.dart b/lib/components/Shared/AdaptivePopupMenuButton.dart index 8964dc1af..dfa8b3dcb 100644 --- a/lib/components/Shared/AdaptivePopupMenuButton.dart +++ b/lib/components/Shared/AdaptivePopupMenuButton.dart @@ -32,7 +32,7 @@ class Action extends StatelessWidget { } return TextButton.icon( style: TextButton.styleFrom( - primary: Theme.of(context).textTheme.bodyMedium?.color, + foregroundColor: Theme.of(context).textTheme.bodyMedium?.color, padding: const EdgeInsets.all(20), ), icon: icon, diff --git a/lib/components/Shared/HeartButton.dart b/lib/components/Shared/HeartButton.dart index 10ea3204c..1ab38c3bb 100644 --- a/lib/components/Shared/HeartButton.dart +++ b/lib/components/Shared/HeartButton.dart @@ -1,8 +1,19 @@ +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/hooks/usePaletteColor.dart'; +import 'package:spotube/provider/Auth.dart'; +import 'package:spotube/provider/SpotifyDI.dart'; +import 'package:spotube/provider/SpotifyRequests.dart'; +import 'package:spotube/utils/type_conversion_utils.dart'; +import 'package:tuple/tuple.dart'; -class HeartButton extends StatelessWidget { +class HeartButton extends ConsumerWidget { final bool isLiked; - final void Function() onPressed; + final void Function()? onPressed; final IconData? icon; final Color? color; const HeartButton({ @@ -14,16 +25,219 @@ class HeartButton extends StatelessWidget { }) : super(key: key); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, ref) { + final auth = ref.watch(authProvider); + + if (!auth.isLoggedIn) return Container(); + return IconButton( icon: Icon( icon ?? (!isLiked ? Icons.favorite_outline_rounded : Icons.favorite_rounded), - color: isLiked ? Theme.of(context).primaryColor : color, + color: isLiked ? Colors.pink : color, ), onPressed: onPressed, ); } } + +Tuple3>, Query> + useTrackToggleLike(Track track, WidgetRef ref) { + final me = useQuery( + job: currentUserQueryJob, externalData: ref.watch(spotifyProvider)); + + final savedTracks = useQuery( + job: playlistTracksQueryJob("user-liked-tracks"), + externalData: ref.watch(spotifyProvider), + ); + + final isLiked = + savedTracks.data?.map((track) => track.id).contains(track.id) ?? false; + + final mounted = useIsMounted(); + + final toggleTrackLike = useMutation>( + job: toggleFavoriteTrackMutationJob(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(); + }, + onError: (payload, variables, queryContext) { + if (!mounted()) return; + savedTracks.setQueryData( + (oldData) { + if (variables.item2) { + return [...(oldData ?? []), track]; + } + + return oldData + ?.where( + (element) => element.id != track.id, + ) + .toList() ?? + []; + }, + ); + }, + ); + + return Tuple3(isLiked, toggleTrackLike, me); +} + +class TrackHeartButton extends HookConsumerWidget { + final Track track; + const TrackHeartButton({ + Key? key, + required this.track, + }) : super(key: key); + + @override + Widget build(BuildContext context, ref) { + final savedTracks = useQuery( + job: playlistTracksQueryJob("user-liked-tracks"), + externalData: ref.watch(spotifyProvider), + ); + final toggler = useTrackToggleLike(track, ref); + if (toggler.item3.isLoading || !toggler.item3.hasData) { + return const CircularProgressIndicator(); + } + + return HeartButton( + isLiked: toggler.item1, + onPressed: savedTracks.hasData + ? () { + toggler.item2.mutate( + Tuple2(ref.read(spotifyProvider), toggler.item1), + ); + } + : null, + ); + } +} + +class PlaylistHeartButton extends HookConsumerWidget { + final PlaylistSimple playlist; + + const PlaylistHeartButton({ + required this.playlist, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context, ref) { + final me = useQuery( + job: currentUserQueryJob, + externalData: ref.watch(spotifyProvider), + ); + + final job = playlistIsFollowedQueryJob("${playlist.id}:${me.data?.id}"); + final isLikedQuery = useQuery( + job: job, + externalData: ref.watch(spotifyProvider), + ); + + final togglePlaylistLike = useMutation>( + job: toggleFavoritePlaylistMutationJob(playlist.id!), + onData: (payload, variables, queryContext) { + isLikedQuery.refetch(); + QueryBowl.of(context) + .getQuery(currentUserPlaylistsQueryJob.queryKey) + ?.refetch(); + }, + ); + + final titleImage = useMemoized( + () => TypeConversionUtils.image_X_UrlString( + playlist.images, + placeholder: ImagePlaceholder.collection, + ), + [playlist.images]); + + final color = usePaletteGenerator( + context, + titleImage, + ).dominantColor; + + if (me.isLoading || !me.hasData) return const CircularProgressIndicator(); + + return HeartButton( + isLiked: isLikedQuery.data ?? false, + color: color?.titleTextColor, + onPressed: isLikedQuery.hasData + ? () { + togglePlaylistLike.mutate( + Tuple2( + ref.read(spotifyProvider), + isLikedQuery.data!, + ), + ); + } + : null, + ); + } +} + +class AlbumHeartButton extends HookConsumerWidget { + final AlbumSimple album; + + const AlbumHeartButton({ + required this.album, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context, ref) { + final spotify = ref.watch(spotifyProvider); + final me = useQuery( + job: currentUserQueryJob, + externalData: spotify, + ); + + final albumIsSaved = useQuery( + job: albumIsSavedForCurrentUserQueryJob(album.id!), + externalData: spotify, + ); + final isLiked = albumIsSaved.data ?? false; + + final toggleAlbumLike = useMutation>( + job: toggleFavoriteAlbumMutationJob(album.id!), + onData: (payload, variables, queryContext) { + albumIsSaved.refetch(); + QueryBowl.of(context) + .getQuery(currentUserAlbumsQueryJob.queryKey) + ?.refetch(); + }, + ); + + if (me.isLoading || !me.hasData) return const CircularProgressIndicator(); + + return HeartButton( + isLiked: isLiked, + onPressed: albumIsSaved.hasData + ? () { + toggleAlbumLike + .mutate(Tuple2(ref.read(spotifyProvider), isLiked)); + } + : null, + ); + } +} diff --git a/lib/components/Shared/TrackCollectionView.dart b/lib/components/Shared/TrackCollectionView.dart index a1e872a50..85e752669 100644 --- a/lib/components/Shared/TrackCollectionView.dart +++ b/lib/components/Shared/TrackCollectionView.dart @@ -1,3 +1,4 @@ +import 'package:fl_query/fl_query.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -13,12 +14,12 @@ import 'package:flutter/material.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/utils/platform.dart'; -class TrackCollectionView extends HookConsumerWidget { +class TrackCollectionView extends HookConsumerWidget { final logger = getLogger(TrackCollectionView); final String id; final String title; final String? description; - final AsyncValue> tracksSnapshot; + final Query, T> tracksSnapshot; final String titleImage; final bool isPlaying; final void Function([Track? currentTrack]) onPlay; @@ -78,7 +79,7 @@ class TrackCollectionView extends HookConsumerWidget { const CircleBorder(), ), ), - onPressed: tracksSnapshot.asData?.value != null ? onPlay : null, + onPressed: tracksSnapshot.data != null ? onPlay : null, child: Icon( isPlaying ? Icons.stop_rounded : Icons.play_arrow_rounded, color: Theme.of(context).backgroundColor, @@ -175,7 +176,14 @@ class TrackCollectionView extends HookConsumerWidget { const BoxConstraints(maxHeight: 200), child: ClipRRect( borderRadius: BorderRadius.circular(10), - child: UniversalImage(path: titleImage), + child: UniversalImage( + path: titleImage, + placeholder: (context, url) { + return const UniversalImage( + path: "assets/album-placeholder.png", + ); + }, + ), ), ), Column( @@ -220,14 +228,25 @@ class TrackCollectionView extends HookConsumerWidget { ); }), ), - tracksSnapshot.when( - data: (tracks) { + HookBuilder( + builder: (context) { + if (tracksSnapshot.isLoading || !tracksSnapshot.hasData) { + return const ShimmerTrackTile(); + } else if (tracksSnapshot.hasError && + tracksSnapshot.isError) { + return SliverToBoxAdapter( + child: Text("Error ${tracksSnapshot.error}")); + } + + final tracks = tracksSnapshot.data!; return TracksTableView( tracks is! List ? tracks - .map((track) => - TypeConversionUtils.simpleTrack_X_Track( - track, album!)) + .map( + (track) => + TypeConversionUtils.simpleTrack_X_Track( + track, album!), + ) .toList() : tracks, onTrackPlayButtonPressed: onPlay, @@ -235,10 +254,7 @@ class TrackCollectionView extends HookConsumerWidget { userPlaylist: isOwned, ); }, - error: (error, _) => - SliverToBoxAdapter(child: Text("Error $error")), - loading: () => const ShimmerTrackTile(), - ), + ) ], )), ); diff --git a/lib/components/Shared/TrackTile.dart b/lib/components/Shared/TrackTile.dart index f8e784abb..88f6bd806 100644 --- a/lib/components/Shared/TrackTile.dart +++ b/lib/components/Shared/TrackTile.dart @@ -4,16 +4,16 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart' hide Image; import 'package:spotube/components/Shared/AdaptivePopupMenuButton.dart'; +import 'package:spotube/components/Shared/HeartButton.dart'; import 'package:spotube/components/Shared/LinkText.dart'; import 'package:spotube/components/Shared/UniversalImage.dart'; import 'package:spotube/hooks/useBreakpoints.dart'; -import 'package:spotube/hooks/useForceUpdate.dart'; import 'package:spotube/models/Logger.dart'; import 'package:spotube/provider/Auth.dart'; import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/SpotifyDI.dart'; -import 'package:spotube/provider/SpotifyRequests.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; +import 'package:tuple/tuple.dart'; class TrackTile extends HookConsumerWidget { final Playback playback; @@ -60,28 +60,6 @@ class TrackTile extends HookConsumerWidget { final breakpoint = useBreakpoints(); final auth = ref.watch(authProvider); final spotify = ref.watch(spotifyProvider); - final update = useForceUpdate(); - - final savedTracksSnapshot = ref.watch(currentUserSavedTracksQuery); - - final isSaved = savedTracksSnapshot.asData?.value.any( - (e) => track.value.id == e.id, - ) ?? - false; - - final actionFavorite = useCallback((bool isLiked) async { - try { - isLiked - ? await spotify.tracks.me.removeOne(track.value.id!) - : await spotify.tracks.me.saveOne(track.value.id!); - } catch (e, stack) { - logger.e("FavoriteButton.onPressed", e, stack); - } finally { - update(); - ref.refresh(currentUserSavedTracksQuery); - ref.refresh(playlistTracksQuery("user-liked-tracks")); - } - }, [track.value.id, spotify]); final actionRemoveFromPlaylist = useCallback(() async { if (playlistId == null) return; @@ -188,6 +166,8 @@ class TrackTile extends HookConsumerWidget { index: track.value.album?.images?.length == 1 ? 0 : 2, ); + final toggler = useTrackToggleLike(track.value, ref); + return AnimatedContainer( duration: const Duration(milliseconds: 500), decoration: BoxDecoration( @@ -291,14 +271,17 @@ class TrackTile extends HookConsumerWidget { if (!isReallyLocal) AdaptiveActions( actions: [ - if (auth.isLoggedIn) + if (toggler.item3.hasData) Action( - icon: Icon(isSaved - ? Icons.favorite_rounded - : Icons.favorite_border_rounded), + icon: toggler.item1 + ? const Icon( + Icons.favorite_rounded, + color: Colors.pink, + ) + : const Icon(Icons.favorite_border_rounded), text: const Text("Save as favorite"), onPressed: () { - actionFavorite(isSaved); + toggler.item2.mutate(Tuple2(spotify, toggler.item1)); }, ), if (auth.isLoggedIn) diff --git a/lib/provider/Auth.dart b/lib/provider/Auth.dart index 36dce5aa1..b4e2d8c22 100644 --- a/lib/provider/Auth.dart +++ b/lib/provider/Auth.dart @@ -30,7 +30,7 @@ class Auth extends PersistedChangeNotifier { Duration get expiresIn => _expiration?.difference(DateTime.now()) ?? Duration.zero; - refresh() async { + Future refresh() async { final data = await ServiceUtils.getAccessToken(authCookie!); _accessToken = data.accessToken; _expiration = data.expiration; @@ -39,15 +39,17 @@ class Auth extends PersistedChangeNotifier { } Timer? _createRefresher() { - if (expiration == null || !isExpired || authCookie == null) { + if (expiration == null || authCookie == null) { return null; } + if (isExpired) { + refresh(); + } _refresher?.cancel(); - return Timer(expiresIn, refresh); + return Timer(expiresIn - const Duration(minutes: 5), refresh); } void _restartRefresher() { - _refresher?.cancel(); _refresher = _createRefresher(); } diff --git a/lib/provider/SpotifyRequests.dart b/lib/provider/SpotifyRequests.dart index 5cf2f0fb3..1a90728af 100644 --- a/lib/provider/SpotifyRequests.dart +++ b/lib/provider/SpotifyRequests.dart @@ -1,16 +1,13 @@ import 'dart:convert'; import 'package:fl_query/fl_query.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:spotube/models/Logger.dart'; import 'package:spotube/models/LyricsModels.dart'; -import 'package:spotube/provider/Playback.dart'; -import 'package:spotube/provider/SpotifyDI.dart'; +import 'package:spotube/models/SpotubeTrack.dart'; import 'package:spotify/spotify.dart'; -import 'package:spotube/provider/UserPreferences.dart'; import 'package:collection/collection.dart'; import 'package:spotube/utils/service_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; +import 'package:tuple/tuple.dart'; final categoriesQueryJob = InfiniteQueryJob, Map, int>( @@ -47,7 +44,7 @@ final categoryPlaylistsQueryJob = final currentUserPlaylistsQueryJob = QueryJob, SpotifyApi>( - queryKey: "current-user-query", + queryKey: "current-user-playlists", task: (_, spotify) { return spotify.playlists.me.all(); }, @@ -60,14 +57,6 @@ final currentUserAlbumsQueryJob = QueryJob, SpotifyApi>( }, ); -final currentUserFollowingArtistsQuery = - FutureProvider.family, String>( - (ref, pageKey) { - final spotify = ref.watch(spotifyProvider); - return spotify.me.following(FollowingType.artist).getPage(15, pageKey); - }, -); - final currentUserFollowingArtistsQueryJob = InfiniteQueryJob, SpotifyApi, String>( queryKey: "user-following-artists", @@ -80,34 +69,17 @@ final currentUserFollowingArtistsQueryJob = }, ); -final artistProfileQuery = FutureProvider.family( - (ref, id) { - final spotify = ref.watch(spotifyProvider); - return spotify.artists.get(id); - }, -); - -final currentUserFollowsArtistQuery = FutureProvider.family( - (ref, artistId) async { - final spotify = ref.watch(spotifyProvider); - final result = await spotify.me.isFollowing( - FollowingType.artist, - [artistId], - ); - return result.first; - }, +final artistProfileQueryJob = QueryJob.withVariableKey( + preQueryKey: "artist-profile", + task: (queryKey, externalData) => + externalData.artists.get(getVariable(queryKey)), ); -final artistTopTracksQuery = - FutureProvider.family, String>((ref, id) { - final spotify = ref.watch(spotifyProvider); - return spotify.artists.getTopTracks(id, "US"); -}); - -final artistAlbumsQuery = FutureProvider.family, String>( - (ref, id) { - final spotify = ref.watch(spotifyProvider); - return spotify.artists.albums(id).getPage(5, 0); +final artistTopTracksQueryJob = + QueryJob.withVariableKey, SpotifyApi>( + preQueryKey: "artist-top-track-query", + task: (queryKey, spotify) { + return spotify.artists.getTopTracks(getVariable(queryKey), "US"); }, ); @@ -123,53 +95,54 @@ final artistAlbumsQueryJob = }, ); -final artistRelatedArtistsQuery = - FutureProvider.family, String>( - (ref, id) { - final spotify = ref.watch(spotifyProvider); - return spotify.artists.getRelatedArtists(id); +final artistRelatedArtistsQueryJob = + QueryJob.withVariableKey, SpotifyApi>( + preQueryKey: "artist-related-artist-query", + task: (queryKey, spotify) { + return spotify.artists.getRelatedArtists(getVariable(queryKey)); }, ); -final currentUserSavedTracksQuery = FutureProvider>((ref) { - final spotify = ref.watch(spotifyProvider); - return spotify.tracks.me.saved.all().then( - (tracks) => tracks.map((e) => e.track!).toList(), - ); -}); +final currentUserFollowsArtistQueryJob = + QueryJob.withVariableKey( + preQueryKey: "user-follows-artists-query", + task: (artistId, spotify) async { + final result = await spotify.me.isFollowing( + FollowingType.artist, + [getVariable(artistId)], + ); + return result.first; + }, +); -final playlistTracksQuery = FutureProvider.family, String>( - (ref, id) { - try { - final spotify = ref.watch(spotifyProvider); - 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(), - ); - } catch (e, stack) { - getLogger("playlistTracksQuery").e( - "Fetching playlist tracks", - e, - stack, - ); - return []; - } +final playlistTracksQueryJob = + 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(), + ); }, ); -final albumTracksQuery = FutureProvider.family, String>( - (ref, id) { - final spotify = ref.watch(spotifyProvider); +final albumTracksQueryJob = + QueryJob.withVariableKey, SpotifyApi>( + preQueryKey: "album-tracks", + task: (queryKey, spotify) { + final id = getVariable(queryKey); return spotify.albums.getTracks(id).all().then((value) => value.toList()); }, ); -final currentUserQuery = FutureProvider( - (ref) async { - final spotify = ref.watch(spotifyProvider); +final currentUserQueryJob = 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 = [ @@ -186,50 +159,112 @@ final currentUserQuery = FutureProvider( }, ); -final playlistIsFollowedQuery = FutureProvider.family( - (ref, raw) { - final data = jsonDecode(raw); - final playlistId = data["playlistId"] as String; - final userId = data["userId"] as String; - final spotify = ref.watch(spotifyProvider); - return spotify.playlists - .followedBy(playlistId, [userId]).then((value) => value.first); +final playlistIsFollowedQueryJob = QueryJob.withVariableKey( + preQueryKey: "playlist-is-followed", + task: (queryKey, spotify) { + final idMap = getVariable(queryKey).split(":"); + + return spotify.playlists.followedBy(idMap.first, [idMap.last]).then( + (value) => value.first, + ); }, ); -final albumIsSavedForCurrentUserQuery = - FutureProvider.family((ref, albumId) { - final spotify = ref.watch(spotifyProvider); - return spotify.me.isSavedAlbums([albumId]).then((value) => value.first); +final albumIsSavedForCurrentUserQueryJob = + QueryJob.withVariableKey(task: (queryKey, spotify) { + return spotify.me + .isSavedAlbums([getVariable(queryKey)]).then((value) => value.first); }); -final searchQuery = FutureProvider.family, String>((ref, term) { - final spotify = ref.watch(spotifyProvider); - if (term.isEmpty) return []; - return spotify.search.get(term).first(10); -}); +final searchMutationJob = MutationJob, Tuple2>( + mutationKey: "search-query", + task: (ref, variables) { + final queryString = variables.item1; + final spotify = variables.item2; + if (queryString.isEmpty) return []; + return spotify.search.get(queryString).first(10); + }, +); -final geniusLyricsQuery = FutureProvider( - (ref) { - final currentTrack = ref.watch(playbackProvider.select((s) => s.track)); - final geniusAccessToken = - ref.watch(userPreferencesProvider.select((s) => s.geniusAccessToken)); +final geniusLyricsQueryJob = QueryJob>( + queryKey: "genius-lyrics-query", + task: (_, externalData) async { + final currentTrack = externalData.item1; + final geniusAccessToken = externalData.item2; if (currentTrack == null) { return "“Give this player a track to play”\n- S'Challa"; } - return ServiceUtils.getLyrics( + final lyrics = await ServiceUtils.getLyrics( currentTrack.name!, currentTrack.artists?.map((s) => s.name).whereNotNull().toList() ?? [], apiKey: geniusAccessToken, optimizeQuery: true, ); + + if (lyrics == null) throw Exception("Unable find lyrics"); + return lyrics; + }, +); + +final rentanadviserLyricsQueryJob = QueryJob( + queryKey: "synced-lyrics", + task: (_, currentTrack) async { + if (currentTrack == null) throw "No track currently"; + + final timedLyrics = await ServiceUtils.getTimedLyrics(currentTrack); + if (timedLyrics == null) throw Exception("Unable to find lyrics"); + + return timedLyrics; }, ); -final rentanadviserLyricsQuery = FutureProvider( - (ref) { - final currentTrack = ref.watch(playbackProvider.select((s) => s.track)); - if (currentTrack == null) return null; - return ServiceUtils.getTimedLyrics(currentTrack); +final toggleFavoriteTrackMutationJob = + MutationJob.withVariableKey>( + preMutationKey: "toggle-track-like", + task: (queryKey, externalData) async { + final trackId = getVariable(queryKey); + final spotify = externalData.item1; + final isLiked = externalData.item2; + + if (isLiked) { + await spotify.tracks.me.removeOne(trackId); + } else { + await spotify.tracks.me.saveOne(trackId); + } + return !isLiked; + }, +); + +final toggleFavoritePlaylistMutationJob = + MutationJob.withVariableKey>( + preMutationKey: "toggle-playlist-like", + task: (queryKey, externalData) async { + final playlistId = getVariable(queryKey); + final spotify = externalData.item1; + final isLiked = externalData.item2; + + if (isLiked) { + await spotify.playlists.unfollowPlaylist(playlistId); + } else { + await spotify.playlists.followPlaylist(playlistId); + } + return !isLiked; + }, +); + +final toggleFavoriteAlbumMutationJob = + MutationJob.withVariableKey>( + preMutationKey: "toggle-album-like", + task: (queryKey, externalData) async { + final albumId = getVariable(queryKey); + final spotify = externalData.item1; + final isLiked = externalData.item2; + + if (isLiked) { + await spotify.me.removeAlbums([albumId]); + } else { + await spotify.me.saveAlbums([albumId]); + } + return !isLiked; }, ); diff --git a/lib/utils/type_conversion_utils.dart b/lib/utils/type_conversion_utils.dart index 970a1afe2..dae62ebdd 100644 --- a/lib/utils/type_conversion_utils.dart +++ b/lib/utils/type_conversion_utils.dart @@ -6,7 +6,6 @@ import 'package:flutter/widgets.dart' hide Image; import 'package:metadata_god/metadata_god.dart' hide Image; import 'package:path/path.dart'; import 'package:spotube/components/Shared/AnchorButton.dart'; -import 'package:spotube/components/Shared/LinkText.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/models/SpotubeTrack.dart'; import 'package:spotube/utils/primitive_utils.dart'; diff --git a/pubspec.lock b/pubspec.lock index a60e1c246..4850ec60a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -485,14 +485,14 @@ packages: name: fl_query url: "https://pub.dartlang.org" source: hosted - version: "0.3.0" + version: "0.3.1" fl_query_hooks: dependency: "direct main" description: name: fl_query_hooks url: "https://pub.dartlang.org" source: hosted - version: "0.3.0" + version: "0.3.1" flutter: dependency: "direct main" description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index 3ccc3669c..95f8b2810 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -57,8 +57,8 @@ dependencies: url: https://github.com/KRTirtho/metadata_god.git ref: 7d195fdde324b382fc12067c56391285807e6233 visibility_detector: ^0.3.3 - fl_query: ^0.3.0 - fl_query_hooks: ^0.3.0 + fl_query: ^0.3.1 + fl_query_hooks: ^0.3.1 flutter_inappwebview: ^5.4.3+7 tuple: ^2.0.1