mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 14:55:43 +00:00
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:
@@ -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.
|
* 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
|
## [1.48.1] - 2026-06-15
|
||||||
|
|
||||||
## Fixed
|
## Fixed
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ export interface PlaylistBulkPlayCallbacksDeps {
|
|||||||
songsLength: number;
|
songsLength: number;
|
||||||
id: string | undefined;
|
id: string | undefined;
|
||||||
tracks: Track[];
|
tracks: Track[];
|
||||||
touchPlaylist: (id: string) => void;
|
|
||||||
playTrack: (track: Track, queue: Track[]) => void;
|
playTrack: (track: Track, queue: Track[]) => void;
|
||||||
enqueue: (tracks: Track[]) => void;
|
enqueue: (tracks: Track[]) => void;
|
||||||
}
|
}
|
||||||
@@ -18,21 +17,21 @@ export interface PlaylistBulkPlayCallbacks {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function usePlaylistBulkPlayCallbacks(deps: PlaylistBulkPlayCallbacksDeps): 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(
|
const handlePlayAll = useCallback(
|
||||||
() => playPlaylistAll({ songsLength, id, tracks, touchPlaylist, playTrack, enqueue }),
|
() => playPlaylistAll({ songsLength, id, tracks, playTrack, enqueue }),
|
||||||
[songsLength, id, tracks, touchPlaylist, playTrack, enqueue],
|
[songsLength, id, tracks, playTrack, enqueue],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleShuffleAll = useCallback(
|
const handleShuffleAll = useCallback(
|
||||||
() => shufflePlaylistAll({ songsLength, id, tracks, touchPlaylist, playTrack, enqueue }),
|
() => shufflePlaylistAll({ songsLength, id, tracks, playTrack, enqueue }),
|
||||||
[songsLength, id, tracks, touchPlaylist, playTrack, enqueue],
|
[songsLength, id, tracks, playTrack, enqueue],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleEnqueueAll = useCallback(
|
const handleEnqueueAll = useCallback(
|
||||||
() => enqueuePlaylistAll({ songsLength, id, tracks, touchPlaylist, playTrack, enqueue }),
|
() => enqueuePlaylistAll({ songsLength, id, tracks, playTrack, enqueue }),
|
||||||
[songsLength, id, tracks, touchPlaylist, playTrack, enqueue],
|
[songsLength, id, tracks, playTrack, enqueue],
|
||||||
);
|
);
|
||||||
|
|
||||||
return { handlePlayAll, handleShuffleAll, handleEnqueueAll };
|
return { handlePlayAll, handleShuffleAll, handleEnqueueAll };
|
||||||
|
|||||||
@@ -223,7 +223,7 @@ export default function PlaylistDetail() {
|
|||||||
|
|
||||||
// ── Playback actions (encapsulated like AlbumHeader) ─────────
|
// ── Playback actions (encapsulated like AlbumHeader) ─────────
|
||||||
const { handlePlayAll, handleShuffleAll, handleEnqueueAll } = usePlaylistBulkPlayCallbacks({
|
const { handlePlayAll, handleShuffleAll, handleEnqueueAll } = usePlaylistBulkPlayCallbacks({
|
||||||
songsLength: songs.length, id, tracks, touchPlaylist, playTrack, enqueue,
|
songsLength: songs.length, id, tracks, playTrack, enqueue,
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Render ────────────────────────────────────────────────────
|
// ── Render ────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { getPlaylist } from '../../api/subsonicPlaylists';
|
import { getPlaylist } from '../../api/subsonicPlaylists';
|
||||||
import { songToTrack } from '../playback/songToTrack';
|
import { songToTrack } from '../playback/songToTrack';
|
||||||
import { usePlayerStore } from '../../store/playerStore';
|
import { usePlayerStore } from '../../store/playerStore';
|
||||||
import { usePlaylistStore } from '../../store/playlistStore';
|
|
||||||
import { playPlaylistAll } from './playlistBulkPlayActions';
|
import { playPlaylistAll } from './playlistBulkPlayActions';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -16,6 +15,5 @@ export async function playPlaylistById(id: string): Promise<void> {
|
|||||||
const { songs } = await getPlaylist(id);
|
const { songs } = await getPlaylist(id);
|
||||||
const tracks = songs.map(songToTrack);
|
const tracks = songs.map(songToTrack);
|
||||||
const { playTrack, enqueue } = usePlayerStore.getState();
|
const { playTrack, enqueue } = usePlayerStore.getState();
|
||||||
const { touchPlaylist } = usePlaylistStore.getState();
|
playPlaylistAll({ songsLength: tracks.length, id, tracks, playTrack, enqueue });
|
||||||
playPlaylistAll({ songsLength: tracks.length, id, tracks, touchPlaylist, 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';
|
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 {
|
export interface BulkPlayDeps {
|
||||||
songsLength: number;
|
songsLength: number;
|
||||||
id: string | undefined;
|
id: string | undefined;
|
||||||
tracks: Track[];
|
tracks: Track[];
|
||||||
touchPlaylist: (id: string) => void;
|
|
||||||
playTrack: (track: Track, queue: Track[]) => void;
|
playTrack: (track: Track, queue: Track[]) => void;
|
||||||
enqueue: (tracks: Track[]) => void;
|
enqueue: (tracks: Track[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function playPlaylistAll(deps: BulkPlayDeps): void {
|
export function playPlaylistAll(deps: BulkPlayDeps): void {
|
||||||
const { songsLength, id, tracks, touchPlaylist, playTrack } = deps;
|
const { songsLength, id, tracks, playTrack } = deps;
|
||||||
if (!songsLength || !id) return;
|
if (!songsLength || !id) return;
|
||||||
touchPlaylist(id);
|
|
||||||
playTrack(tracks[0], tracks);
|
playTrack(tracks[0], tracks);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function shufflePlaylistAll(deps: BulkPlayDeps): void {
|
export function shufflePlaylistAll(deps: BulkPlayDeps): void {
|
||||||
const { songsLength, id, tracks, touchPlaylist, playTrack } = deps;
|
const { songsLength, id, tracks, playTrack } = deps;
|
||||||
if (!songsLength || !id) return;
|
if (!songsLength || !id) return;
|
||||||
touchPlaylist(id);
|
|
||||||
const shuffled = [...tracks];
|
const shuffled = [...tracks];
|
||||||
for (let i = shuffled.length - 1; i > 0; i--) {
|
for (let i = shuffled.length - 1; i > 0; i--) {
|
||||||
const j = Math.floor(Math.random() * (i + 1));
|
const j = Math.floor(Math.random() * (i + 1));
|
||||||
@@ -29,8 +30,7 @@ export function shufflePlaylistAll(deps: BulkPlayDeps): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function enqueuePlaylistAll(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;
|
if (!songsLength || !id) return;
|
||||||
touchPlaylist(id);
|
|
||||||
enqueue(tracks);
|
enqueue(tracks);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user