mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-12-09 08:47:31 +00:00
fix: user liked tracks memory leak due to isStale & updateQueryFn
This commit is contained in:
parent
1b52de0906
commit
57281faa42
@ -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<bool, dynamic, bool> toggleTrackLike,
|
||||
Query<User?, dynamic> 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,
|
||||
);
|
||||
|
||||
@ -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!),
|
||||
|
||||
@ -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<List<Track>> likedTracks(
|
||||
SpotifyApi spotify,
|
||||
WidgetRef ref,
|
||||
) async {
|
||||
final tracks = await spotify.tracks.me.saved.all();
|
||||
|
||||
return tracks.map((e) => e.track!).toList();
|
||||
}
|
||||
|
||||
Query<List<Track>, dynamic> likedTracksQuery(WidgetRef ref) {
|
||||
final query = useCallback((spotify) => likedTracks(spotify, ref), []);
|
||||
final context = useContext();
|
||||
|
||||
return useSpotifyQuery<List<Track>, dynamic>(
|
||||
"user-liked-tracks",
|
||||
query,
|
||||
jsonConfig: JsonConfig(
|
||||
toJson: (tracks) => <String, dynamic>{
|
||||
'tracks': tracks.map((e) => e.toJson()).toList(),
|
||||
},
|
||||
fromJson: (json) => (json['tracks'] as List)
|
||||
.map(
|
||||
(e) => Track.fromJson((e as Map).castKeyDeep<String>()),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
refreshConfig: RefreshConfig.withDefaults(
|
||||
context,
|
||||
// will never make it stale
|
||||
staleDuration: const Duration(days: 60),
|
||||
),
|
||||
ref: ref,
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<Track>> 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 <Track>[];
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
) async {
|
||||
if (playlistId == "user-liked-tracks") return <Track>[];
|
||||
return spotify.playlists.getTracksByPlaylistId(playlistId).all().then(
|
||||
(value) => value.toList(),
|
||||
);
|
||||
@ -174,22 +199,6 @@ class PlaylistQueries {
|
||||
return useSpotifyQuery<List<Track>, dynamic>(
|
||||
"playlist-tracks/$playlistId",
|
||||
(spotify) => tracksOf(playlistId, spotify, ref),
|
||||
jsonConfig: playlistId == "user-liked-tracks"
|
||||
? JsonConfig(
|
||||
toJson: (tracks) => <String, dynamic>{
|
||||
'tracks': tracks.map((e) => e.toJson()).toList()
|
||||
},
|
||||
fromJson: (json) => (json['tracks'] as List)
|
||||
.map((e) => Track.fromJson(
|
||||
(e as Map).castKeyDeep<String>(),
|
||||
))
|
||||
.toList(),
|
||||
)
|
||||
: null,
|
||||
retryConfig: RetryConfig.withConstantDefaults(
|
||||
maxRetries: 1,
|
||||
retryDelay: const Duration(seconds: 5),
|
||||
),
|
||||
ref: ref,
|
||||
);
|
||||
}
|
||||
|
||||
@ -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<User?, dynamic> me(WidgetRef ref) {
|
||||
final context = useContext();
|
||||
|
||||
return useSpotifyQuery<User, dynamic>(
|
||||
"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,
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user