Files
psysonic/src/components/settings/PersonalisationTab.tsx
T
Frank Stellmacher 1c23305887 feat(queue): add Timeline display mode (#1004)
* feat(queue): add Timeline display mode

A third queue display mode alongside Queue and Playlist. Timeline shows
the full queue anchored on the current track — played history above
(dimmed), upcoming below — with 'History' and 'Up next' dividers and the
current row auto-centered. It already follows shuffle order since the
queue is stored in play order. The header mode button now cycles
queue → timeline → playlist; Settings gains a third option.

* i18n(queue): Timeline mode strings (9 locales)

* docs(changelog): note Timeline queue mode (#1004)
2026-06-05 16:19:20 +02:00

199 lines
7.4 KiB
TypeScript

import { useTranslation } from 'react-i18next';
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';
import { useQueueToolbarStore } from '../../store/queueToolbarStore';
import { useSidebarStore } from '../../store/sidebarStore';
import SettingsSubSection from '../SettingsSubSection';
import { ArtistLayoutCustomizer } from './ArtistLayoutCustomizer';
import { HomeCustomizer } from './HomeCustomizer';
import { PlayerBarLayoutCustomizer } from './PlayerBarLayoutCustomizer';
import { PlaylistLayoutCustomizer } from './PlaylistLayoutCustomizer';
import { QueueToolbarCustomizer } from './QueueToolbarCustomizer';
import { SidebarCustomizer } from './SidebarCustomizer';
export function PersonalisationTab() {
const { t } = useTranslation();
const queueDisplayMode = useAuthStore(s => s.queueDisplayMode);
const setQueueDisplayMode = useAuthStore(s => s.setQueueDisplayMode);
return (
<>
<SettingsSubSection
title={t('settings.sidebarTitle')}
icon={<PanelLeft size={16} />}
action={
<button
type="button"
className="btn btn-ghost"
style={{ fontSize: 12, color: 'var(--text-muted)', padding: '2px 6px' }}
onClick={() => useSidebarStore.getState().reset()}
data-tooltip={t('settings.sidebarReset')}
aria-label={t('settings.sidebarReset')}
>
<RotateCcw size={14} />
</button>
}
>
<SidebarCustomizer />
</SettingsSubSection>
<SettingsSubSection
title={t('settings.homeCustomizerTitle')}
icon={<LayoutGrid size={16} />}
action={
<button
type="button"
className="btn btn-ghost"
style={{ fontSize: 12, color: 'var(--text-muted)', padding: '2px 6px' }}
onClick={() => useHomeStore.getState().reset()}
data-tooltip={t('settings.sidebarReset')}
aria-label={t('settings.sidebarReset')}
>
<RotateCcw size={14} />
</button>
}
>
<HomeCustomizer />
</SettingsSubSection>
<SettingsSubSection
title={t('settings.artistLayoutTitle')}
icon={<Users size={16} />}
advanced
action={
<button
type="button"
className="btn btn-ghost"
style={{ fontSize: 12, color: 'var(--text-muted)', padding: '2px 6px' }}
onClick={() => useArtistLayoutStore.getState().reset()}
data-tooltip={t('settings.artistLayoutReset')}
aria-label={t('settings.artistLayoutReset')}
>
<RotateCcw size={14} />
</button>
}
>
<ArtistLayoutCustomizer />
</SettingsSubSection>
<SettingsSubSection
title={t('settings.queueModeTitle')}
icon={<ListOrdered size={16} />}
>
<div className="settings-card">
{/* Three mutually exclusive modes — exactly one is always active, so
turning one on turns the others 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 className="settings-section-divider" />
<div className="settings-toggle-row">
<div>
<div style={{ fontWeight: 500 }}>{t('queue.modeTimeline')}</div>
<div style={{ fontSize: 12, color: 'var(--text-muted)' }}>{t('settings.queueModeTimelineSub')}</div>
</div>
<label className="toggle-switch" aria-label={t('queue.modeTimeline')}>
<input
type="checkbox"
checked={queueDisplayMode === 'timeline'}
onChange={e => { if (e.target.checked) setQueueDisplayMode('timeline'); }}
/>
<span className="toggle-track" />
</label>
</div>
</div>
</SettingsSubSection>
<SettingsSubSection
title={t('settings.queueToolbarTitle')}
icon={<ListMusic size={16} />}
advanced
action={
<button
type="button"
className="btn btn-ghost"
style={{ fontSize: 12, color: 'var(--text-muted)', padding: '2px 6px' }}
onClick={() => useQueueToolbarStore.getState().reset()}
data-tooltip={t('settings.queueToolbarReset')}
aria-label={t('settings.queueToolbarReset')}
>
<RotateCcw size={14} />
</button>
}
>
<QueueToolbarCustomizer />
</SettingsSubSection>
<SettingsSubSection
title={t('settings.playlistLayoutTitle')}
icon={<ListTodo size={16} />}
advanced
action={
<button
type="button"
className="btn btn-ghost"
style={{ fontSize: 12, color: 'var(--text-muted)', padding: '2px 6px' }}
onClick={() => usePlaylistLayoutStore.getState().reset()}
data-tooltip={t('settings.playlistLayoutReset')}
aria-label={t('settings.playlistLayoutReset')}
>
<RotateCcw size={14} />
</button>
}
>
<PlaylistLayoutCustomizer />
</SettingsSubSection>
<SettingsSubSection
title={t('settings.playerBarTitle')}
icon={<Disc3 size={16} />}
advanced
action={
<button
type="button"
className="btn btn-ghost"
style={{ fontSize: 12, color: 'var(--text-muted)', padding: '2px 6px' }}
onClick={() => usePlayerBarLayoutStore.getState().reset()}
data-tooltip={t('settings.playerBarReset')}
aria-label={t('settings.playerBarReset')}
>
<RotateCcw size={14} />
</button>
}
>
<PlayerBarLayoutCustomizer />
</SettingsSubSection>
</>
);
}