mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 23:05:46 +00:00
feat(queue): persist queue header duration mode (#724)
* feat(queue): persist header duration mode (#625) The queue header chip cycles total / remaining / ETA, but the choice lived in QueuePanel `useState` and reset to 'total' on every app launch. Move it into `authStore` (persisted under `psysonic-auth`) so the chosen mode survives restarts like other UI preferences. - `DurationMode` consolidated in `authStoreTypes` (re-exported from `queuePanelHelpers` so existing component imports stay valid). - New `queueDurationDisplayMode` field + `setQueueDurationDisplayMode` setter wired through `createUiAppearanceActions`, default `'total'`. - `computeAuthStoreRehydration` validates the persisted value against the three known modes (pattern matches the existing seekbarStyle block) — garbage, null, undefined, or a missing key map back to `'total'` so the chip never receives an unknown mode. - `QueuePanel` reads / writes via `useAuthStore` selectors instead of local `useState`. - Tests: trivial-setter coverage in `authStore.settings.test.ts` and a new `authStoreRehydrate.test.ts` covering corrupt, missing, and valid rehydration cases. Reuses kveld9's design from PR #625; not merged because the 1.46 refactor split locales and reshuffled queue-helper exports. Credited via Co-Authored-By trailer. Co-Authored-By: Kveld. <kveld912@proton.me> * docs(changelog): queue header duration mode persistence (#724) Adds the CHANGELOG entry + kveld9 credits line referencing the real PR numbers (no #TBD placeholder) — feedback.md §2.7+§2.8 workflow: commit docs as a second push onto the open PR. --------- Co-authored-by: Kveld. <kveld912@proton.me>
This commit is contained in:
committed by
GitHub
parent
187170057d
commit
ccf4d5d8cb
@@ -65,6 +65,7 @@ describe('trivial pass-through setters', () => {
|
||||
['setLyricsStaticOnly', 'lyricsStaticOnly', true],
|
||||
['setShowChangelogOnUpdate', 'showChangelogOnUpdate', false],
|
||||
['setQueueNowPlayingCollapsed', 'queueNowPlayingCollapsed', true],
|
||||
['setQueueDurationDisplayMode', 'queueDurationDisplayMode', 'eta'],
|
||||
['setEnableHiRes', 'enableHiRes', true],
|
||||
['setHotCacheEnabled', 'hotCacheEnabled', true],
|
||||
['setMixMinRatingFilterEnabled', 'mixMinRatingFilterEnabled', true],
|
||||
|
||||
@@ -89,6 +89,7 @@ export const useAuthStore = create<AuthState>()(
|
||||
advancedSettingsEnabled: false,
|
||||
seekbarStyle: 'truewave',
|
||||
queueNowPlayingCollapsed: false,
|
||||
queueDurationDisplayMode: 'total',
|
||||
enableHiRes: false,
|
||||
audioOutputDevice: null,
|
||||
hotCacheEnabled: false,
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
import { computeAuthStoreRehydration } from './authStoreRehydrate';
|
||||
import { useAuthStore } from './authStore';
|
||||
import type { AuthState } from './authStoreTypes';
|
||||
import { resetAuthStore } from '@/test/helpers/storeReset';
|
||||
|
||||
describe('computeAuthStoreRehydration — queueDurationDisplayMode', () => {
|
||||
beforeEach(() => {
|
||||
resetAuthStore();
|
||||
});
|
||||
|
||||
it.each(['invalid_mode', 123, null, undefined] as const)(
|
||||
'maps corrupted value %j back to "total"',
|
||||
(corrupt) => {
|
||||
const base = useAuthStore.getState();
|
||||
const patch = computeAuthStoreRehydration({
|
||||
...base,
|
||||
queueDurationDisplayMode: corrupt as never,
|
||||
});
|
||||
expect(patch.queueDurationDisplayMode).toBe('total');
|
||||
},
|
||||
);
|
||||
|
||||
it('maps a rehydrated payload without the key back to "total"', () => {
|
||||
const base = useAuthStore.getState();
|
||||
const { queueDurationDisplayMode: _drop, ...without } = base;
|
||||
const patch = computeAuthStoreRehydration(without as AuthState);
|
||||
expect(patch.queueDurationDisplayMode).toBe('total');
|
||||
});
|
||||
|
||||
it.each(['total', 'remaining', 'eta'] as const)(
|
||||
'does not overwrite a valid mode (%s)',
|
||||
(mode) => {
|
||||
const base = useAuthStore.getState();
|
||||
const patch = computeAuthStoreRehydration({
|
||||
...base,
|
||||
queueDurationDisplayMode: mode,
|
||||
});
|
||||
expect(patch.queueDurationDisplayMode).toBeUndefined();
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
import type {
|
||||
AuthState,
|
||||
DiscordCoverSource,
|
||||
DurationMode,
|
||||
LyricsSourceConfig,
|
||||
SeekbarStyle,
|
||||
} from './authStoreTypes';
|
||||
@@ -79,6 +80,16 @@ export function computeAuthStoreRehydration(state: AuthState): Partial<AuthState
|
||||
? {}
|
||||
: { seekbarStyle: 'truewave' as SeekbarStyle };
|
||||
|
||||
// Garbage / null / undefined / missing key from a legacy or tampered persist
|
||||
// payload maps back to 'total' so the duration chip never receives an
|
||||
// unknown mode (would render an empty label).
|
||||
const VALID_QUEUE_DURATION_MODES = new Set<string>(['total', 'remaining', 'eta']);
|
||||
const queueDurationDisplayModeMigrated = VALID_QUEUE_DURATION_MODES.has(
|
||||
(state as { queueDurationDisplayMode?: unknown }).queueDurationDisplayMode as string,
|
||||
)
|
||||
? {}
|
||||
: { queueDurationDisplayMode: 'total' as DurationMode };
|
||||
|
||||
// The `animationMode` 3-state setting was removed; users on `'reduced'`
|
||||
// or `'static'` collapse onto the former `'full'` path automatically as
|
||||
// soon as the field is gone from the store. Strip the persisted field
|
||||
@@ -130,6 +141,7 @@ export function computeAuthStoreRehydration(state: AuthState): Partial<AuthState
|
||||
...lyricsSourcesMigrated,
|
||||
...wheelSmoothOneTime,
|
||||
...seekbarStyleMigrated,
|
||||
...queueDurationDisplayModeMigrated,
|
||||
...discordCoverSourceMigrated,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ export interface ServerProfile {
|
||||
}
|
||||
|
||||
export type SeekbarStyle = 'truewave' | 'pseudowave' | 'linedot' | 'bar' | 'thick' | 'segmented' | 'neon' | 'pulsewave' | 'particletrail' | 'liquidfill' | 'retrotape';
|
||||
/** Queue header duration chip: total duration / time left / ETA finish clock. */
|
||||
export type DurationMode = 'total' | 'remaining' | 'eta';
|
||||
export type LoggingMode = 'off' | 'normal' | 'debug';
|
||||
export type NormalizationEngine = 'off' | 'replaygain' | 'loudness';
|
||||
export type DiscordCoverSource = 'none' | 'apple' | 'server';
|
||||
@@ -136,6 +138,8 @@ export interface AuthState {
|
||||
seekbarStyle: SeekbarStyle;
|
||||
/** Persisted UI toggle: is the Now Playing section in queue panel collapsed */
|
||||
queueNowPlayingCollapsed: boolean;
|
||||
/** Queue header duration chip mode (cycle: total → remaining → ETA). */
|
||||
queueDurationDisplayMode: DurationMode;
|
||||
|
||||
/** Alpha: native hi-res sample rate output (disabled = safe 44.1 kHz mode) */
|
||||
enableHiRes: boolean;
|
||||
@@ -289,6 +293,7 @@ export interface AuthState {
|
||||
setAdvancedSettingsEnabled: (v: boolean) => void;
|
||||
setSeekbarStyle: (v: SeekbarStyle) => void;
|
||||
setQueueNowPlayingCollapsed: (v: boolean) => void;
|
||||
setQueueDurationDisplayMode: (v: DurationMode) => void;
|
||||
setEnableHiRes: (v: boolean) => void;
|
||||
setAudioOutputDevice: (v: string | null) => void;
|
||||
setHotCacheEnabled: (v: boolean) => void;
|
||||
|
||||
@@ -22,6 +22,7 @@ export function createUiAppearanceActions(set: SetState): Pick<
|
||||
| 'setLinuxWebkitKineticScroll'
|
||||
| 'setSeekbarStyle'
|
||||
| 'setQueueNowPlayingCollapsed'
|
||||
| 'setQueueDurationDisplayMode'
|
||||
| 'setShowFullscreenLyrics'
|
||||
| 'setFsLyricsStyle'
|
||||
| 'setSidebarLyricsStyle'
|
||||
@@ -42,6 +43,7 @@ export function createUiAppearanceActions(set: SetState): Pick<
|
||||
setLinuxWebkitKineticScroll: (v) => set({ linuxWebkitKineticScroll: v }),
|
||||
setSeekbarStyle: (v) => set({ seekbarStyle: v }),
|
||||
setQueueNowPlayingCollapsed: (v) => set({ queueNowPlayingCollapsed: v }),
|
||||
setQueueDurationDisplayMode: (v) => set({ queueDurationDisplayMode: v }),
|
||||
setShowFullscreenLyrics: (v) => set({ showFullscreenLyrics: v }),
|
||||
setFsLyricsStyle: (v) => set({ fsLyricsStyle: v }),
|
||||
setSidebarLyricsStyle: (v) => set({ sidebarLyricsStyle: v }),
|
||||
|
||||
Reference in New Issue
Block a user