feat(player): queue undo/redo with hotkeys and playback-aware restore

Snapshot queue, current track, time, and pause before queue mutations.
Ctrl+Z/Cmd+Z restores prior state; Ctrl+Shift+Z/Cmd+Shift+Z reapplies undone
edits when the redo stack is non-empty; new edits clear redo.

Resync the Rust audio engine only when the current song identity changes
from the pre-apply state so reorder/enqueue-style edits keep live playback.

Register the document capture listener from main.tsx after the window label
is set. Mini player forwards undo/redo via mini:undo-queue and mini:redo-queue.
This commit is contained in:
Maxim Isaev
2026-04-27 00:17:10 +03:00
parent e0c53da94d
commit 39f4d03da5
5 changed files with 408 additions and 7 deletions
+10
View File
@@ -198,6 +198,14 @@ export function initMiniPlayerBridgeOnMain(): () => void {
usePlayerStore.getState().shuffleQueue();
});
const undoQueueUnlisten = listen('mini:undo-queue', () => {
usePlayerStore.getState().undoLastQueueEdit();
});
const redoQueueUnlisten = listen('mini:redo-queue', () => {
usePlayerStore.getState().redoLastQueueEdit();
});
// Gapless ↔ Crossfade are mutually exclusive (see CLAUDE.md). Bridge handles
// the exclusion so the mini doesn't need to know about both states to act.
const gaplessUnlisten = listen<{ value: boolean }>('mini:set-gapless', (e) => {
@@ -241,6 +249,8 @@ export function initMiniPlayerBridgeOnMain(): () => void {
navigateUnlisten.then(fn => fn()).catch(() => {});
volumeUnlisten.then(fn => fn()).catch(() => {});
shuffleUnlisten.then(fn => fn()).catch(() => {});
undoQueueUnlisten.then(fn => fn()).catch(() => {});
redoQueueUnlisten.then(fn => fn()).catch(() => {});
gaplessUnlisten.then(fn => fn()).catch(() => {});
crossfadeUnlisten.then(fn => fn()).catch(() => {});
infiniteQueueUnlisten.then(fn => fn()).catch(() => {});