feat(queue): preserve Play Next order toggle (#464)

* feat(queue): add preservePlayNextOrder setting + playNext store action

- New Track.playNextAdded flag (analogous to autoAdded / radioAdded).
  Stale flags behind queueIndex are harmless — only forward streak scan.
- New playerStore action playNext(tracks): tags incoming tracks and
  delegates to enqueueAt for unified undo + server sync.
- New authStore boolean preservePlayNextOrder (default false). When on,
  playNext appends behind the existing Play-Next streak (Spotify-style)
  instead of inserting directly after the current track.

* refactor(context-menu): centralise Play Next; add Settings toggle + i18n

- Replace 3 inline splice/enqueueAt call sites in ContextMenu with the
  new playNext action. Side-benefit: the single-song path now goes
  through enqueueAt and gets undo + queue sync (previously missing).
- Settings → Audio → Playback: new toggle below Gapless.
- 8 locales: preservePlayNextOrder + preservePlayNextOrderDesc.

* docs(contributors): credit + changelog entry for #464
This commit is contained in:
Frank Stellmacher
2026-05-05 22:33:15 +02:00
committed by GitHub
parent e1f2cb4c37
commit 0fab2849e5
13 changed files with 76 additions and 27 deletions
+5 -27
View File
@@ -1019,12 +1019,13 @@ function MultiPlaylistToPlaylistSubmenu({ playlists, onDone, triggerId }: { play
export default function ContextMenu() {
const { t } = useTranslation();
const orbitRole = useOrbitStore(s => s.role);
const { contextMenu, closeContextMenu, playTrack, enqueue, queue, currentTrack, removeTrack, lastfmLovedCache, setLastfmLovedForSong, starredOverrides, setStarredOverride, openSongInfo, userRatingOverrides, setUserRatingOverride } = usePlayerStore(
const { contextMenu, closeContextMenu, playTrack, enqueue, playNext, queue, currentTrack, removeTrack, lastfmLovedCache, setLastfmLovedForSong, starredOverrides, setStarredOverride, openSongInfo, userRatingOverrides, setUserRatingOverride } = usePlayerStore(
useShallow(s => ({
contextMenu: s.contextMenu,
closeContextMenu: s.closeContextMenu,
playTrack: s.playTrack,
enqueue: s.enqueue,
playNext: s.playNext,
queue: s.queue,
currentTrack: s.currentTrack,
removeTrack: s.removeTrack,
@@ -1528,16 +1529,7 @@ export default function ContextMenu() {
<div className="context-menu-item" onClick={() => handleAction(() => playTrack(song, [song]))}>
<Play size={14} /> {t('contextMenu.playNow')}
</div>
<div className="context-menu-item" onClick={() => handleAction(() => {
if (!currentTrack) {
playTrack(song, [song]);
return;
}
const currentIdx = usePlayerStore.getState().queueIndex;
const newQueue = [...queue];
newQueue.splice(currentIdx + 1, 0, song);
usePlayerStore.setState({ queue: newQueue });
})}>
<div className="context-menu-item" onClick={() => handleAction(() => playNext([song]))}>
<ChevronsRight size={14} /> {t('contextMenu.playNext')}
</div>
<div className="context-menu-item" onClick={() => handleAction(() => enqueue([song]))}>
@@ -1691,16 +1683,7 @@ export default function ContextMenu() {
<div className="context-menu-item" onClick={() => handleAction(() => playTrack(song, [song]))}>
<Play size={14} /> {t('contextMenu.playNow')}
</div>
<div className="context-menu-item" onClick={() => handleAction(() => {
if (!currentTrack) {
playTrack(song, [song]);
return;
}
const currentIdx = usePlayerStore.getState().queueIndex;
const newQueue = [...queue];
newQueue.splice(currentIdx + 1, 0, song);
usePlayerStore.setState({ queue: newQueue });
})}>
<div className="context-menu-item" onClick={() => handleAction(() => playNext([song]))}>
<ChevronsRight size={14} /> {t('contextMenu.playNext')}
</div>
<div className="context-menu-item" onClick={() => handleAction(() => enqueue([song]))}>
@@ -1830,12 +1813,7 @@ export default function ContextMenu() {
const albumData = await getAlbum(album.id);
const tracks = albumData.songs.map(songToTrack);
if (tracks.length === 0) return;
if (!currentTrack) {
playTrack(tracks[0], tracks);
return;
}
const currentIdx = usePlayerStore.getState().queueIndex;
usePlayerStore.getState().enqueueAt(tracks, currentIdx + 1);
playNext(tracks);
})}>
<ChevronsRight size={14} /> {t('contextMenu.playNext')}
</div>