mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 14:55:43 +00:00
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)
This commit is contained in:
committed by
GitHub
parent
41157ccaca
commit
1c23305887
@@ -31,6 +31,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Queue — Timeline display mode
|
||||||
|
|
||||||
|
**By [@Psychotoxical](https://github.com/Psychotoxical), PR [#1004](https://github.com/Psychotoxical/psysonic/pull/1004), suggested by [@Legislate3030](https://github.com/Legislate3030)**
|
||||||
|
|
||||||
|
* New third queue display mode (cycle the header button, or pick it in **Settings → Personalisation → Queue display**). Timeline keeps the current track centered with played history above and upcoming tracks below — both visible at once — so it's easy to follow playback and jump back to earlier songs.
|
||||||
|
* The up-next order respects shuffle, and a "History" / "Up next" divider marks the boundary.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Changed
|
## Changed
|
||||||
|
|
||||||
### Dependencies — npm and Rust refresh
|
### Dependencies — npm and Rust refresh
|
||||||
|
|||||||
@@ -156,16 +156,16 @@ describe('QueuePanel — display mode', () => {
|
|||||||
expect(container.textContent).toContain('No upcoming tracks');
|
expect(container.textContent).toContain('No upcoming tracks');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('header mode-toggle button flips queueDisplayMode (default queue → playlist)', () => {
|
it('header mode-toggle button advances queueDisplayMode (default queue → timeline)', () => {
|
||||||
seedQueue(makeTracks(3), { index: 0, currentTrack: makeTrack() });
|
seedQueue(makeTracks(3), { index: 0, currentTrack: makeTrack() });
|
||||||
const { container } = renderWithProviders(<QueuePanel />);
|
const { container } = renderWithProviders(<QueuePanel />);
|
||||||
// The mode toggle is the first .queue-action-btn in the header (the
|
// The mode toggle is the first .queue-action-btn in the header (the
|
||||||
// collapse chevron is the second). The default mode is 'queue', so the
|
// collapse chevron is the second). The toggle's label names its target;
|
||||||
// toggle's label names its target ("Playlist").
|
// from the default 'queue' that is the next mode in the cycle, "Timeline".
|
||||||
const toggle = container.querySelector<HTMLButtonElement>('.queue-header .queue-action-btn');
|
const toggle = container.querySelector<HTMLButtonElement>('.queue-header .queue-action-btn');
|
||||||
expect(toggle?.getAttribute('aria-label')).toBe('Playlist');
|
expect(toggle?.getAttribute('aria-label')).toBe('Timeline');
|
||||||
toggle!.click();
|
toggle!.click();
|
||||||
expect(useAuthStore.getState().queueDisplayMode).toBe('playlist');
|
expect(useAuthStore.getState().queueDisplayMode).toBe('timeline');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useDeferredValue, useMemo, useSyncExternalStore } from 'react';
|
import { useDeferredValue, useMemo, useSyncExternalStore } from 'react';
|
||||||
import { ChevronDown, ListMusic, ListOrdered } from 'lucide-react';
|
import { AlignCenterVertical, ChevronDown, ListMusic, ListOrdered } from 'lucide-react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import type { TFunction } from 'i18next';
|
import type { TFunction } from 'i18next';
|
||||||
import { usePlayerStore } from '../../store/playerStore';
|
import { usePlayerStore } from '../../store/playerStore';
|
||||||
@@ -81,6 +81,16 @@ export function QueueHeader({
|
|||||||
|
|
||||||
const isEta = durationMode === 'eta';
|
const isEta = durationMode === 'eta';
|
||||||
|
|
||||||
|
// Cycle the panel through the three render modes; icon + title show the active
|
||||||
|
// one, tooltip names the one a click switches to.
|
||||||
|
const DISPLAY_MODE_CYCLE: QueueDisplayMode[] = ['queue', 'timeline', 'playlist'];
|
||||||
|
const nextDisplayMode =
|
||||||
|
DISPLAY_MODE_CYCLE[(DISPLAY_MODE_CYCLE.indexOf(queueDisplayMode) + 1) % DISPLAY_MODE_CYCLE.length];
|
||||||
|
const displayModeLabel = (m: QueueDisplayMode) =>
|
||||||
|
m === 'playlist' ? t('queue.modePlaylist') : m === 'timeline' ? t('queue.modeTimeline') : t('queue.title');
|
||||||
|
const displayModeIcon = (m: QueueDisplayMode) =>
|
||||||
|
m === 'playlist' ? <ListMusic size={15} /> : m === 'timeline' ? <AlignCenterVertical size={15} /> : <ListOrdered size={15} />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="queue-header">
|
<div className="queue-header">
|
||||||
<div style={{ display: "flex", flexDirection: "column", minWidth: 0, flex: 1 }}>
|
<div style={{ display: "flex", flexDirection: "column", minWidth: 0, flex: 1 }}>
|
||||||
@@ -91,17 +101,17 @@ export function QueueHeader({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="queue-action-btn"
|
className="queue-action-btn"
|
||||||
onClick={() => setQueueDisplayMode(queueDisplayMode === 'playlist' ? 'queue' : 'playlist')}
|
onClick={() => setQueueDisplayMode(nextDisplayMode)}
|
||||||
data-tooltip={queueDisplayMode === 'playlist' ? t('queue.title') : t('queue.modePlaylist')}
|
data-tooltip={displayModeLabel(nextDisplayMode)}
|
||||||
aria-label={queueDisplayMode === 'playlist' ? t('queue.title') : t('queue.modePlaylist')}
|
aria-label={displayModeLabel(nextDisplayMode)}
|
||||||
style={{ width: 24, height: 24, alignSelf: 'center', flexShrink: 0 }}
|
style={{ width: 24, height: 24, alignSelf: 'center', flexShrink: 0 }}
|
||||||
>
|
>
|
||||||
{queueDisplayMode === 'playlist' ? <ListMusic size={15} /> : <ListOrdered size={15} />}
|
{displayModeIcon(queueDisplayMode)}
|
||||||
</button>
|
</button>
|
||||||
{/* Title doubles as the mode indicator so the panel reads "Playlist"
|
{/* Title doubles as the mode indicator so the panel names the active
|
||||||
in playlist mode rather than the misleading "Queue". */}
|
mode rather than always reading "Queue". */}
|
||||||
<h2 style={{ fontSize: "16px", fontWeight: 700, margin: 0, flexShrink: 0 }}>
|
<h2 style={{ fontSize: "16px", fontWeight: 700, margin: 0, flexShrink: 0 }}>
|
||||||
{queueDisplayMode === 'playlist' ? t('queue.modePlaylist') : t('queue.title')}
|
{displayModeLabel(queueDisplayMode)}
|
||||||
</h2>
|
</h2>
|
||||||
{queue.length > 0 && (
|
{queue.length > 0 && (
|
||||||
<span style={{ fontSize: "13px", color: "var(--text-muted)", whiteSpace: "nowrap", userSelect: "none" }}>
|
<span style={{ fontSize: "13px", color: "var(--text-muted)", whiteSpace: "nowrap", userSelect: "none" }}>
|
||||||
|
|||||||
@@ -108,6 +108,16 @@ export function QueueList({
|
|||||||
return pinToTop(0, displayBaseIndex);
|
return pinToTop(0, displayBaseIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (queueDisplayMode === 'timeline') {
|
||||||
|
// Anchor the current track in the middle — history above, up-next below.
|
||||||
|
rowVirtualizer.scrollToIndex(queueIndex, { align: 'center' });
|
||||||
|
const id = requestAnimationFrame(() => {
|
||||||
|
const el = queueListRef.current?.querySelector<HTMLElement>(`[data-queue-idx="${queueIndex}"]`);
|
||||||
|
el?.scrollIntoView({ block: 'center', behavior: 'instant' });
|
||||||
|
});
|
||||||
|
return () => cancelAnimationFrame(id);
|
||||||
|
}
|
||||||
|
|
||||||
// Playlist: lazy. Let the highlight wander while the now-playing row stays
|
// Playlist: lazy. Let the highlight wander while the now-playing row stays
|
||||||
// visible; only re-pin it to the top once it has scrolled out of view.
|
// visible; only re-pin it to the top once it has scrolled out of view.
|
||||||
const viewport = queueListRef.current;
|
const viewport = queueListRef.current;
|
||||||
@@ -148,6 +158,8 @@ export function QueueList({
|
|||||||
const base = queue[idx];
|
const base = queue[idx];
|
||||||
const track = resolveQueueTrack(base);
|
const track = resolveQueueTrack(base);
|
||||||
const isPlaying = absIdx === queueIndex;
|
const isPlaying = absIdx === queueIndex;
|
||||||
|
const isTimeline = queueDisplayMode === 'timeline';
|
||||||
|
const isPast = isTimeline && absIdx < queueIndex;
|
||||||
const isFirstAutoAdded = base.autoAdded && (idx === 0 || !queue[idx - 1].autoAdded);
|
const isFirstAutoAdded = base.autoAdded && (idx === 0 || !queue[idx - 1].autoAdded);
|
||||||
const isFirstRadioAdded = base.radioAdded && (idx === 0 || !queue[idx - 1].radioAdded);
|
const isFirstRadioAdded = base.radioAdded && (idx === 0 || !queue[idx - 1].radioAdded);
|
||||||
|
|
||||||
@@ -169,6 +181,16 @@ export function QueueList({
|
|||||||
ref={rowVirtualizer.measureElement}
|
ref={rowVirtualizer.measureElement}
|
||||||
style={{ position: 'absolute', top: 0, left: 0, width: '100%', transform: `translateY(${vi.start}px)` }}
|
style={{ position: 'absolute', top: 0, left: 0, width: '100%', transform: `translateY(${vi.start}px)` }}
|
||||||
>
|
>
|
||||||
|
{isTimeline && idx === 0 && queueIndex > 0 && (
|
||||||
|
<div className="queue-divider" style={{ margin: '2px 0' }}>
|
||||||
|
<span style={{ fontSize: '11px', fontWeight: 600, color: 'var(--text-muted)' }}>{t('queue.history')}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{isTimeline && absIdx === queueIndex + 1 && (
|
||||||
|
<div className="queue-divider" style={{ margin: '2px 0' }}>
|
||||||
|
<span style={{ fontSize: '11px', fontWeight: 600, color: 'var(--text-muted)' }}>{t('queue.upNext')}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{isFirstRadioAdded && (
|
{isFirstRadioAdded && (
|
||||||
<div className="queue-divider" style={{ margin: '2px 0' }}>
|
<div className="queue-divider" style={{ margin: '2px 0' }}>
|
||||||
<span style={{ fontSize: '11px', fontWeight: 500, color: 'var(--text-muted)' }}>{t('queue.radioAdded')}</span>
|
<span style={{ fontSize: '11px', fontWeight: 500, color: 'var(--text-muted)' }}>{t('queue.radioAdded')}</span>
|
||||||
@@ -213,7 +235,7 @@ export function QueueList({
|
|||||||
document.addEventListener('mousemove', onMove);
|
document.addEventListener('mousemove', onMove);
|
||||||
document.addEventListener('mouseup', onUp);
|
document.addEventListener('mouseup', onUp);
|
||||||
}}
|
}}
|
||||||
style={dragStyle}
|
style={{ ...(isPast && !isPlaying ? { opacity: 0.5 } : null), ...dragStyle }}
|
||||||
>
|
>
|
||||||
<div className="queue-item-info">
|
<div className="queue-item-info">
|
||||||
<div className="queue-item-title truncate" style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
<div className="queue-item-title truncate" style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
||||||
|
|||||||
@@ -84,8 +84,8 @@ export function PersonalisationTab() {
|
|||||||
icon={<ListOrdered size={16} />}
|
icon={<ListOrdered size={16} />}
|
||||||
>
|
>
|
||||||
<div className="settings-card">
|
<div className="settings-card">
|
||||||
{/* Two mutually exclusive modes — exactly one is always active, so
|
{/* Three mutually exclusive modes — exactly one is always active, so
|
||||||
turning one on turns the other off; the active one cannot be
|
turning one on turns the others off; the active one cannot be
|
||||||
switched off directly (ignore the uncheck). */}
|
switched off directly (ignore the uncheck). */}
|
||||||
<div className="settings-toggle-row">
|
<div className="settings-toggle-row">
|
||||||
<div>
|
<div>
|
||||||
@@ -116,6 +116,21 @@ export function PersonalisationTab() {
|
|||||||
<span className="toggle-track" />
|
<span className="toggle-track" />
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</SettingsSubSection>
|
</SettingsSubSection>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
export const queue = {
|
export const queue = {
|
||||||
title: 'Warteschlange',
|
title: 'Warteschlange',
|
||||||
modePlaylist: 'Playlist',
|
modePlaylist: 'Playlist',
|
||||||
|
modeTimeline: 'Zeitachse',
|
||||||
|
upNext: 'Als Nächstes',
|
||||||
|
history: 'Verlauf',
|
||||||
noUpcoming: 'Keine weiteren Titel.',
|
noUpcoming: 'Keine weiteren Titel.',
|
||||||
savePlaylist: 'Playlist speichern',
|
savePlaylist: 'Playlist speichern',
|
||||||
updatePlaylist: 'Playlist aktualisieren',
|
updatePlaylist: 'Playlist aktualisieren',
|
||||||
|
|||||||
@@ -330,6 +330,7 @@ export const settings = {
|
|||||||
queueModeTitle: 'Warteschlangen-Ansicht',
|
queueModeTitle: 'Warteschlangen-Ansicht',
|
||||||
queueModeQueueSub: 'Zeigt nur kommende Titel. Der laufende Titel bleibt im Kopf und verschwindet aus der Liste, sobald er gespielt wurde.',
|
queueModeQueueSub: 'Zeigt nur kommende Titel. Der laufende Titel bleibt im Kopf und verschwindet aus der Liste, sobald er gespielt wurde.',
|
||||||
queueModePlaylistSub: 'Behält die ganze Warteschlange in der Liste, der laufende Titel oben hervorgehoben; gespielte Titel bleiben stehen.',
|
queueModePlaylistSub: 'Behält die ganze Warteschlange in der Liste, der laufende Titel oben hervorgehoben; gespielte Titel bleiben stehen.',
|
||||||
|
queueModeTimelineSub: 'Aktuellen Titel zentrieren — gespielter Verlauf darüber, kommende Titel (in Shuffle-Reihenfolge) darunter.',
|
||||||
queueToolbarTitle: 'Warteschlangen-Toolbar',
|
queueToolbarTitle: 'Warteschlangen-Toolbar',
|
||||||
queueToolbarReset: 'Zurücksetzen',
|
queueToolbarReset: 'Zurücksetzen',
|
||||||
queueToolbarSeparator: 'Trennlinie',
|
queueToolbarSeparator: 'Trennlinie',
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
export const queue = {
|
export const queue = {
|
||||||
title: 'Queue',
|
title: 'Queue',
|
||||||
modePlaylist: 'Playlist',
|
modePlaylist: 'Playlist',
|
||||||
|
modeTimeline: 'Timeline',
|
||||||
|
upNext: 'Up next',
|
||||||
|
history: 'History',
|
||||||
noUpcoming: 'No upcoming tracks.',
|
noUpcoming: 'No upcoming tracks.',
|
||||||
savePlaylist: 'Save Playlist',
|
savePlaylist: 'Save Playlist',
|
||||||
updatePlaylist: 'Update Playlist',
|
updatePlaylist: 'Update Playlist',
|
||||||
|
|||||||
@@ -394,6 +394,7 @@ export const settings = {
|
|||||||
queueModeTitle: 'Queue Display Mode',
|
queueModeTitle: 'Queue Display Mode',
|
||||||
queueModeQueueSub: 'Show only upcoming tracks. The current track stays in the header and leaves the list once it has played.',
|
queueModeQueueSub: 'Show only upcoming tracks. The current track stays in the header and leaves the list once it has played.',
|
||||||
queueModePlaylistSub: 'Keep the whole queue in the list with the current track highlighted at the top; played tracks stay.',
|
queueModePlaylistSub: 'Keep the whole queue in the list with the current track highlighted at the top; played tracks stay.',
|
||||||
|
queueModeTimelineSub: 'Center the current track, with played history above and upcoming tracks (in shuffle order) below.',
|
||||||
queueToolbarTitle: 'Queue Toolbar',
|
queueToolbarTitle: 'Queue Toolbar',
|
||||||
queueToolbarReset: 'Reset to default',
|
queueToolbarReset: 'Reset to default',
|
||||||
queueToolbarSeparator: 'Separator',
|
queueToolbarSeparator: 'Separator',
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
export const queue = {
|
export const queue = {
|
||||||
title: 'Cola',
|
title: 'Cola',
|
||||||
modePlaylist: 'Lista',
|
modePlaylist: 'Lista',
|
||||||
|
modeTimeline: 'Cronología',
|
||||||
|
upNext: 'A continuación',
|
||||||
|
history: 'Historial',
|
||||||
noUpcoming: 'No hay próximas pistas.',
|
noUpcoming: 'No hay próximas pistas.',
|
||||||
savePlaylist: 'Guardar Lista',
|
savePlaylist: 'Guardar Lista',
|
||||||
updatePlaylist: 'Actualizar Lista',
|
updatePlaylist: 'Actualizar Lista',
|
||||||
|
|||||||
@@ -329,6 +329,7 @@ export const settings = {
|
|||||||
queueModeTitle: 'Modo de visualización de la cola',
|
queueModeTitle: 'Modo de visualización de la cola',
|
||||||
queueModeQueueSub: 'Muestra solo las próximas pistas. La pista actual permanece en el encabezado y sale de la lista al reproducirse.',
|
queueModeQueueSub: 'Muestra solo las próximas pistas. La pista actual permanece en el encabezado y sale de la lista al reproducirse.',
|
||||||
queueModePlaylistSub: 'Mantiene toda la cola en la lista, con la pista actual resaltada arriba; las reproducidas permanecen.',
|
queueModePlaylistSub: 'Mantiene toda la cola en la lista, con la pista actual resaltada arriba; las reproducidas permanecen.',
|
||||||
|
queueModeTimelineSub: 'Centrar la pista actual, con el historial reproducido arriba y las próximas pistas (en orden aleatorio) debajo.',
|
||||||
queueToolbarTitle: 'Barra de herramientas de cola',
|
queueToolbarTitle: 'Barra de herramientas de cola',
|
||||||
queueToolbarReset: 'Restablecer a predeterminado',
|
queueToolbarReset: 'Restablecer a predeterminado',
|
||||||
queueToolbarSeparator: 'Separador',
|
queueToolbarSeparator: 'Separador',
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
export const queue = {
|
export const queue = {
|
||||||
title: 'File d\'attente',
|
title: 'File d\'attente',
|
||||||
modePlaylist: 'Liste de lecture',
|
modePlaylist: 'Liste de lecture',
|
||||||
|
modeTimeline: 'Chronologie',
|
||||||
|
upNext: 'À suivre',
|
||||||
|
history: 'Historique',
|
||||||
noUpcoming: 'Aucune piste à venir.',
|
noUpcoming: 'Aucune piste à venir.',
|
||||||
savePlaylist: 'Enregistrer la liste',
|
savePlaylist: 'Enregistrer la liste',
|
||||||
updatePlaylist: 'Mettre à jour la liste',
|
updatePlaylist: 'Mettre à jour la liste',
|
||||||
|
|||||||
@@ -327,6 +327,7 @@ export const settings = {
|
|||||||
queueModeTitle: 'Mode d\'affichage de la file',
|
queueModeTitle: 'Mode d\'affichage de la file',
|
||||||
queueModeQueueSub: 'N\'affiche que les pistes à venir. La piste en cours reste dans l\'en-tête et quitte la liste une fois jouée.',
|
queueModeQueueSub: 'N\'affiche que les pistes à venir. La piste en cours reste dans l\'en-tête et quitte la liste une fois jouée.',
|
||||||
queueModePlaylistSub: 'Conserve toute la file dans la liste, la piste en cours surlignée en haut ; les pistes jouées restent.',
|
queueModePlaylistSub: 'Conserve toute la file dans la liste, la piste en cours surlignée en haut ; les pistes jouées restent.',
|
||||||
|
queueModeTimelineSub: 'Centrer le morceau en cours, avec l\'historique au-dessus et les morceaux à venir (dans l\'ordre aléatoire) en dessous.',
|
||||||
queueToolbarTitle: 'Barre d\'outils de file d\'attente',
|
queueToolbarTitle: 'Barre d\'outils de file d\'attente',
|
||||||
queueToolbarReset: 'Réinitialiser',
|
queueToolbarReset: 'Réinitialiser',
|
||||||
queueToolbarSeparator: 'Séparateur',
|
queueToolbarSeparator: 'Séparateur',
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
export const queue = {
|
export const queue = {
|
||||||
title: 'Kø',
|
title: 'Kø',
|
||||||
modePlaylist: 'Spilleliste',
|
modePlaylist: 'Spilleliste',
|
||||||
|
modeTimeline: 'Tidslinje',
|
||||||
|
upNext: 'Neste',
|
||||||
|
history: 'Historikk',
|
||||||
noUpcoming: 'Ingen kommende spor.',
|
noUpcoming: 'Ingen kommende spor.',
|
||||||
savePlaylist: 'Lagre spilleliste',
|
savePlaylist: 'Lagre spilleliste',
|
||||||
updatePlaylist: 'Oppdater spilleliste',
|
updatePlaylist: 'Oppdater spilleliste',
|
||||||
|
|||||||
@@ -326,6 +326,7 @@ export const settings = {
|
|||||||
queueModeTitle: 'Visningsmodus for kø',
|
queueModeTitle: 'Visningsmodus for kø',
|
||||||
queueModeQueueSub: 'Viser bare kommende spor. Det gjeldende sporet blir i toppen og forsvinner fra listen når det er spilt.',
|
queueModeQueueSub: 'Viser bare kommende spor. Det gjeldende sporet blir i toppen og forsvinner fra listen når det er spilt.',
|
||||||
queueModePlaylistSub: 'Beholder hele køen i listen med gjeldende spor uthevet øverst; avspilte spor blir værende.',
|
queueModePlaylistSub: 'Beholder hele køen i listen med gjeldende spor uthevet øverst; avspilte spor blir værende.',
|
||||||
|
queueModeTimelineSub: 'Sentrer gjeldende spor, med avspilt historikk over og kommende spor (i shuffle-rekkefølge) under.',
|
||||||
queueToolbarTitle: 'Kø-verktøylinje',
|
queueToolbarTitle: 'Kø-verktøylinje',
|
||||||
queueToolbarReset: 'Tilbakestill til standard',
|
queueToolbarReset: 'Tilbakestill til standard',
|
||||||
queueToolbarSeparator: 'Skilje',
|
queueToolbarSeparator: 'Skilje',
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
export const queue = {
|
export const queue = {
|
||||||
title: 'Wachtrij',
|
title: 'Wachtrij',
|
||||||
modePlaylist: 'Afspeellijst',
|
modePlaylist: 'Afspeellijst',
|
||||||
|
modeTimeline: 'Tijdlijn',
|
||||||
|
upNext: 'Hierna',
|
||||||
|
history: 'Geschiedenis',
|
||||||
noUpcoming: 'Geen volgende nummers.',
|
noUpcoming: 'Geen volgende nummers.',
|
||||||
savePlaylist: 'Afspeellijst opslaan',
|
savePlaylist: 'Afspeellijst opslaan',
|
||||||
updatePlaylist: 'Afspeellijst bijwerken',
|
updatePlaylist: 'Afspeellijst bijwerken',
|
||||||
|
|||||||
@@ -327,6 +327,7 @@ export const settings = {
|
|||||||
queueModeTitle: 'Weergavemodus wachtrij',
|
queueModeTitle: 'Weergavemodus wachtrij',
|
||||||
queueModeQueueSub: 'Toont alleen komende nummers. Het huidige nummer blijft in de kop en verdwijnt uit de lijst zodra het is afgespeeld.',
|
queueModeQueueSub: 'Toont alleen komende nummers. Het huidige nummer blijft in de kop en verdwijnt uit de lijst zodra het is afgespeeld.',
|
||||||
queueModePlaylistSub: 'Houdt de hele wachtrij in de lijst met het huidige nummer bovenaan gemarkeerd; afgespeelde nummers blijven staan.',
|
queueModePlaylistSub: 'Houdt de hele wachtrij in de lijst met het huidige nummer bovenaan gemarkeerd; afgespeelde nummers blijven staan.',
|
||||||
|
queueModeTimelineSub: 'Centreer de huidige track, met de afgespeelde geschiedenis erboven en de komende tracks (in shuffle-volgorde) eronder.',
|
||||||
queueToolbarTitle: 'Wachtrij-werkbalk',
|
queueToolbarTitle: 'Wachtrij-werkbalk',
|
||||||
queueToolbarReset: 'Standaard herstellen',
|
queueToolbarReset: 'Standaard herstellen',
|
||||||
queueToolbarSeparator: 'Scheiding',
|
queueToolbarSeparator: 'Scheiding',
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
export const queue = {
|
export const queue = {
|
||||||
title: 'Coadă',
|
title: 'Coadă',
|
||||||
modePlaylist: 'Playlist',
|
modePlaylist: 'Playlist',
|
||||||
|
modeTimeline: 'Cronologie',
|
||||||
|
upNext: 'Urmează',
|
||||||
|
history: 'Istoric',
|
||||||
noUpcoming: 'Nicio piesă în continuare.',
|
noUpcoming: 'Nicio piesă în continuare.',
|
||||||
savePlaylist: 'Salvează Playlist',
|
savePlaylist: 'Salvează Playlist',
|
||||||
updatePlaylist: 'Actualizează Playlist',
|
updatePlaylist: 'Actualizează Playlist',
|
||||||
|
|||||||
@@ -332,6 +332,7 @@ export const settings = {
|
|||||||
queueModeTitle: 'Mod de afișare a cozii',
|
queueModeTitle: 'Mod de afișare a cozii',
|
||||||
queueModeQueueSub: 'Arată doar piesele următoare. Piesa curentă rămâne în antet și iese din listă după ce a fost redată.',
|
queueModeQueueSub: 'Arată doar piesele următoare. Piesa curentă rămâne în antet și iese din listă după ce a fost redată.',
|
||||||
queueModePlaylistSub: 'Păstrează toată coada în listă, cu piesa curentă evidențiată sus; piesele redate rămân.',
|
queueModePlaylistSub: 'Păstrează toată coada în listă, cu piesa curentă evidențiată sus; piesele redate rămân.',
|
||||||
|
queueModeTimelineSub: 'Centrează piesa curentă, cu istoricul redat deasupra și piesele următoare (în ordine aleatorie) dedesubt.',
|
||||||
queueToolbarTitle: 'Toolbar Coadă',
|
queueToolbarTitle: 'Toolbar Coadă',
|
||||||
queueToolbarReset: 'Resetează la implicit',
|
queueToolbarReset: 'Resetează la implicit',
|
||||||
queueToolbarSeparator: 'Separator',
|
queueToolbarSeparator: 'Separator',
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
export const queue = {
|
export const queue = {
|
||||||
title: 'Очередь',
|
title: 'Очередь',
|
||||||
modePlaylist: 'Плейлист',
|
modePlaylist: 'Плейлист',
|
||||||
|
modeTimeline: 'Хронология',
|
||||||
|
upNext: 'Далее',
|
||||||
|
history: 'История',
|
||||||
noUpcoming: 'Нет следующих треков.',
|
noUpcoming: 'Нет следующих треков.',
|
||||||
savePlaylist: 'Сохранить как плейлист',
|
savePlaylist: 'Сохранить как плейлист',
|
||||||
updatePlaylist: 'Обновить плейлист',
|
updatePlaylist: 'Обновить плейлист',
|
||||||
|
|||||||
@@ -408,6 +408,7 @@ export const settings = {
|
|||||||
queueModeTitle: 'Режим отображения очереди',
|
queueModeTitle: 'Режим отображения очереди',
|
||||||
queueModeQueueSub: 'Показывает только предстоящие треки. Текущий трек остаётся в заголовке и исчезает из списка после воспроизведения.',
|
queueModeQueueSub: 'Показывает только предстоящие треки. Текущий трек остаётся в заголовке и исчезает из списка после воспроизведения.',
|
||||||
queueModePlaylistSub: 'Сохраняет всю очередь в списке, текущий трек выделен вверху; воспроизведённые треки остаются.',
|
queueModePlaylistSub: 'Сохраняет всю очередь в списке, текущий трек выделен вверху; воспроизведённые треки остаются.',
|
||||||
|
queueModeTimelineSub: 'Центрировать текущий трек: история воспроизведения сверху, предстоящие треки (в порядке перемешивания) снизу.',
|
||||||
queueToolbarTitle: 'Панель инструментов очереди',
|
queueToolbarTitle: 'Панель инструментов очереди',
|
||||||
queueToolbarReset: 'Сбросить',
|
queueToolbarReset: 'Сбросить',
|
||||||
queueToolbarSeparator: 'Разделитель',
|
queueToolbarSeparator: 'Разделитель',
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
export const queue = {
|
export const queue = {
|
||||||
title: '队列',
|
title: '队列',
|
||||||
modePlaylist: '播放列表',
|
modePlaylist: '播放列表',
|
||||||
|
modeTimeline: '时间轴',
|
||||||
|
upNext: '即将播放',
|
||||||
|
history: '历史',
|
||||||
noUpcoming: '没有后续曲目。',
|
noUpcoming: '没有后续曲目。',
|
||||||
savePlaylist: '保存为播放列表',
|
savePlaylist: '保存为播放列表',
|
||||||
updatePlaylist: '更新播放列表',
|
updatePlaylist: '更新播放列表',
|
||||||
|
|||||||
@@ -326,6 +326,7 @@ export const settings = {
|
|||||||
queueModeTitle: '队列显示模式',
|
queueModeTitle: '队列显示模式',
|
||||||
queueModeQueueSub: '仅显示即将播放的曲目。当前曲目保留在标题栏中,播放完毕后从列表移除。',
|
queueModeQueueSub: '仅显示即将播放的曲目。当前曲目保留在标题栏中,播放完毕后从列表移除。',
|
||||||
queueModePlaylistSub: '在列表中保留整个队列,当前曲目在顶部高亮显示;已播放的曲目保留。',
|
queueModePlaylistSub: '在列表中保留整个队列,当前曲目在顶部高亮显示;已播放的曲目保留。',
|
||||||
|
queueModeTimelineSub: '将当前曲目居中显示,已播放的历史在上方,即将播放的曲目(按随机顺序)在下方。',
|
||||||
queueToolbarTitle: '队列工具栏',
|
queueToolbarTitle: '队列工具栏',
|
||||||
queueToolbarReset: '重置为默认',
|
queueToolbarReset: '重置为默认',
|
||||||
queueToolbarSeparator: '分隔符',
|
queueToolbarSeparator: '分隔符',
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ export function computeAuthStoreRehydration(state: AuthState): Partial<AuthState
|
|||||||
|
|
||||||
// Missing key (pre-feature persist) / garbage maps to 'queue' — the default
|
// Missing key (pre-feature persist) / garbage maps to 'queue' — the default
|
||||||
// mode, which lists only upcoming tracks.
|
// mode, which lists only upcoming tracks.
|
||||||
const VALID_QUEUE_DISPLAY_MODES = new Set<string>(['playlist', 'queue']);
|
const VALID_QUEUE_DISPLAY_MODES = new Set<string>(['playlist', 'queue', 'timeline']);
|
||||||
const queueDisplayModeMigrated = VALID_QUEUE_DISPLAY_MODES.has(
|
const queueDisplayModeMigrated = VALID_QUEUE_DISPLAY_MODES.has(
|
||||||
(state as { queueDisplayMode?: unknown }).queueDisplayMode as string,
|
(state as { queueDisplayMode?: unknown }).queueDisplayMode as string,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export type DurationMode = 'total' | 'remaining' | 'eta';
|
|||||||
* - `queue`: the list shows upcoming tracks only — the current track lives in
|
* - `queue`: the list shows upcoming tracks only — the current track lives in
|
||||||
* the header and drops out of the list once it has played.
|
* the header and drops out of the list once it has played.
|
||||||
*/
|
*/
|
||||||
export type QueueDisplayMode = 'playlist' | 'queue';
|
export type QueueDisplayMode = 'playlist' | 'queue' | 'timeline';
|
||||||
export type LoggingMode = 'off' | 'normal' | 'debug';
|
export type LoggingMode = 'off' | 'normal' | 'debug';
|
||||||
/**
|
/**
|
||||||
* Wall-clock format for ETA / sleep-timer labels. `'auto'` follows the user's
|
* Wall-clock format for ETA / sleep-timer labels. `'auto'` follows the user's
|
||||||
@@ -185,7 +185,9 @@ export interface AuthState {
|
|||||||
queueNowPlayingCollapsed: boolean;
|
queueNowPlayingCollapsed: boolean;
|
||||||
/** Queue header duration chip mode (cycle: total → remaining → ETA). */
|
/** Queue header duration chip mode (cycle: total → remaining → ETA). */
|
||||||
queueDurationDisplayMode: DurationMode;
|
queueDurationDisplayMode: DurationMode;
|
||||||
/** Queue panel render mode: full timeline (`playlist`) vs. upcoming-only (`queue`). */
|
/** Queue panel render mode: full list from top (`playlist`), upcoming-only
|
||||||
|
* (`queue`), or full list centered on the current track with history above
|
||||||
|
* and up-next below (`timeline`). */
|
||||||
queueDisplayMode: QueueDisplayMode;
|
queueDisplayMode: QueueDisplayMode;
|
||||||
|
|
||||||
/** Alpha: native hi-res sample rate output (disabled = safe 44.1 kHz mode) */
|
/** Alpha: native hi-res sample rate output (disabled = safe 44.1 kHz mode) */
|
||||||
|
|||||||
Reference in New Issue
Block a user