diff --git a/lib/components/shared/heart_button.dart b/lib/components/shared/heart_button.dart index 2b877ecf..c18ca2e4 100644 --- a/lib/components/shared/heart_button.dart +++ b/lib/components/shared/heart_button.dart @@ -29,7 +29,7 @@ class HeartButton extends HookConsumerWidget { Widget build(BuildContext context, ref) { final auth = ref.watch(AuthenticationNotifier.provider); - if (auth == null) return Container(); + if (auth == null) return const SizedBox.shrink(); return IconButton( tooltip: tooltip, @@ -57,18 +57,21 @@ class HeartButton extends HookConsumerWidget { } } -({ +typedef UseTrackToggleLike = ({ bool isLiked, Mutation toggleTrackLike, Query me, -}) useTrackToggleLike(Track track, WidgetRef ref) { +}); + +UseTrackToggleLike useTrackToggleLike(Track track, WidgetRef ref) { final me = useQueries.user.me(ref); - final savedTracks = - useQueries.playlist.tracksOfQuery(ref, "user-liked-tracks"); + final savedTracks = useQueries.playlist.likedTracksQuery(ref); - final isLiked = - savedTracks.data?.any((element) => element.id == track.id) ?? false; + final isLiked = useMemoized( + () => savedTracks.data?.any((element) => element.id == track.id) ?? false, + [savedTracks.data, track.id], + ); final mounted = useIsMounted(); @@ -76,28 +79,48 @@ class HeartButton extends HookConsumerWidget { ref, track.id!, onMutate: (isLiked) { - savedTracks.setData( - [ - if (isLiked == true) - ...?savedTracks.data?.where((element) => element.id != track.id) - else - ...?savedTracks.data?..add(track) - ], - ); + print("Toggle Like onMutate: $isLiked"); + + if (isLiked) { + savedTracks.setData( + savedTracks.data + ?.where((element) => element.id != track.id) + .toList() ?? + [], + ); + } else { + savedTracks.setData( + [ + ...?savedTracks.data, + track, + ], + ); + } return isLiked; }, onData: (data, recoveryData) async { + print("Toggle Like onData: $data"); await savedTracks.refresh(); }, onError: (payload, isLiked) { + print("Toggle Like onError: $payload"); if (!mounted()) return; - savedTracks.setData([ - if (isLiked != true) - ...?savedTracks.data?.where((element) => element.id != track.id) - else - ...?savedTracks.data?..add(track), - ]); + if (isLiked != true) { + savedTracks.setData( + savedTracks.data + ?.where((element) => element.id != track.id) + .toList() ?? + [], + ); + } else { + savedTracks.setData( + [ + ...?savedTracks.data, + track, + ], + ); + } }, ); @@ -113,21 +136,21 @@ class TrackHeartButton extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { - final savedTracks = - useQueries.playlist.tracksOfQuery(ref, "user-liked-tracks"); - final toggler = useTrackToggleLike(track, ref); - if (toggler.me.isLoading || !toggler.me.hasData) { + final savedTracks = useQueries.playlist.likedTracksQuery(ref); + final (:me, :isLiked, :toggleTrackLike) = useTrackToggleLike(track, ref); + + if (me.isLoading || !me.hasData) { return const CircularProgressIndicator(); } return HeartButton( - tooltip: toggler.isLiked + tooltip: isLiked ? context.l10n.remove_from_favorites : context.l10n.save_as_favorite, - isLiked: toggler.isLiked, + isLiked: isLiked, onPressed: savedTracks.hasData ? () { - toggler.toggleTrackLike.mutate(toggler.isLiked); + toggleTrackLike.mutate(isLiked); } : null, ); diff --git a/lib/pages/playlist/playlist.dart b/lib/pages/playlist/playlist.dart index baee0669..722fcb6d 100644 --- a/lib/pages/playlist/playlist.dart +++ b/lib/pages/playlist/playlist.dart @@ -55,7 +55,12 @@ class PlaylistView extends HookConsumerWidget { final mediaQuery = MediaQuery.of(context); final meSnapshot = useQueries.user.me(ref); - final tracksSnapshot = useQueries.playlist.tracksOfQuery(ref, playlist.id!); + final playlistTrackSnapshot = + useQueries.playlist.tracksOfQuery(ref, playlist.id!); + final likedTracksSnapshot = useQueries.playlist.likedTracksQuery(ref); + final tracksSnapshot = playlist.id! == "user-liked-tracks" + ? likedTracksSnapshot + : playlistTrackSnapshot; final isPlaylistPlaying = useMemoized( () => proxyPlaylist.collections.contains(playlist.id!), diff --git a/lib/services/queries/playlist.dart b/lib/services/queries/playlist.dart index 15514e04..f3a00eac 100644 --- a/lib/services/queries/playlist.dart +++ b/lib/services/queries/playlist.dart @@ -1,4 +1,5 @@ import 'dart:io'; +import 'dart:math'; import 'package:catcher/catcher.dart'; import 'package:fl_query/fl_query.dart'; @@ -145,23 +146,47 @@ class PlaylistQueries { ); } + Future> likedTracks( + SpotifyApi spotify, + WidgetRef ref, + ) async { + final tracks = await spotify.tracks.me.saved.all(); + + return tracks.map((e) => e.track!).toList(); + } + + Query, dynamic> likedTracksQuery(WidgetRef ref) { + final query = useCallback((spotify) => likedTracks(spotify, ref), []); + final context = useContext(); + + return useSpotifyQuery, dynamic>( + "user-liked-tracks", + query, + jsonConfig: 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(), + ), + refreshConfig: RefreshConfig.withDefaults( + context, + // will never make it stale + staleDuration: const Duration(days: 60), + ), + ref: ref, + ); + } + Future> tracksOf( String playlistId, SpotifyApi spotify, WidgetRef ref, - ) { - if (playlistId == "user-liked-tracks") { - return spotify.tracks.me.saved - .all() - .then( - (tracks) => tracks.map((e) => e.track!).toList(), - ) - .catchError((e) { - final isLoggedIn = ref.read(AuthenticationNotifier.provider) != null; - if (e is SocketException && isLoggedIn) return []; - throw e; - }); - } + ) async { + if (playlistId == "user-liked-tracks") return []; return spotify.playlists.getTracksByPlaylistId(playlistId).all().then( (value) => value.toList(), ); @@ -174,22 +199,6 @@ class PlaylistQueries { return useSpotifyQuery, dynamic>( "playlist-tracks/$playlistId", (spotify) => tracksOf(playlistId, spotify, ref), - 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, - retryConfig: RetryConfig.withConstantDefaults( - maxRetries: 1, - retryDelay: const Duration(seconds: 5), - ), ref: ref, ); } diff --git a/lib/services/queries/user.dart b/lib/services/queries/user.dart index 63d58afd..89792592 100644 --- a/lib/services/queries/user.dart +++ b/lib/services/queries/user.dart @@ -1,4 +1,5 @@ import 'package:fl_query/fl_query.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/hooks/use_spotify_query.dart'; @@ -8,6 +9,8 @@ import 'package:spotube/utils/type_conversion_utils.dart'; class UserQueries { const UserQueries(); Query me(WidgetRef ref) { + final context = useContext(); + return useSpotifyQuery( "current-user", (spotify) async { @@ -26,6 +29,11 @@ class UserQueries { } return me; }, + refreshConfig: RefreshConfig.withDefaults( + context, + // will never make it stale + staleDuration: const Duration(days: 60), + ), ref: ref, ); }