feat(playlists): system playlists default to shuffle on tile play
Closes Fable #412 (For You force-refresh on play) and #413 (shuffle-on-play default for system playlists). Web (PlaylistCard.svelte + player/store.svelte.ts): - Drop the refreshForYou() call from the play handler. The daily 03:00 user-local snapshot is what plays now. Stops burning server compute on every press and stops swapping the playlist out from under the user. - Generalize the kebab affordance to render for any system playlist (was Discover-only). Adds "Refresh For You" as an explicit replacement so users can still force a regen when they want one. - Extend playQueue(tracks, startIndex, { shuffle? }) to Fisher-Yates the queue when shuffle:true. PlaylistCard passes shuffle:true for any non-null system_variant. Flutter (player_provider.dart + playlists/widgets/playlist_card.dart): - playTracks now accepts shuffle:bool. When true, picks a random starting index and enables AudioServiceShuffleMode.all after setQueueFromTracks. PlaylistCard passes shuffle:playlist.isSystem. User playlists keep linear order. Detail-screen play buttons are unchanged for now (follow-up if user requests). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,11 @@
|
||||
|
||||
const isOwn = $derived(user.value?.id === playlist.user_id);
|
||||
const isDiscover = $derived(playlist.system_variant === 'discover');
|
||||
const isForYou = $derived(playlist.system_variant === 'for_you');
|
||||
const isSystem = $derived(playlist.system_variant != null);
|
||||
const refreshLabel = $derived(
|
||||
isDiscover ? 'Refresh Discover' : isForYou ? 'Refresh For You' : 'Refresh',
|
||||
);
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
@@ -40,38 +45,34 @@
|
||||
if (starting) return;
|
||||
starting = true;
|
||||
try {
|
||||
let detailID = playlist.id;
|
||||
// For-You: refresh first so click means "give me a fresh mix
|
||||
// and play it." Atomic replace creates a new playlist row, so
|
||||
// the response's playlist_id is what we follow up with.
|
||||
if (playlist.system_variant === 'for_you') {
|
||||
const refreshed = await refreshForYou();
|
||||
if (!refreshed.playlist_id) {
|
||||
// Empty library — nothing to play.
|
||||
return;
|
||||
}
|
||||
detailID = refreshed.playlist_id;
|
||||
// Invalidate caches so the home + detail views see the new
|
||||
// playlist on their next read.
|
||||
await queryClient.invalidateQueries({ queryKey: qk.playlists() });
|
||||
}
|
||||
const detail = await getPlaylist(detailID);
|
||||
const detail = await getPlaylist(playlist.id);
|
||||
const refs = toTrackRefs(detail.tracks);
|
||||
if (refs.length > 0) {
|
||||
playQueue(refs, 0);
|
||||
// System playlists default to shuffle so replaying within a
|
||||
// day feels different. User playlists keep stored order.
|
||||
// Explicit refresh moved to the kebab menu — playing no
|
||||
// longer rebuilds the snapshot.
|
||||
playQueue(refs, 0, { shuffle: playlist.system_variant != null });
|
||||
}
|
||||
} finally {
|
||||
starting = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function onRefreshDiscover(e: MouseEvent) {
|
||||
async function onRefresh(e: MouseEvent) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
menuOpen = false;
|
||||
try {
|
||||
await refreshDiscover();
|
||||
pushToast('Discover refreshed.');
|
||||
if (isDiscover) {
|
||||
await refreshDiscover();
|
||||
pushToast('Discover refreshed.');
|
||||
} else if (isForYou) {
|
||||
await refreshForYou();
|
||||
pushToast('For You refreshed.');
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
await queryClient.invalidateQueries({ queryKey: qk.playlist(playlist.id) });
|
||||
await queryClient.invalidateQueries({ queryKey: qk.playlists() });
|
||||
} catch (err: unknown) {
|
||||
@@ -83,7 +84,7 @@
|
||||
<svelte:window onclick={() => { menuOpen = false; }} />
|
||||
|
||||
<div class="card relative">
|
||||
{#if isDiscover}
|
||||
{#if isSystem}
|
||||
<div class="kebab-wrap absolute right-1 top-1 z-10">
|
||||
<button
|
||||
type="button"
|
||||
@@ -108,11 +109,11 @@
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
onclick={onRefreshDiscover}
|
||||
onclick={onRefresh}
|
||||
class="flex w-full items-center gap-2 rounded px-2 py-1.5 text-sm text-text-primary hover:bg-surface-hover"
|
||||
>
|
||||
<RefreshCcw size={14} strokeWidth={1} />
|
||||
Refresh Discover
|
||||
{refreshLabel}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user