mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-14 16:25:16 +00:00
feat: add or remove track, playlist or album to queue support
This commit is contained in:
parent
9bad8c9eb8
commit
b8f3493138
@ -25,6 +25,8 @@ abstract class SpotubeIcons {
|
|||||||
static const heart = FeatherIcons.heart;
|
static const heart = FeatherIcons.heart;
|
||||||
static const heartFilled = Icons.favorite_rounded;
|
static const heartFilled = Icons.favorite_rounded;
|
||||||
static const queue = Icons.queue_music_rounded;
|
static const queue = Icons.queue_music_rounded;
|
||||||
|
static const queueAdd = Icons.add_to_photos_outlined;
|
||||||
|
static const queueRemove = Icons.remove_outlined;
|
||||||
static const download = FeatherIcons.download;
|
static const download = FeatherIcons.download;
|
||||||
static const done = FeatherIcons.checkCircle;
|
static const done = FeatherIcons.checkCircle;
|
||||||
static const alternativeRoute = Icons.alt_route_rounded;
|
static const alternativeRoute = Icons.alt_route_rounded;
|
||||||
|
@ -1,13 +1,16 @@
|
|||||||
|
import 'package:fl_query/fl_query.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.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/components/shared/playbutton_card.dart';
|
import 'package:spotube/components/shared/playbutton_card.dart';
|
||||||
import 'package:spotube/hooks/use_breakpoint_value.dart';
|
import 'package:spotube/hooks/use_breakpoint_value.dart';
|
||||||
import 'package:spotube/provider/spotify_provider.dart';
|
|
||||||
import 'package:spotube/provider/playlist_queue_provider.dart';
|
import 'package:spotube/provider/playlist_queue_provider.dart';
|
||||||
|
import 'package:spotube/provider/spotify_provider.dart';
|
||||||
|
import 'package:spotube/services/queries/queries.dart';
|
||||||
import 'package:spotube/utils/service_utils.dart';
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||||
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
class AlbumCard extends HookConsumerWidget {
|
class AlbumCard extends HookConsumerWidget {
|
||||||
final Album album;
|
final Album album;
|
||||||
@ -23,7 +26,11 @@ class AlbumCard extends HookConsumerWidget {
|
|||||||
final playlist = ref.watch(PlaylistQueueNotifier.provider);
|
final playlist = ref.watch(PlaylistQueueNotifier.provider);
|
||||||
final playing = useStream(PlaylistQueueNotifier.playing).data ?? false;
|
final playing = useStream(PlaylistQueueNotifier.playing).data ?? false;
|
||||||
final playlistNotifier = ref.watch(PlaylistQueueNotifier.notifier);
|
final playlistNotifier = ref.watch(PlaylistQueueNotifier.notifier);
|
||||||
bool isPlaylistPlaying = playlistNotifier.isPlayingPlaylist(album.tracks!);
|
final queryBowl = QueryBowl.of(context);
|
||||||
|
final query = queryBowl.getQuery<List<TrackSimple>, SpotifyApi>(
|
||||||
|
Queries.album.tracksOf(album.id!).queryKey);
|
||||||
|
final tracks = useState(query?.data ?? album.tracks ?? <Track>[]);
|
||||||
|
bool isPlaylistPlaying = playlistNotifier.isPlayingPlaylist(tracks.value);
|
||||||
final int marginH =
|
final int marginH =
|
||||||
useBreakpointValue(sm: 10, md: 15, lg: 20, xl: 20, xxl: 20);
|
useBreakpointValue(sm: 10, md: 15, lg: 20, xl: 20, xxl: 20);
|
||||||
return PlaybuttonCard(
|
return PlaybuttonCard(
|
||||||
@ -42,20 +49,43 @@ class AlbumCard extends HookConsumerWidget {
|
|||||||
ServiceUtils.navigate(context, "/album/${album.id}", extra: album);
|
ServiceUtils.navigate(context, "/album/${album.id}", extra: album);
|
||||||
},
|
},
|
||||||
onPlaybuttonPressed: () async {
|
onPlaybuttonPressed: () async {
|
||||||
SpotifyApi spotify = ref.read(spotifyProvider);
|
|
||||||
if (isPlaylistPlaying && playing) {
|
if (isPlaylistPlaying && playing) {
|
||||||
return playlistNotifier.pause();
|
return playlistNotifier.pause();
|
||||||
} else if (isPlaylistPlaying && !playing) {
|
} else if (isPlaylistPlaying && !playing) {
|
||||||
return playlistNotifier.resume();
|
return playlistNotifier.resume();
|
||||||
}
|
}
|
||||||
List<Track> tracks = (await spotify.albums.getTracks(album.id!).all())
|
|
||||||
.map((track) =>
|
|
||||||
TypeConversionUtils.simpleTrack_X_Track(track, album))
|
|
||||||
.toList();
|
|
||||||
if (tracks.isEmpty) return;
|
|
||||||
|
|
||||||
await playlistNotifier.loadAndPlay(tracks);
|
await playlistNotifier.loadAndPlay(album.tracks
|
||||||
|
?.map(
|
||||||
|
(e) => TypeConversionUtils.simpleTrack_X_Track(e, album))
|
||||||
|
.toList() ??
|
||||||
|
[]);
|
||||||
},
|
},
|
||||||
|
onAddToQueuePressed: () async {
|
||||||
|
if (isPlaylistPlaying) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final fetchedTracks =
|
||||||
|
await queryBowl.fetchQuery<List<TrackSimple>, SpotifyApi>(
|
||||||
|
Queries.album.tracksOf(album.id!),
|
||||||
|
externalData: ref.read(spotifyProvider),
|
||||||
|
key: ValueKey(const Uuid().v4()),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (fetchedTracks == null || fetchedTracks.isEmpty) return;
|
||||||
|
|
||||||
|
playlistNotifier.add(
|
||||||
|
fetchedTracks
|
||||||
|
.map((e) => TypeConversionUtils.simpleTrack_X_Track(e, album))
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
tracks.value = fetchedTracks;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text("Added ${album.tracks?.length} tracks to queue"),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -70,6 +70,25 @@ class PlaylistCard extends HookConsumerWidget {
|
|||||||
await playlistNotifier.loadAndPlay(fetchedTracks);
|
await playlistNotifier.loadAndPlay(fetchedTracks);
|
||||||
tracks.value = fetchedTracks;
|
tracks.value = fetchedTracks;
|
||||||
},
|
},
|
||||||
|
onAddToQueuePressed: () async {
|
||||||
|
if (isPlaylistPlaying) return;
|
||||||
|
List<Track> fetchedTracks = await queryBowl.fetchQuery(
|
||||||
|
key: ValueKey(const Uuid().v4()),
|
||||||
|
Queries.playlist.tracksOf(playlist.id!),
|
||||||
|
externalData: ref.read(spotifyProvider),
|
||||||
|
) ??
|
||||||
|
[];
|
||||||
|
|
||||||
|
if (fetchedTracks.isEmpty) return;
|
||||||
|
|
||||||
|
playlistNotifier.add(fetchedTracks);
|
||||||
|
tracks.value = fetchedTracks;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text("Added ${fetchedTracks.length} tracks to queue"),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,7 @@ enum PlaybuttonCardViewType { square, list }
|
|||||||
class PlaybuttonCard extends HookWidget {
|
class PlaybuttonCard extends HookWidget {
|
||||||
final void Function()? onTap;
|
final void Function()? onTap;
|
||||||
final void Function()? onPlaybuttonPressed;
|
final void Function()? onPlaybuttonPressed;
|
||||||
|
final void Function()? onAddToQueuePressed;
|
||||||
final String? description;
|
final String? description;
|
||||||
final EdgeInsetsGeometry? margin;
|
final EdgeInsetsGeometry? margin;
|
||||||
final String imageUrl;
|
final String imageUrl;
|
||||||
@ -29,6 +30,7 @@ class PlaybuttonCard extends HookWidget {
|
|||||||
this.margin,
|
this.margin,
|
||||||
this.description,
|
this.description,
|
||||||
this.onPlaybuttonPressed,
|
this.onPlaybuttonPressed,
|
||||||
|
this.onAddToQueuePressed,
|
||||||
this.onTap,
|
this.onTap,
|
||||||
this.viewType = PlaybuttonCardViewType.square,
|
this.viewType = PlaybuttonCardViewType.square,
|
||||||
Key? key,
|
Key? key,
|
||||||
@ -101,6 +103,15 @@ class PlaybuttonCard extends HookWidget {
|
|||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
final addToQueueButton = PlatformIconButton(
|
||||||
|
onPressed: onAddToQueuePressed,
|
||||||
|
backgroundColor:
|
||||||
|
PlatformTheme.of(context).secondaryBackgroundColor,
|
||||||
|
hoverColor: PlatformTheme.of(context)
|
||||||
|
.secondaryBackgroundColor
|
||||||
|
?.withOpacity(0.5),
|
||||||
|
icon: const Icon(SpotubeIcons.queueAdd),
|
||||||
|
);
|
||||||
final image = Padding(
|
final image = Padding(
|
||||||
padding: EdgeInsets.all(
|
padding: EdgeInsets.all(
|
||||||
platform == TargetPlatform.windows ? 5 : 0,
|
platform == TargetPlatform.windows ? 5 : 0,
|
||||||
@ -131,7 +142,16 @@ class PlaybuttonCard extends HookWidget {
|
|||||||
textDirection: TextDirection.ltr,
|
textDirection: TextDirection.ltr,
|
||||||
bottom: 10,
|
bottom: 10,
|
||||||
end: 5,
|
end: 5,
|
||||||
child: playButton,
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
if (!isPlaying) addToQueueButton,
|
||||||
|
if (platform != TargetPlatform.linux)
|
||||||
|
const SizedBox(width: 5),
|
||||||
|
playButton,
|
||||||
|
],
|
||||||
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -192,6 +212,8 @@ class PlaybuttonCard extends HookWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
|
addToQueueButton,
|
||||||
|
const SizedBox(width: 10),
|
||||||
playButton,
|
playButton,
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
],
|
],
|
||||||
|
@ -31,6 +31,7 @@ class TrackCollectionView<T> extends HookConsumerWidget {
|
|||||||
final String titleImage;
|
final String titleImage;
|
||||||
final bool isPlaying;
|
final bool isPlaying;
|
||||||
final void Function([Track? currentTrack]) onPlay;
|
final void Function([Track? currentTrack]) onPlay;
|
||||||
|
final void Function() onAddToQueue;
|
||||||
final void Function([Track? currentTrack]) onShuffledPlay;
|
final void Function([Track? currentTrack]) onShuffledPlay;
|
||||||
final void Function() onShare;
|
final void Function() onShare;
|
||||||
final Widget? heartBtn;
|
final Widget? heartBtn;
|
||||||
@ -49,6 +50,7 @@ class TrackCollectionView<T> extends HookConsumerWidget {
|
|||||||
required this.isPlaying,
|
required this.isPlaying,
|
||||||
required this.onPlay,
|
required this.onPlay,
|
||||||
required this.onShuffledPlay,
|
required this.onShuffledPlay,
|
||||||
|
required this.onAddToQueue,
|
||||||
required this.onShare,
|
required this.onShare,
|
||||||
required this.routePath,
|
required this.routePath,
|
||||||
this.heartBtn,
|
this.heartBtn,
|
||||||
@ -87,14 +89,20 @@ class TrackCollectionView<T> extends HookConsumerWidget {
|
|||||||
onPressed: onShuffledPlay,
|
onPressed: onShuffledPlay,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 5),
|
const SizedBox(width: 5),
|
||||||
|
// add to queue playlist
|
||||||
|
if (!isPlaying)
|
||||||
|
PlatformIconButton(
|
||||||
|
onPressed: tracksSnapshot.data != null ? onAddToQueue : null,
|
||||||
|
icon: Icon(
|
||||||
|
SpotubeIcons.queueAdd,
|
||||||
|
color: color?.titleTextColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
// play playlist
|
// play playlist
|
||||||
PlatformIconButton(
|
PlatformIconButton(
|
||||||
backgroundColor: PlatformTheme.of(context).primaryColor,
|
backgroundColor: PlatformTheme.of(context).primaryColor,
|
||||||
onPressed: tracksSnapshot.data != null ? onPlay : null,
|
onPressed: tracksSnapshot.data != null ? onPlay : null,
|
||||||
icon: Icon(
|
icon: Icon(isPlaying ? SpotubeIcons.stop : SpotubeIcons.play),
|
||||||
isPlaying ? SpotubeIcons.stop : SpotubeIcons.play,
|
|
||||||
color: PlatformTextTheme.of(context).body?.color,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
];
|
];
|
||||||
|
@ -74,6 +74,8 @@ class TrackTile extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
final auth = ref.watch(authProvider);
|
final auth = ref.watch(authProvider);
|
||||||
final spotify = ref.watch(spotifyProvider);
|
final spotify = ref.watch(spotifyProvider);
|
||||||
|
final playlistQueueNotifier = ref.watch(PlaylistQueueNotifier.notifier);
|
||||||
|
|
||||||
final removingTrack = useState<String?>(null);
|
final removingTrack = useState<String?>(null);
|
||||||
final removeTrack = useMutation<bool, Tuple2<SpotifyApi, String>>(
|
final removeTrack = useMutation<bool, Tuple2<SpotifyApi, String>>(
|
||||||
job: Mutations.playlist.removeTrackOf(playlistId ?? ""),
|
job: Mutations.playlist.removeTrackOf(playlistId ?? ""),
|
||||||
@ -319,6 +321,34 @@ class TrackTile extends HookConsumerWidget {
|
|||||||
if (!isLocal)
|
if (!isLocal)
|
||||||
AdaptiveActions(
|
AdaptiveActions(
|
||||||
actions: [
|
actions: [
|
||||||
|
if (!playlistQueueNotifier.isTrackOnQueue(track.value))
|
||||||
|
Action(
|
||||||
|
icon: const Icon(SpotubeIcons.queueAdd),
|
||||||
|
text: const PlatformText("Add to queue"),
|
||||||
|
onPressed: () {
|
||||||
|
playlistQueueNotifier.add([track.value]);
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: PlatformText(
|
||||||
|
"Added ${track.value.name} to queue"),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Action(
|
||||||
|
icon: const Icon(SpotubeIcons.queueRemove),
|
||||||
|
text: const PlatformText("Remove from queue"),
|
||||||
|
onPressed: () {
|
||||||
|
playlistQueueNotifier.remove([track.value]);
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: PlatformText(
|
||||||
|
"Removed ${track.value.name} from queue"),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
if (toggler.item3.hasData)
|
if (toggler.item3.hasData)
|
||||||
Action(
|
Action(
|
||||||
icon: toggler.item1
|
icon: toggler.item1
|
||||||
@ -334,7 +364,7 @@ class TrackTile extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
if (auth.isLoggedIn)
|
if (auth.isLoggedIn)
|
||||||
Action(
|
Action(
|
||||||
icon: const Icon(SpotubeIcons.addFilled),
|
icon: const Icon(SpotubeIcons.playlistAdd),
|
||||||
text: const PlatformText("Add To playlist"),
|
text: const PlatformText("Add To playlist"),
|
||||||
onPressed: actionAddToPlaylist,
|
onPressed: actionAddToPlaylist,
|
||||||
),
|
),
|
||||||
|
@ -98,10 +98,25 @@ class AlbumPage extends HookConsumerWidget {
|
|||||||
ref,
|
ref,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
playback.stop();
|
playback.remove(
|
||||||
|
tracksSnapshot.data!
|
||||||
|
.map((track) =>
|
||||||
|
TypeConversionUtils.simpleTrack_X_Track(track, album))
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onAddToQueue: () {
|
||||||
|
if (tracksSnapshot.hasData && !isAlbumPlaying) {
|
||||||
|
playback.add(
|
||||||
|
tracksSnapshot.data!
|
||||||
|
.map((track) =>
|
||||||
|
TypeConversionUtils.simpleTrack_X_Track(track, album))
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
onShare: () {
|
onShare: () {
|
||||||
Clipboard.setData(
|
Clipboard.setData(
|
||||||
ClipboardData(text: "https://open.spotify.com/album/${album.id}"),
|
ClipboardData(text: "https://open.spotify.com/album/${album.id}"),
|
||||||
|
@ -323,6 +323,27 @@ class ArtistPage extends HookConsumerWidget {
|
|||||||
style:
|
style:
|
||||||
PlatformTheme.of(context).textTheme?.headline,
|
PlatformTheme.of(context).textTheme?.headline,
|
||||||
),
|
),
|
||||||
|
if (!isPlaylistPlaying)
|
||||||
|
PlatformIconButton(
|
||||||
|
icon: const Icon(
|
||||||
|
SpotubeIcons.queueAdd,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
playlistNotifier.add(topTracks.toList());
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
width: 300,
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
content: PlatformText(
|
||||||
|
"Added ${topTracks.length} tracks to queue",
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (platform != TargetPlatform.linux)
|
||||||
|
const SizedBox(width: 5),
|
||||||
PlatformIconButton(
|
PlatformIconButton(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
isPlaylistPlaying
|
isPlaylistPlaying
|
||||||
|
@ -92,10 +92,15 @@ class PlaylistView extends HookConsumerWidget {
|
|||||||
currentTrack: track,
|
currentTrack: track,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
playlistNotifier.stop();
|
playlistNotifier.remove(tracksSnapshot.data!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onAddToQueue: () {
|
||||||
|
if (tracksSnapshot.hasData && !isPlaylistPlaying) {
|
||||||
|
playlistNotifier.add(tracksSnapshot.data!);
|
||||||
|
}
|
||||||
|
},
|
||||||
bottomSpace: breakpoint.isLessThanOrEqualTo(Breakpoints.md),
|
bottomSpace: breakpoint.isLessThanOrEqualTo(Breakpoints.md),
|
||||||
showShare: playlist.id != "user-liked-tracks",
|
showShare: playlist.id != "user-liked-tracks",
|
||||||
routePath: "/playlist/${playlist.id}",
|
routePath: "/playlist/${playlist.id}",
|
||||||
|
@ -233,16 +233,38 @@ class PlaylistQueueNotifier extends PersistedStateNotifier<PlaylistQueue?> {
|
|||||||
: [];
|
: [];
|
||||||
|
|
||||||
// modifiers
|
// modifiers
|
||||||
void add(Track track) {
|
void add(List<Track> tracks) {
|
||||||
|
if (!isLoaded) {
|
||||||
|
loadAndPlay(tracks);
|
||||||
|
} else {
|
||||||
state = state?.copyWith(
|
state = state?.copyWith(
|
||||||
tracks: state!.tracks..add(track),
|
tracks: state!.tracks..addAll(tracks),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void remove(Track track) {
|
void remove(List<Track> tracks) {
|
||||||
state = state?.copyWith(
|
if (!isLoaded) return;
|
||||||
tracks: state!.tracks..remove(track),
|
final trackIds = tracks.map((e) => e.id!).toSet();
|
||||||
|
final newTracks = state!.tracks.whereNot(
|
||||||
|
(element) => trackIds.contains(element.id),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (newTracks.isEmpty) {
|
||||||
|
stop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state = state?.copyWith(
|
||||||
|
tracks: newTracks.toSet(),
|
||||||
|
active: !newTracks.contains(state!.activeTrack)
|
||||||
|
? state!.active > newTracks.length - 1
|
||||||
|
? newTracks.length - 1
|
||||||
|
: state!.active
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
if (state!.isLoading) {
|
||||||
|
play();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void shuffle() {
|
void shuffle() {
|
||||||
@ -453,6 +475,16 @@ class PlaylistQueueNotifier extends PersistedStateNotifier<PlaylistQueue?> {
|
|||||||
.every((track) => trackIds.contains(track.id!));
|
.every((track) => trackIds.contains(track.id!));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool isTrackOnQueue(TrackSimple track) {
|
||||||
|
if (!isLoaded) return false;
|
||||||
|
if (state!.isShuffled) {
|
||||||
|
final trackIds = state!.tempTracks.map((track) => track.id!);
|
||||||
|
return trackIds.contains(track.id!);
|
||||||
|
}
|
||||||
|
final trackIds = state!.tracks.map((track) => track.id!);
|
||||||
|
return trackIds.contains(track.id!);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<PlaylistQueue>? fromJson(Map<String, dynamic> json) {
|
Future<PlaylistQueue>? fromJson(Map<String, dynamic> json) {
|
||||||
if (json.isEmpty) return null;
|
if (json.isEmpty) return null;
|
||||||
|
Loading…
Reference in New Issue
Block a user