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:
Kveld.
2026-05-02 16:45:55 -03:00
committed by GitHub
parent 5662dafe97
commit 18b4a982ef
33 changed files with 10115 additions and 1498 deletions
+5 -20
View File
@@ -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 });
},
}));