mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-12-11 01:17:30 +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) {
|
Widget build(BuildContext context, ref) {
|
||||||
final auth = ref.watch(AuthenticationNotifier.provider);
|
final auth = ref.watch(AuthenticationNotifier.provider);
|
||||||
|
|
||||||
if (auth == null) return Container();
|
if (auth == null) return const SizedBox.shrink();
|
||||||
|
|
||||||
return IconButton(
|
return IconButton(
|
||||||
tooltip: tooltip,
|
tooltip: tooltip,
|
||||||
@ -57,18 +57,21 @@ class HeartButton extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
({
|
typedef UseTrackToggleLike = ({
|
||||||
bool isLiked,
|
bool isLiked,
|
||||||
Mutation<bool, dynamic, bool> toggleTrackLike,
|
Mutation<bool, dynamic, bool> toggleTrackLike,
|
||||||
Query<User?, dynamic> me,
|
Query<User?, dynamic> me,
|
||||||
}) useTrackToggleLike(Track track, WidgetRef ref) {
|
});
|
||||||
|
|
||||||
|
UseTrackToggleLike useTrackToggleLike(Track track, WidgetRef ref) {
|
||||||
final me = useQueries.user.me(ref);
|
final me = useQueries.user.me(ref);
|
||||||
|
|
||||||
final savedTracks =
|
final savedTracks = useQueries.playlist.likedTracksQuery(ref);
|
||||||
useQueries.playlist.tracksOfQuery(ref, "user-liked-tracks");
|
|
||||||
|
|
||||||
final isLiked =
|
final isLiked = useMemoized(
|
||||||
savedTracks.data?.any((element) => element.id == track.id) ?? false;
|
() => savedTracks.data?.any((element) => element.id == track.id) ?? false,
|
||||||
|
[savedTracks.data, track.id],
|
||||||
|
);
|
||||||
|
|
||||||
final mounted = useIsMounted();
|
final mounted = useIsMounted();
|
||||||
|
|
||||||
@ -76,28 +79,48 @@ class HeartButton extends HookConsumerWidget {
|
|||||||
ref,
|
ref,
|
||||||
track.id!,
|
track.id!,
|
||||||
onMutate: (isLiked) {
|
onMutate: (isLiked) {
|
||||||
|
print("Toggle Like onMutate: $isLiked");
|
||||||
|
|
||||||
|
if (isLiked) {
|
||||||
|
savedTracks.setData(
|
||||||
|
savedTracks.data
|
||||||
|
?.where((element) => element.id != track.id)
|
||||||
|
.toList() ??
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
savedTracks.setData(
|
savedTracks.setData(
|
||||||
[
|
[
|
||||||
if (isLiked == true)
|
...?savedTracks.data,
|
||||||
...?savedTracks.data?.where((element) => element.id != track.id)
|
track,
|
||||||
else
|
|
||||||
...?savedTracks.data?..add(track)
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
}
|
||||||
return isLiked;
|
return isLiked;
|
||||||
},
|
},
|
||||||
onData: (data, recoveryData) async {
|
onData: (data, recoveryData) async {
|
||||||
|
print("Toggle Like onData: $data");
|
||||||
await savedTracks.refresh();
|
await savedTracks.refresh();
|
||||||
},
|
},
|
||||||
onError: (payload, isLiked) {
|
onError: (payload, isLiked) {
|
||||||
|
print("Toggle Like onError: $payload");
|
||||||
if (!mounted()) return;
|
if (!mounted()) return;
|
||||||
|
|
||||||
savedTracks.setData([
|
if (isLiked != true) {
|
||||||
if (isLiked != true)
|
savedTracks.setData(
|
||||||
...?savedTracks.data?.where((element) => element.id != track.id)
|
savedTracks.data
|
||||||
else
|
?.where((element) => element.id != track.id)
|
||||||
...?savedTracks.data?..add(track),
|
.toList() ??
|
||||||
]);
|
[],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
savedTracks.setData(
|
||||||
|
[
|
||||||
|
...?savedTracks.data,
|
||||||
|
track,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -113,21 +136,21 @@ class TrackHeartButton extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final savedTracks =
|
final savedTracks = useQueries.playlist.likedTracksQuery(ref);
|
||||||
useQueries.playlist.tracksOfQuery(ref, "user-liked-tracks");
|
final (:me, :isLiked, :toggleTrackLike) = useTrackToggleLike(track, ref);
|
||||||
final toggler = useTrackToggleLike(track, ref);
|
|
||||||
if (toggler.me.isLoading || !toggler.me.hasData) {
|
if (me.isLoading || !me.hasData) {
|
||||||
return const CircularProgressIndicator();
|
return const CircularProgressIndicator();
|
||||||
}
|
}
|
||||||
|
|
||||||
return HeartButton(
|
return HeartButton(
|
||||||
tooltip: toggler.isLiked
|
tooltip: isLiked
|
||||||
? context.l10n.remove_from_favorites
|
? context.l10n.remove_from_favorites
|
||||||
: context.l10n.save_as_favorite,
|
: context.l10n.save_as_favorite,
|
||||||
isLiked: toggler.isLiked,
|
isLiked: isLiked,
|
||||||
onPressed: savedTracks.hasData
|
onPressed: savedTracks.hasData
|
||||||
? () {
|
? () {
|
||||||
toggler.toggleTrackLike.mutate(toggler.isLiked);
|
toggleTrackLike.mutate(isLiked);
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -55,7 +55,12 @@ class PlaylistView extends HookConsumerWidget {
|
|||||||
final mediaQuery = MediaQuery.of(context);
|
final mediaQuery = MediaQuery.of(context);
|
||||||
|
|
||||||
final meSnapshot = useQueries.user.me(ref);
|
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(
|
final isPlaylistPlaying = useMemoized(
|
||||||
() => proxyPlaylist.collections.contains(playlist.id!),
|
() => proxyPlaylist.collections.contains(playlist.id!),
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:catcher/catcher.dart';
|
import 'package:catcher/catcher.dart';
|
||||||
import 'package:fl_query/fl_query.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(
|
Future<List<Track>> tracksOf(
|
||||||
String playlistId,
|
String playlistId,
|
||||||
SpotifyApi spotify,
|
SpotifyApi spotify,
|
||||||
WidgetRef ref,
|
WidgetRef ref,
|
||||||
) {
|
) async {
|
||||||
if (playlistId == "user-liked-tracks") {
|
if (playlistId == "user-liked-tracks") return <Track>[];
|
||||||
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;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return spotify.playlists.getTracksByPlaylistId(playlistId).all().then(
|
return spotify.playlists.getTracksByPlaylistId(playlistId).all().then(
|
||||||
(value) => value.toList(),
|
(value) => value.toList(),
|
||||||
);
|
);
|
||||||
@ -174,22 +199,6 @@ class PlaylistQueries {
|
|||||||
return useSpotifyQuery<List<Track>, dynamic>(
|
return useSpotifyQuery<List<Track>, dynamic>(
|
||||||
"playlist-tracks/$playlistId",
|
"playlist-tracks/$playlistId",
|
||||||
(spotify) => tracksOf(playlistId, spotify, ref),
|
(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,
|
ref: ref,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import 'package:fl_query/fl_query.dart';
|
import 'package:fl_query/fl_query.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
import 'package:spotube/hooks/use_spotify_query.dart';
|
import 'package:spotube/hooks/use_spotify_query.dart';
|
||||||
@ -8,6 +9,8 @@ import 'package:spotube/utils/type_conversion_utils.dart';
|
|||||||
class UserQueries {
|
class UserQueries {
|
||||||
const UserQueries();
|
const UserQueries();
|
||||||
Query<User?, dynamic> me(WidgetRef ref) {
|
Query<User?, dynamic> me(WidgetRef ref) {
|
||||||
|
final context = useContext();
|
||||||
|
|
||||||
return useSpotifyQuery<User, dynamic>(
|
return useSpotifyQuery<User, dynamic>(
|
||||||
"current-user",
|
"current-user",
|
||||||
(spotify) async {
|
(spotify) async {
|
||||||
@ -26,6 +29,11 @@ class UserQueries {
|
|||||||
}
|
}
|
||||||
return me;
|
return me;
|
||||||
},
|
},
|
||||||
|
refreshConfig: RefreshConfig.withDefaults(
|
||||||
|
context,
|
||||||
|
// will never make it stale
|
||||||
|
staleDuration: const Duration(days: 60),
|
||||||
|
),
|
||||||
ref: ref,
|
ref: ref,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user