From 6d4d82d6a349cafdc66608f5d37490b9f4f9d705 Mon Sep 17 00:00:00 2001 From: Psychotoxical <171614930+Psychotoxical@users.noreply.github.com> Date: Sat, 27 Jun 2026 15:24:28 +0200 Subject: [PATCH] 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 --- CHANGELOG.md | 6 +++ src/hooks/usePlaylistBulkPlayCallbacks.ts | 15 +++--- src/pages/PlaylistDetail.tsx | 2 +- src/utils/playlist/playPlaylistById.ts | 4 +- .../playlist/playlistBulkPlayActions.test.ts | 49 +++++++++++++++++++ src/utils/playlist/playlistBulkPlayActions.ts | 14 +++--- 6 files changed, 71 insertions(+), 19 deletions(-) create mode 100644 src/utils/playlist/playlistBulkPlayActions.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b0984bf..b64e7448 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/hooks/usePlaylistBulkPlayCallbacks.ts b/src/hooks/usePlaylistBulkPlayCallbacks.ts index b6bad8c4..ec3551f3 100644 --- a/src/hooks/usePlaylistBulkPlayCallbacks.ts +++ b/src/hooks/usePlaylistBulkPlayCallbacks.ts @@ -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 }; diff --git a/src/pages/PlaylistDetail.tsx b/src/pages/PlaylistDetail.tsx index 04df8f18..2c15a536 100644 --- a/src/pages/PlaylistDetail.tsx +++ b/src/pages/PlaylistDetail.tsx @@ -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 ──────────────────────────────────────────────────── diff --git a/src/utils/playlist/playPlaylistById.ts b/src/utils/playlist/playPlaylistById.ts index f013e873..3bb13dea 100644 --- a/src/utils/playlist/playPlaylistById.ts +++ b/src/utils/playlist/playPlaylistById.ts @@ -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 { 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 }); } diff --git a/src/utils/playlist/playlistBulkPlayActions.test.ts b/src/utils/playlist/playlistBulkPlayActions.test.ts new file mode 100644 index 00000000..ecc9ab41 --- /dev/null +++ b/src/utils/playlist/playlistBulkPlayActions.test.ts @@ -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(); + }); +}); diff --git a/src/utils/playlist/playlistBulkPlayActions.ts b/src/utils/playlist/playlistBulkPlayActions.ts index cb592e54..b62d4a02 100644 --- a/src/utils/playlist/playlistBulkPlayActions.ts +++ b/src/utils/playlist/playlistBulkPlayActions.ts @@ -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); }