feat(queue): switchable queue display mode (Queue vs Playlist) (#922)

* feat(queue): add queueDisplayMode setting with rehydrate (default queue)

* feat(queue): render upcoming-only or full timeline by mode, with header toggle

* feat(settings): add queue display mode toggle to Personalisation

* i18n: queue display mode strings across all locales

* test(queue): display-mode rendering and absolute index mapping

* docs: changelog + credits for queue display mode (#922)
This commit is contained in:
Frank Stellmacher
2026-05-30 00:26:07 +02:00
committed by GitHub
parent 7b06be5ba2
commit 1de2b0e850
30 changed files with 342 additions and 32 deletions
+44 -1
View File
@@ -1,6 +1,7 @@
import { useTranslation } from 'react-i18next';
import { Disc3, LayoutGrid, ListMusic, ListTodo, PanelLeft, RotateCcw, Users } from 'lucide-react';
import { Disc3, LayoutGrid, ListMusic, ListOrdered, ListTodo, PanelLeft, RotateCcw, Users } from 'lucide-react';
import { useArtistLayoutStore } from '../../store/artistLayoutStore';
import { useAuthStore } from '../../store/authStore';
import { useHomeStore } from '../../store/homeStore';
import { usePlayerBarLayoutStore } from '../../store/playerBarLayoutStore';
import { usePlaylistLayoutStore } from '../../store/playlistLayoutStore';
@@ -16,6 +17,8 @@ import { SidebarCustomizer } from './SidebarCustomizer';
export function PersonalisationTab() {
const { t } = useTranslation();
const queueDisplayMode = useAuthStore(s => s.queueDisplayMode);
const setQueueDisplayMode = useAuthStore(s => s.setQueueDisplayMode);
return (
<>
<SettingsSubSection
@@ -76,6 +79,46 @@ export function PersonalisationTab() {
<ArtistLayoutCustomizer />
</SettingsSubSection>
<SettingsSubSection
title={t('settings.queueModeTitle')}
icon={<ListOrdered size={16} />}
>
<div className="settings-card">
{/* Two mutually exclusive modes — exactly one is always active, so
turning one on turns the other off; the active one cannot be
switched off directly (ignore the uncheck). */}
<div className="settings-toggle-row">
<div>
<div style={{ fontWeight: 500 }}>{t('queue.title')}</div>
<div style={{ fontSize: 12, color: 'var(--text-muted)' }}>{t('settings.queueModeQueueSub')}</div>
</div>
<label className="toggle-switch" aria-label={t('queue.title')}>
<input
type="checkbox"
checked={queueDisplayMode === 'queue'}
onChange={e => { if (e.target.checked) setQueueDisplayMode('queue'); }}
/>
<span className="toggle-track" />
</label>
</div>
<div className="settings-section-divider" />
<div className="settings-toggle-row">
<div>
<div style={{ fontWeight: 500 }}>{t('queue.modePlaylist')}</div>
<div style={{ fontSize: 12, color: 'var(--text-muted)' }}>{t('settings.queueModePlaylistSub')}</div>
</div>
<label className="toggle-switch" aria-label={t('queue.modePlaylist')}>
<input
type="checkbox"
checked={queueDisplayMode === 'playlist'}
onChange={e => { if (e.target.checked) setQueueDisplayMode('playlist'); }}
/>
<span className="toggle-track" />
</label>
</div>
</div>
</SettingsSubSection>
<SettingsSubSection
title={t('settings.queueToolbarTitle')}
icon={<ListMusic size={16} />}