mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 23:05:46 +00:00
feat: queue-ux-improvements (#419)
* feat(queue): add ETA display, equalizer indicator and collapsible now playing
* deleted endsAt and showDuration strings, changed eta update to 30s
* feat(queue): ETA tooltip, persistent Now Playing collapse, EQ bar pause, remove redundant Play icon
* feat(queue): fold ETA into existing total/remaining toggle as third mode
The standalone ETA span next to the track counter is removed; instead the
clickable duration label in the queue header now rotates through three
modes per click: total → remaining → eta → total. Counter (N/M) stays
where it was.
ETA mode keeps the live-feel treatment from the original PR (accent
colour while playing, muted at 50% opacity when paused). The other two
modes use plain accent.
i18n: queue.etaTooltip removed (no longer a separate descriptive label),
queue.showEta added as the action tooltip ('Show estimated end time')
in all 8 locales — matches the showRemaining / showTotal pattern.
* docs(changelog): add #419 queue UX improvements entry
Adds the [1.45.0] / Added entry for this PR's queue panel refinements
(position counter, tri-state duration toggle including ETA, collapsible
Now Playing section, animated EQ indicator).
---------
Co-authored-by: Psychotoxical <171614930+Psychotoxical@users.noreply.github.com>
This commit is contained in:
@@ -165,6 +165,8 @@ interface AuthState {
|
||||
*/
|
||||
lyricsStaticOnly: boolean;
|
||||
showFullscreenLyrics: boolean;
|
||||
/** Persisted UI toggle: is the Now Playing section in queue panel collapsed */
|
||||
queueNowPlayingCollapsed: boolean;
|
||||
/** 'rail' = classic 5-line sliding rail; 'apple' = full-screen scrolling list */
|
||||
fsLyricsStyle: 'rail' | 'apple';
|
||||
/** Sidebar lyrics scroll style: 'classic' = scrollIntoView center; 'apple' = scroll to 35% */
|
||||
@@ -176,8 +178,6 @@ interface AuthState {
|
||||
lastSeenChangelogVersion: string;
|
||||
|
||||
seekbarStyle: SeekbarStyle;
|
||||
/** Cap animated seekbar styles to 30 fps (and similar GPU-friendly tweaks) for low-end hardware. */
|
||||
reducedAnimations: boolean;
|
||||
|
||||
/** Alpha: native hi-res sample rate output (disabled = safe 44.1 kHz mode) */
|
||||
enableHiRes: boolean;
|
||||
@@ -318,6 +318,7 @@ interface AuthState {
|
||||
setLyricsMode: (v: 'standard' | 'lyricsplus') => void;
|
||||
setLyricsStaticOnly: (v: boolean) => void;
|
||||
setShowFullscreenLyrics: (v: boolean) => void;
|
||||
setQueueNowPlayingCollapsed: (v: boolean) => void;
|
||||
setFsLyricsStyle: (v: 'rail' | 'apple') => void;
|
||||
setSidebarLyricsStyle: (v: 'classic' | 'apple') => void;
|
||||
setShowFsArtistPortrait: (v: boolean) => void;
|
||||
@@ -325,7 +326,6 @@ interface AuthState {
|
||||
setShowChangelogOnUpdate: (v: boolean) => void;
|
||||
setLastSeenChangelogVersion: (v: string) => void;
|
||||
setSeekbarStyle: (v: SeekbarStyle) => void;
|
||||
setReducedAnimations: (v: boolean) => void;
|
||||
setEnableHiRes: (v: boolean) => void;
|
||||
setAudioOutputDevice: (v: string | null) => void;
|
||||
setHotCacheEnabled: (v: boolean) => void;
|
||||
@@ -438,6 +438,7 @@ export const useAuthStore = create<AuthState>()(
|
||||
lyricsMode: 'standard',
|
||||
lyricsStaticOnly: false,
|
||||
showFullscreenLyrics: true,
|
||||
queueNowPlayingCollapsed: false,
|
||||
fsLyricsStyle: 'rail',
|
||||
sidebarLyricsStyle: 'classic',
|
||||
showFsArtistPortrait: true,
|
||||
@@ -445,7 +446,6 @@ export const useAuthStore = create<AuthState>()(
|
||||
showChangelogOnUpdate: true,
|
||||
lastSeenChangelogVersion: '',
|
||||
seekbarStyle: 'truewave',
|
||||
reducedAnimations: false,
|
||||
enableHiRes: false,
|
||||
audioOutputDevice: null,
|
||||
hotCacheEnabled: false,
|
||||
@@ -599,6 +599,7 @@ export const useAuthStore = create<AuthState>()(
|
||||
setLyricsMode: (v) => set({ lyricsMode: v }),
|
||||
setLyricsStaticOnly: (v) => set({ lyricsStaticOnly: v }),
|
||||
setShowFullscreenLyrics: (v: boolean) => set({ showFullscreenLyrics: v }),
|
||||
setQueueNowPlayingCollapsed: (v: boolean) => set({ queueNowPlayingCollapsed: v }),
|
||||
setFsLyricsStyle: (v) => set({ fsLyricsStyle: v }),
|
||||
setSidebarLyricsStyle: (v) => set({ sidebarLyricsStyle: v }),
|
||||
setShowFsArtistPortrait: (v: boolean) => set({ showFsArtistPortrait: v }),
|
||||
@@ -607,7 +608,6 @@ export const useAuthStore = create<AuthState>()(
|
||||
setLastSeenChangelogVersion: (v) => set({ lastSeenChangelogVersion: v }),
|
||||
|
||||
setSeekbarStyle: (v) => set({ seekbarStyle: v }),
|
||||
setReducedAnimations: (v) => set({ reducedAnimations: v }),
|
||||
setEnableHiRes: (v) => set({ enableHiRes: v }),
|
||||
setAudioOutputDevice: (v) => set({ audioOutputDevice: v }),
|
||||
setHotCacheEnabled: (v) => set({ hotCacheEnabled: v }),
|
||||
|
||||
@@ -21,15 +21,6 @@ interface PreviewState {
|
||||
elapsed: number;
|
||||
/** Total preview window in seconds (echoes the duration_sec arg). */
|
||||
duration: number;
|
||||
/**
|
||||
* True only after the engine has emitted `audio:preview-start` for the
|
||||
* current `previewingId` — i.e. audio is actually playing. Drives the
|
||||
* progress-ring animation so the ring doesn't run ahead of the speaker
|
||||
* during the engine's download/decode/seek warmup. Reset to false on every
|
||||
* `startPreview` call so a switch from track A to track B doesn't carry
|
||||
* over A's animation state.
|
||||
*/
|
||||
audioStarted: boolean;
|
||||
|
||||
startPreview: (song: { id: string; title: string; artist: string; coverArt?: string; duration?: number }, location: TrackPreviewLocation) => Promise<void>;
|
||||
stopPreview: () => Promise<void>;
|
||||
@@ -49,7 +40,6 @@ export const usePreviewStore = create<PreviewState>((set, get) => ({
|
||||
previewingTrack: null,
|
||||
elapsed: 0,
|
||||
duration: 30,
|
||||
audioStarted: false,
|
||||
|
||||
startPreview: async (song, location) => {
|
||||
const auth = useAuthStore.getState();
|
||||
@@ -87,7 +77,6 @@ export const usePreviewStore = create<PreviewState>((set, get) => ({
|
||||
previewingTrack: { id: song.id, title: song.title, artist: song.artist, coverArt: song.coverArt },
|
||||
elapsed: 0,
|
||||
duration: previewDuration,
|
||||
audioStarted: false,
|
||||
});
|
||||
|
||||
try {
|
||||
@@ -101,7 +90,7 @@ export const usePreviewStore = create<PreviewState>((set, get) => ({
|
||||
} catch (e) {
|
||||
// Roll back optimistic state on failure.
|
||||
if (get().previewingId === song.id) {
|
||||
set({ previewingId: null, previewingTrack: null, elapsed: 0, audioStarted: false });
|
||||
set({ previewingId: null, previewingTrack: null, elapsed: 0 });
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
@@ -113,19 +102,15 @@ export const usePreviewStore = create<PreviewState>((set, get) => ({
|
||||
await invoke('audio_preview_stop');
|
||||
} catch {
|
||||
/* engine will emit preview-end anyway; clear locally as fallback */
|
||||
set({ previewingId: null, previewingTrack: null, elapsed: 0, audioStarted: false });
|
||||
set({ previewingId: null, previewingTrack: null, elapsed: 0 });
|
||||
}
|
||||
},
|
||||
|
||||
_onStart: (id) => {
|
||||
const current = get().previewingId;
|
||||
if (current !== id) {
|
||||
if (get().previewingId !== id) {
|
||||
// Engine fired start for an id we didn't track locally — keep id but
|
||||
// leave previewingTrack as-is (the caller's startPreview() set it).
|
||||
set({ previewingId: id, elapsed: 0, audioStarted: true });
|
||||
} else {
|
||||
// Audio is now actually playing — unblock the progress-ring animation.
|
||||
set({ audioStarted: true });
|
||||
set({ previewingId: id, elapsed: 0 });
|
||||
}
|
||||
},
|
||||
|
||||
@@ -136,6 +121,6 @@ export const usePreviewStore = create<PreviewState>((set, get) => ({
|
||||
|
||||
_onEnd: (id) => {
|
||||
if (get().previewingId !== id) return;
|
||||
set({ previewingId: null, previewingTrack: null, elapsed: 0, audioStarted: false });
|
||||
set({ previewingId: null, previewingTrack: null, elapsed: 0 });
|
||||
},
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user