fix(playlist): stop the detail page reloading on Play/Shuffle/Enqueue (#1201)

* fix(playlist): stop the detail page reloading on Play/Shuffle/Enqueue

* docs(changelog): playlist play no longer reloads the page
This commit is contained in:
Psychotoxical
2026-06-27 15:24:28 +02:00
committed by GitHub
parent 7e6a2100e5
commit 6d4d82d6a3
6 changed files with 71 additions and 19 deletions
+6
View File
@@ -357,6 +357,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* On Windows, the "update available" notice now waits until a release is a couple of days old, so it no longer points to a version that WinGet has not finished publishing yet. macOS and Linux are unaffected.
### Playlist no longer reloads when you press Play
**By [@Psychotoxical](https://github.com/Psychotoxical), PR [#1201](https://github.com/Psychotoxical/psysonic/pull/1201)**
* Pressing Play, Shuffle or Add to queue on a playlist no longer reloads the whole page with a spinner — it just starts playback. Editing the playlist (adding or removing songs) still refreshes the list as before.
## [1.48.1] - 2026-06-15
## Fixed
+7 -8
View File
@@ -6,7 +6,6 @@ export interface PlaylistBulkPlayCallbacksDeps {
songsLength: number;
id: string | undefined;
tracks: Track[];
touchPlaylist: (id: string) => void;
playTrack: (track: Track, queue: Track[]) => void;
enqueue: (tracks: Track[]) => void;
}
@@ -18,21 +17,21 @@ export interface PlaylistBulkPlayCallbacks {
}
export function usePlaylistBulkPlayCallbacks(deps: PlaylistBulkPlayCallbacksDeps): PlaylistBulkPlayCallbacks {
const { songsLength, id, tracks, touchPlaylist, playTrack, enqueue } = deps;
const { songsLength, id, tracks, playTrack, enqueue } = deps;
const handlePlayAll = useCallback(
() => playPlaylistAll({ songsLength, id, tracks, touchPlaylist, playTrack, enqueue }),
[songsLength, id, tracks, touchPlaylist, playTrack, enqueue],
() => playPlaylistAll({ songsLength, id, tracks, playTrack, enqueue }),
[songsLength, id, tracks, playTrack, enqueue],
);
const handleShuffleAll = useCallback(
() => shufflePlaylistAll({ songsLength, id, tracks, touchPlaylist, playTrack, enqueue }),
[songsLength, id, tracks, touchPlaylist, playTrack, enqueue],
() => shufflePlaylistAll({ songsLength, id, tracks, playTrack, enqueue }),
[songsLength, id, tracks, playTrack, enqueue],
);
const handleEnqueueAll = useCallback(
() => enqueuePlaylistAll({ songsLength, id, tracks, touchPlaylist, playTrack, enqueue }),
[songsLength, id, tracks, touchPlaylist, playTrack, enqueue],
() => enqueuePlaylistAll({ songsLength, id, tracks, playTrack, enqueue }),
[songsLength, id, tracks, playTrack, enqueue],
);
return { handlePlayAll, handleShuffleAll, handleEnqueueAll };
+1 -1
View File
@@ -223,7 +223,7 @@ export default function PlaylistDetail() {
// ── Playback actions (encapsulated like AlbumHeader) ─────────
const { handlePlayAll, handleShuffleAll, handleEnqueueAll } = usePlaylistBulkPlayCallbacks({
songsLength: songs.length, id, tracks, touchPlaylist, playTrack, enqueue,
songsLength: songs.length, id, tracks, playTrack, enqueue,
});
// ── Render ────────────────────────────────────────────────────
+1 -3
View File
@@ -1,7 +1,6 @@
import { getPlaylist } from '../../api/subsonicPlaylists';
import { songToTrack } from '../playback/songToTrack';
import { usePlayerStore } from '../../store/playerStore';
import { usePlaylistStore } from '../../store/playlistStore';
import { playPlaylistAll } from './playlistBulkPlayActions';
/**
@@ -16,6 +15,5 @@ export async function playPlaylistById(id: string): Promise<void> {
const { songs } = await getPlaylist(id);
const tracks = songs.map(songToTrack);
const { playTrack, enqueue } = usePlayerStore.getState();
const { touchPlaylist } = usePlaylistStore.getState();
playPlaylistAll({ songsLength: tracks.length, id, tracks, touchPlaylist, playTrack, enqueue });
playPlaylistAll({ songsLength: tracks.length, id, tracks, playTrack, enqueue });
}
@@ -0,0 +1,49 @@
import { describe, expect, it, vi } from 'vitest';
import { enqueuePlaylistAll, playPlaylistAll, shufflePlaylistAll } from './playlistBulkPlayActions';
import type { Track } from '../../store/playerStoreTypes';
// Only id/queue identity matters for these actions.
const tracks = [{ id: 'a' }, { id: 'b' }, { id: 'c' }] as unknown as Track[];
describe('playlistBulkPlayActions', () => {
it('playPlaylistAll starts the first track with the full queue', () => {
const playTrack = vi.fn();
const enqueue = vi.fn();
playPlaylistAll({ songsLength: tracks.length, id: 'p1', tracks, playTrack, enqueue });
expect(playTrack).toHaveBeenCalledWith(tracks[0], tracks);
expect(enqueue).not.toHaveBeenCalled();
});
it('enqueuePlaylistAll appends every track without starting playback', () => {
const playTrack = vi.fn();
const enqueue = vi.fn();
enqueuePlaylistAll({ songsLength: tracks.length, id: 'p1', tracks, playTrack, enqueue });
expect(enqueue).toHaveBeenCalledWith(tracks);
expect(playTrack).not.toHaveBeenCalled();
});
it('shufflePlaylistAll plays a track from the playlist with the full queue', () => {
const playTrack = vi.fn();
shufflePlaylistAll({ songsLength: tracks.length, id: 'p1', tracks, playTrack, enqueue: vi.fn() });
expect(playTrack).toHaveBeenCalledTimes(1);
const [first, queue] = playTrack.mock.calls[0];
expect(tracks).toContain(first);
expect(queue).toHaveLength(tracks.length);
});
it('no-ops on an empty playlist', () => {
const playTrack = vi.fn();
const enqueue = vi.fn();
playPlaylistAll({ songsLength: 0, id: 'p1', tracks: [], playTrack, enqueue });
shufflePlaylistAll({ songsLength: 0, id: 'p1', tracks: [], playTrack, enqueue });
enqueuePlaylistAll({ songsLength: 0, id: 'p1', tracks: [], playTrack, enqueue });
expect(playTrack).not.toHaveBeenCalled();
expect(enqueue).not.toHaveBeenCalled();
});
it('no-ops without a playlist id', () => {
const playTrack = vi.fn();
playPlaylistAll({ songsLength: tracks.length, id: undefined, tracks, playTrack, enqueue: vi.fn() });
expect(playTrack).not.toHaveBeenCalled();
});
});
@@ -1,25 +1,26 @@
import type { Track } from '../../store/playerStoreTypes';
// No `touchPlaylist` here: playing/shuffling/enqueuing does not modify the
// playlist. Touching it bumps `lastModified`, which is the playlist detail
// page's load-effect trigger, so it would re-fetch and flash the whole
// container on every Play click. Real mutations (add/remove/save) still touch.
export interface BulkPlayDeps {
songsLength: number;
id: string | undefined;
tracks: Track[];
touchPlaylist: (id: string) => void;
playTrack: (track: Track, queue: Track[]) => void;
enqueue: (tracks: Track[]) => void;
}
export function playPlaylistAll(deps: BulkPlayDeps): void {
const { songsLength, id, tracks, touchPlaylist, playTrack } = deps;
const { songsLength, id, tracks, playTrack } = deps;
if (!songsLength || !id) return;
touchPlaylist(id);
playTrack(tracks[0], tracks);
}
export function shufflePlaylistAll(deps: BulkPlayDeps): void {
const { songsLength, id, tracks, touchPlaylist, playTrack } = deps;
const { songsLength, id, tracks, playTrack } = deps;
if (!songsLength || !id) return;
touchPlaylist(id);
const shuffled = [...tracks];
for (let i = shuffled.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
@@ -29,8 +30,7 @@ export function shufflePlaylistAll(deps: BulkPlayDeps): void {
}
export function enqueuePlaylistAll(deps: BulkPlayDeps): void {
const { songsLength, id, tracks, touchPlaylist, enqueue } = deps;
const { songsLength, id, tracks, enqueue } = deps;
if (!songsLength || !id) return;
touchPlaylist(id);
enqueue(tracks);
}