From e89213b8f9779be3baed5fcdc695cd3b146db4b7 Mon Sep 17 00:00:00 2001 From: luciano Date: Wed, 5 Aug 2026 14:52:14 +0200 Subject: [PATCH] Fix playlist freeze: self-healing drag guard + identity-based row actions A lost dragend (dragged node removed from the DOM) left dragFrom/cueDragging stuck, permanently deferring renderPlaylist: the list stopped redrawing and row buttons acted on stale indexes (delete/play appeared dead). The guard now unlocks itself when no .dragging element exists, and row actions resolve the track index at click time by object identity. Co-Authored-By: Claude Fable 5 --- package.json | 2 +- src/control.js | 31 ++++++++++++++++++++++++------- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 59f7c77..a91d526 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dj-visualizer", - "version": "1.7.1", + "version": "1.7.2", "description": "Audio-reactive fractal & abstract visualizer for external display (Mac)", "main": "main.js", "author": { diff --git a/src/control.js b/src/control.js index 46ada5b..ca93f51 100644 --- a/src/control.js +++ b/src/control.js @@ -1153,7 +1153,13 @@ function renderPlaylist() { // Never rebuild the list mid-drag: replacing the dragged node would cancel // the browser drag instantly (progress re-renders arrive every ~400ms while // a track plays, which made reordering impossible). Deferred to dragend. - if (dragFrom >= 0 || cueDragging) { plDirty = true; return; } + if (dragFrom >= 0 || cueDragging) { + if (document.querySelector('#playlist .dragging')) { plDirty = true; return; } + // Leaked guard: dragend never fires when the dragged node left the DOM, + // which would freeze the list forever. No .dragging element = no real + // drag in progress, so unlock and render. + dragFrom = -1; cueDragging = false; clearDropMarks(); + } const ol = $('#playlist'); ol.innerHTML = ''; playlist.forEach((tr, i) => { @@ -1214,20 +1220,31 @@ function renderPlaylist() { (isCur ? '' : '') + '' + body; + // Resolve the track's index at CLICK time by identity: if the list was + // mutated while this DOM was on screen, the render-time index is stale and + // would hit the wrong track (or none). + const liveIdx = () => playlist.indexOf(tr); // Clicking the name (re)starts the track from the beginning. - li.querySelector('.tname').addEventListener('click', () => playIndex(i)); + li.querySelector('.tname').addEventListener('click', () => playIndex(liveIdx())); // On the current track the button pauses/resumes/replays; on others it starts that track. li.querySelector('.play-btn').addEventListener('click', () => { - if (i === currentIndex) togglePlayPause(); - else playIndex(i); + const k = liveIdx(); + if (k < 0) return; + if (k === currentIndex) togglePlayPause(); + else playIndex(k); + }); + li.querySelector('.del-btn').addEventListener('click', () => { + const k = liveIdx(); + if (k >= 0) removeTrack(k); }); - li.querySelector('.del-btn').addEventListener('click', () => removeTrack(i)); li.querySelector('.key-btn').addEventListener('click', () => { - capturingFor = capturingFor === i ? -1 : i; + const k = liveIdx(); + capturingFor = capturingFor === k ? -1 : k; renderPlaylist(); }); li.querySelector('.scene-btn').addEventListener('click', () => { - sceneEditing = sceneEditing === i ? -1 : i; + const k = liveIdx(); + sceneEditing = sceneEditing === k ? -1 : k; renderPlaylist(); });