diff --git a/lib/components/heart_button/heart_button.dart b/lib/components/heart_button/heart_button.dart index eca3c513..14a0572f 100644 --- a/lib/components/heart_button/heart_button.dart +++ b/lib/components/heart_button/heart_button.dart @@ -38,6 +38,7 @@ class HeartButton extends HookConsumerWidget { child: IconButton( variance: variance, size: size, + enabled: onPressed != null, icon: AnimatedSwitcher( switchInCurve: Curves.fastOutSlowIn, switchOutCurve: Curves.fastOutSlowIn, @@ -74,7 +75,8 @@ class TrackHeartButton extends HookConsumerWidget { Widget build(BuildContext context, ref) { final savedTracks = ref.watch(metadataPluginSavedTracksProvider); final me = ref.watch(metadataPluginUserProvider); - final (:isLiked, :toggleTrackLike) = useTrackToggleLike(track, ref); + final (:isLiked, :isLoading, :toggleTrackLike) = + useTrackToggleLike(track, ref); if (me.isLoading) { return const CircularProgressIndicator(); @@ -85,11 +87,11 @@ class TrackHeartButton extends HookConsumerWidget { ? context.l10n.remove_from_favorites : context.l10n.save_as_favorite, isLiked: isLiked, - onPressed: savedTracks.asData?.value != null - ? () { + onPressed: savedTracks.asData?.value == null || isLoading + ? null + : () { toggleTrackLike(track); - } - : null, + }, ); } } diff --git a/lib/components/heart_button/use_track_toggle_like.dart b/lib/components/heart_button/use_track_toggle_like.dart index 349a4ab2..af961578 100644 --- a/lib/components/heart_button/use_track_toggle_like.dart +++ b/lib/components/heart_button/use_track_toggle_like.dart @@ -4,6 +4,7 @@ import 'package:spotube/provider/metadata_plugin/library/tracks.dart'; typedef UseTrackToggleLike = ({ bool isLiked, + bool isLoading, Future Function(SpotubeTrackObject track) toggleTrackLike, }); @@ -11,12 +12,11 @@ UseTrackToggleLike useTrackToggleLike(SpotubeTrackObject track, WidgetRef ref) { final savedTracksNotifier = ref.watch(metadataPluginSavedTracksProvider.notifier); - final isSavedTrack = ref.watch( - metadataPluginIsSavedTrackProvider(track.id), - ); + final isSavedTrack = ref.watch(metadataPluginIsSavedTrackProvider(track.id)); return ( isLiked: isSavedTrack.asData?.value ?? false, + isLoading: isSavedTrack.isLoading, toggleTrackLike: (track) async { final isLikedTrack = await ref.read( metadataPluginIsSavedTrackProvider(track.id).future, diff --git a/lib/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart b/lib/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart index 0c120e71..3ac90a06 100644 --- a/lib/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart +++ b/lib/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart @@ -14,6 +14,7 @@ import 'package:very_good_infinite_list/very_good_infinite_list.dart'; class HorizontalPlaybuttonCardView extends HookWidget { final Widget title; final List items; + final Widget? error; final VoidCallback onFetchMore; final bool isLoadingNextPage; final bool hasNextPage; @@ -26,6 +27,7 @@ class HorizontalPlaybuttonCardView extends HookWidget { required this.onFetchMore, required this.isLoadingNextPage, this.titleTrailing, + this.error, super.key, }) : assert( items.every( @@ -64,54 +66,57 @@ class HorizontalPlaybuttonCardView extends HookWidget { if (titleTrailing != null) titleTrailing!, ], ), - SizedBox( - height: isArtist ? 250 : 225, - child: NotificationListener( - // disable multiple scrollbar to use this - onNotification: (notification) => true, - child: ScrollConfiguration( - behavior: ScrollConfiguration.of(context).copyWith( - dragDevices: PointerDeviceKind.values.toSet(), - ), - child: items.isEmpty - ? ListView.builder( - scrollDirection: Axis.horizontal, - itemCount: 5, - itemBuilder: (context, index) { - return AlbumCard(FakeData.albumSimple); - }, - ) - : InfiniteList( - scrollController: scrollController, - scrollDirection: Axis.horizontal, - padding: const EdgeInsets.symmetric(vertical: 8.0), - itemCount: items.length, - onFetchData: onFetchMore, - loadingBuilder: (context) => Skeletonizer( - enabled: true, - child: isArtist - ? ArtistCard(FakeData.artist) - : AlbumCard(FakeData.albumSimple), - ), - isLoading: isLoadingNextPage, - hasReachedMax: !hasNextPage, - separatorBuilder: (context, index) => Gap(12 * scale), - itemBuilder: (context, index) { - final item = items[index]; + if (error != null) + error! + else + SizedBox( + height: isArtist ? 250 : 225, + child: NotificationListener( + // disable multiple scrollbar to use this + onNotification: (notification) => true, + child: ScrollConfiguration( + behavior: ScrollConfiguration.of(context).copyWith( + dragDevices: PointerDeviceKind.values.toSet(), + ), + child: items.isEmpty + ? ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: 5, + itemBuilder: (context, index) { + return AlbumCard(FakeData.albumSimple); + }, + ) + : InfiniteList( + scrollController: scrollController, + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(vertical: 8.0), + itemCount: items.length, + onFetchData: onFetchMore, + loadingBuilder: (context) => Skeletonizer( + enabled: true, + child: isArtist + ? ArtistCard(FakeData.artist) + : AlbumCard(FakeData.albumSimple), + ), + isLoading: isLoadingNextPage, + hasReachedMax: !hasNextPage, + separatorBuilder: (context, index) => Gap(12 * scale), + itemBuilder: (context, index) { + final item = items[index]; - return switch (item) { - SpotubeSimplePlaylistObject() => - PlaylistCard(item as SpotubeSimplePlaylistObject), - SpotubeSimpleAlbumObject() => - AlbumCard(item as SpotubeSimpleAlbumObject), - SpotubeFullArtistObject() => - ArtistCard(item as SpotubeFullArtistObject), - _ => const SizedBox.shrink(), - }; - }), + return switch (item) { + SpotubeSimplePlaylistObject() => PlaylistCard( + item as SpotubeSimplePlaylistObject), + SpotubeSimpleAlbumObject() => + AlbumCard(item as SpotubeSimpleAlbumObject), + SpotubeFullArtistObject() => + ArtistCard(item as SpotubeFullArtistObject), + _ => const SizedBox.shrink(), + }; + }), + ), ), ), - ), ], ), ); diff --git a/lib/components/track_presentation/presentation_list.dart b/lib/components/track_presentation/presentation_list.dart index d41416b4..19772c7c 100644 --- a/lib/components/track_presentation/presentation_list.dart +++ b/lib/components/track_presentation/presentation_list.dart @@ -6,6 +6,7 @@ import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter_extension.dart'; import 'package:skeletonizer/skeletonizer.dart'; import 'package:spotube/collections/fake.dart'; +import 'package:spotube/components/fallbacks/error_box.dart'; import 'package:spotube/components/track_presentation/presentation_props.dart'; import 'package:spotube/components/track_presentation/presentation_state.dart'; import 'package:spotube/components/track_presentation/use_track_tile_play_callback.dart'; @@ -30,6 +31,19 @@ class PresentationListSection extends HookConsumerWidget { final onTileTap = useTrackTilePlayCallback(ref); if (state.presentationTracks.isEmpty && !options.pagination.isLoading) { + if (options.error != null) { + return SliverToBoxAdapter( + child: Center( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: ErrorBox( + error: options.error!, + onRetry: options.pagination.onRefresh, + ), + ), + ), + ); + } return SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.all(8.0), diff --git a/lib/components/track_presentation/presentation_props.dart b/lib/components/track_presentation/presentation_props.dart index 72f65c71..1992487f 100644 --- a/lib/components/track_presentation/presentation_props.dart +++ b/lib/components/track_presentation/presentation_props.dart @@ -50,6 +50,7 @@ class TrackPresentationOptions { final PaginationProps pagination; final bool isLiked; final String? shareUrl; + final Object? error; // events final FutureOr Function()? onHeart; // if null heart button will hidden @@ -67,6 +68,7 @@ class TrackPresentationOptions { this.shareUrl, this.isLiked = false, this.onHeart, + this.error, }) : assert(collection is SpotubeSimpleAlbumObject || collection is SpotubeSimplePlaylistObject); @@ -90,7 +92,8 @@ class TrackPresentationOptions { other.pagination == pagination && other.isLiked == isLiked && other.shareUrl == shareUrl && - other.onHeart == onHeart; + other.onHeart == onHeart && + other.error == error; } @override @@ -105,5 +108,6 @@ class TrackPresentationOptions { pagination.hashCode ^ isLiked.hashCode ^ shareUrl.hashCode ^ - onHeart.hashCode; + onHeart.hashCode ^ + error.hashCode; } diff --git a/lib/modules/home/sections/new_releases.dart b/lib/modules/home/sections/new_releases.dart index b2f46b10..2a1f2f91 100644 --- a/lib/modules/home/sections/new_releases.dart +++ b/lib/modules/home/sections/new_releases.dart @@ -1,11 +1,13 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart'; +import 'package:spotube/components/fallbacks/error_box.dart'; import 'package:spotube/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/provider/metadata_plugin/album/releases.dart'; import 'package:spotube/provider/metadata_plugin/core/auth.dart'; import 'package:spotube/provider/metadata_plugin/utils/common.dart'; +import 'package:spotube/services/metadata/errors/exceptions.dart'; class HomeNewReleasesSection extends HookConsumerWidget { const HomeNewReleasesSection({super.key}); @@ -24,12 +26,30 @@ class HomeNewReleasesSection extends HookConsumerWidget { return const SizedBox.shrink(); } + if (newReleases.error + case MetadataPluginException( + errorCode: MetadataPluginErrorCode.noDefaultPlugin, + message: _, + )) { + return const SizedBox.shrink(); + } + return HorizontalPlaybuttonCardView( items: newReleases.asData?.value.items ?? [], title: Text(context.l10n.new_releases), isLoadingNextPage: newReleases.isLoadingNextPage, hasNextPage: newReleases.asData?.value.hasMore ?? false, onFetchMore: newReleasesNotifier.fetchMore, + error: newReleases.hasError + ? Center( + child: ErrorBox( + error: newReleases.error!, + onRetry: () { + ref.invalidate(metadataPluginAlbumReleasesProvider); + }, + ), + ) + : null, ); } } diff --git a/lib/pages/album/album.dart b/lib/pages/album/album.dart index 57b81cea..049d8023 100644 --- a/lib/pages/album/album.dart +++ b/lib/pages/album/album.dart @@ -48,6 +48,7 @@ class AlbumPage extends HookConsumerWidget { description: "${context.l10n.released} • ${album.releaseDate} • ${album.artists.first.name}", tracks: tracks.asData?.value.items ?? [], + error: tracks.error, pagination: PaginationProps( hasNextPage: tracks.asData?.value.hasMore ?? false, isLoading: tracks.isLoading || tracks.isLoadingNextPage, diff --git a/lib/pages/playlist/liked_playlist.dart b/lib/pages/playlist/liked_playlist.dart index 80c05303..3897acef 100644 --- a/lib/pages/playlist/liked_playlist.dart +++ b/lib/pages/playlist/liked_playlist.dart @@ -51,6 +51,7 @@ class LikedPlaylistPage extends HookConsumerWidget { title: playlist.name, description: playlist.description, tracks: tracks, + error: likedTracks.error, routePath: '/playlist/${playlist.id}', isLiked: false, shareUrl: null, diff --git a/lib/pages/playlist/playlist.dart b/lib/pages/playlist/playlist.dart index b50bf147..4aca5945 100644 --- a/lib/pages/playlist/playlist.dart +++ b/lib/pages/playlist/playlist.dart @@ -79,6 +79,7 @@ class PlaylistPage extends HookConsumerWidget { owner: playlist.owner.name, ownerImage: playlist.owner.images.lastOrNull?.url, tracks: tracks.asData?.value.items ?? [], + error: tracks.error, routePath: '/playlist/${playlist.id}', isLiked: isFavoritePlaylist.asData?.value ?? false, shareUrl: playlist.externalUri, diff --git a/lib/provider/metadata_plugin/library/albums.dart b/lib/provider/metadata_plugin/library/albums.dart index daa21151..10438025 100644 --- a/lib/provider/metadata_plugin/library/albums.dart +++ b/lib/provider/metadata_plugin/library/albums.dart @@ -1,6 +1,6 @@ import 'package:riverpod/riverpod.dart'; import 'package:spotube/models/metadata/metadata.dart'; -import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart'; +import 'package:spotube/provider/metadata_plugin/core/auth.dart'; import 'package:spotube/provider/metadata_plugin/utils/paginated.dart'; class MetadataPluginSavedAlbumNotifier @@ -18,38 +18,50 @@ class MetadataPluginSavedAlbumNotifier @override build() async { - ref.watch(metadataPluginProvider); + await ref.watch(metadataPluginAuthenticatedProvider.future); return await fetch(0, 20); } Future addFavorite(List albums) async { - await update((state) async { - (await metadataPlugin).album.save(albums.map((e) => e.id).toList()); - return state.copyWith( - items: [...state.items, ...albums], - ); - }); + if (albums.isEmpty || state.value == null) return; + final oldState = state.value; - for (final album in albums) { - ref.invalidate(metadataPluginIsSavedAlbumProvider(album.id)); + state = AsyncData( + state.value!.copyWith( + items: [ + ...albums, + ...state.value!.items, + ], + ), + ); + try { + await (await metadataPlugin).album.save(albums.map((e) => e.id).toList()); + } catch (e) { + state = AsyncData(oldState!); + rethrow; } } Future removeFavorite(List albums) async { - await update((state) async { - final albumIds = albums.map((e) => e.id).toList(); - (await metadataPlugin).album.unsave(albumIds); - return state.copyWith( - items: state.items + if (albums.isEmpty || state.value == null) return; + + final oldState = state.value; + + final albumIds = albums.map((e) => e.id).toList(); + state = AsyncData( + state.value!.copyWith( + items: state.value!.items .where( (e) => albumIds.contains((e).id) == false, ) .toList(), - ); - }); - - for (final album in albums) { - ref.invalidate(metadataPluginIsSavedAlbumProvider(album.id)); + ), + ); + try { + await (await metadataPlugin).album.unsave(albumIds); + } catch (e) { + state = AsyncData(oldState!); + rethrow; } } } @@ -63,9 +75,14 @@ final metadataPluginSavedAlbumsProvider = AsyncNotifierProvider< final metadataPluginIsSavedAlbumProvider = FutureProvider.autoDispose.family( (ref, albumId) async { - final metadataPlugin = await ref.watch(metadataPluginProvider.future); + final savedAlbums = + await ref.watch(metadataPluginSavedAlbumsProvider.future); + final savedAlbumsNotifier = + ref.read(metadataPluginSavedAlbumsProvider.notifier); + final allSavedAlbums = savedAlbums.hasMore + ? await savedAlbumsNotifier.fetchAll() + : savedAlbums.items; - return metadataPlugin!.user - .isSavedAlbums([albumId]).then((value) => value.first); + return allSavedAlbums.any((element) => element.id == albumId); }, ); diff --git a/lib/provider/metadata_plugin/library/artists.dart b/lib/provider/metadata_plugin/library/artists.dart index 30d0f641..31f976e0 100644 --- a/lib/provider/metadata_plugin/library/artists.dart +++ b/lib/provider/metadata_plugin/library/artists.dart @@ -1,6 +1,6 @@ import 'package:riverpod/riverpod.dart'; import 'package:spotube/models/metadata/metadata.dart'; -import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart'; +import 'package:spotube/provider/metadata_plugin/core/auth.dart'; import 'package:spotube/provider/metadata_plugin/utils/paginated.dart'; class MetadataPluginSavedArtistNotifier @@ -20,38 +20,53 @@ class MetadataPluginSavedArtistNotifier @override build() async { - ref.watch(metadataPluginProvider); + await ref.watch(metadataPluginAuthenticatedProvider.future); return await fetch(0, 20); } Future addFavorite(List artists) async { - await update((state) async { - (await metadataPlugin).artist.save(artists.map((e) => e.id).toList()); - return state.copyWith( - items: [...state.items, ...artists], - ); - }); + if (artists.isEmpty || state.value == null) return; + final oldState = state.value; - for (final artist in artists) { - ref.invalidate(metadataPluginIsSavedArtistProvider(artist.id)); + state = AsyncData( + state.value!.copyWith( + items: [ + ...artists, + ...state.value!.items, + ], + ), + ); + try { + await (await metadataPlugin) + .artist + .save(artists.map((e) => e.id).toList()); + } catch (e) { + state = AsyncData(oldState!); + rethrow; } } Future removeFavorite(List artists) async { - await update((state) async { - final artistIds = artists.map((e) => e.id).toList(); - (await metadataPlugin).artist.unsave(artistIds); - return state.copyWith( - items: state.items + if (artists.isEmpty || state.value == null) return; + + final oldState = state.value; + + final artistIds = artists.map((e) => e.id).toList(); + state = AsyncData( + state.value!.copyWith( + items: state.value!.items .where( (e) => artistIds.contains((e).id) == false, ) .toList(), - ); - }); + ), + ); - for (final artist in artists) { - ref.invalidate(metadataPluginIsSavedArtistProvider(artist.id)); + try { + await (await metadataPlugin).artist.unsave(artistIds); + } catch (e) { + state = AsyncData(oldState!); + rethrow; } } } @@ -65,9 +80,15 @@ final metadataPluginSavedArtistsProvider = AsyncNotifierProvider< final metadataPluginIsSavedArtistProvider = FutureProvider.autoDispose.family( (ref, artistId) async { - final metadataPlugin = await ref.watch(metadataPluginProvider.future); + final savedArtists = + await ref.watch(metadataPluginSavedArtistsProvider.future); + final savedArtistsNotifier = + ref.read(metadataPluginSavedArtistsProvider.notifier); - return metadataPlugin!.user - .isSavedArtists([artistId]).then((value) => value.first); + final allSavedArtists = savedArtists.hasMore + ? await savedArtistsNotifier.fetchAll() + : savedArtists.items; + + return allSavedArtists.any((element) => element.id == artistId); }, ); diff --git a/lib/provider/metadata_plugin/library/playlists.dart b/lib/provider/metadata_plugin/library/playlists.dart index dbd3ae28..6350d610 100644 --- a/lib/provider/metadata_plugin/library/playlists.dart +++ b/lib/provider/metadata_plugin/library/playlists.dart @@ -1,6 +1,7 @@ import 'package:collection/collection.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotube/models/metadata/metadata.dart'; +import 'package:spotube/provider/metadata_plugin/core/auth.dart'; import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart'; import 'package:spotube/provider/metadata_plugin/tracks/playlist.dart'; import 'package:spotube/provider/metadata_plugin/utils/paginated.dart'; @@ -21,7 +22,8 @@ class MetadataPluginSavedPlaylistsNotifier @override build() async { - ref.watch(metadataPluginProvider); + await ref.watch(metadataPluginAuthenticatedProvider.future); + final playlists = await fetch(0, 20); return playlists; @@ -42,25 +44,43 @@ class MetadataPluginSavedPlaylistsNotifier } Future addFavorite(SpotubeSimplePlaylistObject playlist) async { - await update((state) async { - (await metadataPlugin).playlist.save(playlist.id); - return state.copyWith( - items: [...state.items, playlist], - ); - }); + if (state.value == null) return; - ref.invalidate(metadataPluginIsSavedPlaylistProvider(playlist.id)); + final oldState = state.value; + + state = AsyncData( + state.value!.copyWith( + items: [ + playlist, + ...state.value!.items, + ], + ), + ); + + try { + await (await metadataPlugin).playlist.save(playlist.id); + } catch (e) { + state = AsyncData(oldState!); + rethrow; + } } Future removeFavorite(SpotubeSimplePlaylistObject playlist) async { - await update((state) async { - (await metadataPlugin).playlist.unsave(playlist.id); - return state.copyWith( - items: state.items.where((e) => (e).id != playlist.id).toList(), - ); - }); + if (state.value == null) return; - ref.invalidate(metadataPluginIsSavedPlaylistProvider(playlist.id)); + final oldState = state.value; + state = AsyncData( + state.value!.copyWith( + items: state.value!.items.where((e) => (e).id != playlist.id).toList(), + ), + ); + + try { + await (await metadataPlugin).playlist.unsave(playlist.id); + } catch (e) { + state = AsyncData(oldState!); + rethrow; + } } Future delete(String playlistId) async { @@ -114,8 +134,16 @@ final metadataPluginIsSavedPlaylistProvider = throw MetadataPluginException.noDefaultPlugin(); } - final follows = await plugin.user.isSavedPlaylist(id); + final savedPlaylists = + await ref.watch(metadataPluginSavedPlaylistsProvider.future); - return follows; + final savedPlaylistsNotifier = + ref.read(metadataPluginSavedPlaylistsProvider.notifier); + + final allSavedPlaylists = savedPlaylists.hasMore + ? await savedPlaylistsNotifier.fetchAll() + : savedPlaylists.items; + + return allSavedPlaylists.any((element) => element.id == id); }, ); diff --git a/lib/provider/metadata_plugin/library/tracks.dart b/lib/provider/metadata_plugin/library/tracks.dart index b9747c6c..d19865dd 100644 --- a/lib/provider/metadata_plugin/library/tracks.dart +++ b/lib/provider/metadata_plugin/library/tracks.dart @@ -1,6 +1,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/models/metadata/metadata.dart'; -import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart'; +import 'package:spotube/provider/metadata_plugin/core/auth.dart'; import 'package:spotube/provider/metadata_plugin/utils/common.dart'; import 'package:spotube/provider/metadata_plugin/utils/paginated.dart'; @@ -22,20 +22,57 @@ class MetadataPluginSavedTracksNotifier build() async { ref.cacheFor(); - ref.watch(metadataPluginProvider); + await ref.watch(metadataPluginAuthenticatedProvider.future); return await fetch(0, 20); } Future addFavorite(List tracks) async { - await (await metadataPlugin).track.save(tracks.map((e) => e.id).toList()); + if (state.value == null) { + return; + } - ref.invalidateSelf(); + final oldState = state.value; + state = AsyncData( + state.value!.copyWith( + items: [ + ...tracks.whereType(), + ...state.value!.items + ], + ), + ); + + try { + await (await metadataPlugin).track.save(tracks.map((e) => e.id).toList()); + } catch (e) { + state = AsyncData(oldState!); + rethrow; + } } Future removeFavorite(List tracks) async { - await (await metadataPlugin).track.unsave(tracks.map((e) => e.id).toList()); + if (state.value == null) { + return; + } - ref.invalidateSelf(); + final oldState = state.value; + state = AsyncData( + state.value!.copyWith( + items: state.value!.items + .where( + (savedTrack) => !tracks.any((track) => track.id == savedTrack.id), + ) + .toList(), + ), + ); + + try { + await (await metadataPlugin) + .track + .unsave(tracks.map((e) => e.id).toList()); + } catch (e) { + state = AsyncData(oldState!); + rethrow; + } } } @@ -48,9 +85,11 @@ final metadataPluginSavedTracksProvider = AutoDisposeAsyncNotifierProvider< final metadataPluginIsSavedTrackProvider = FutureProvider.autoDispose.family( (ref, trackId) async { - await ref.watch(metadataPluginSavedTracksProvider.future); - final allSavedTracks = - await ref.read(metadataPluginSavedTracksProvider.notifier).fetchAll(); + final savedTracks = + await ref.watch(metadataPluginSavedTracksProvider.future); + final allSavedTracks = savedTracks.hasMore + ? await ref.read(metadataPluginSavedTracksProvider.notifier).fetchAll() + : savedTracks.items; return allSavedTracks.any((track) => track.id == trackId); },