Playlist: rebuild DOM only on structural changes, patch progress in place
Every 400ms progress tick rebuilt the whole list (now heavy with per-track waveforms), so nodes were replaced between mousedown and mouseup and row buttons never received their click while a track was playing. renderPlaylist now computes a skeleton signature and, when unchanged, refreshes times, progress bar and waveforms on the existing nodes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e89213b8f9
commit
b5dc0275c8
1 file changed
+57
-16
+57
-16
@@ -1149,6 +1149,59 @@ function buildSceneEditor(tr, i, li) {
|
||||
return ed;
|
||||
}
|
||||
|
||||
// Time labels, progress and trim-range numbers for one track row. Shared by
|
||||
// the full rebuild and by the in-place patcher so the two can never disagree.
|
||||
function trackDynamics(tr, i) {
|
||||
const isCur = i === currentIndex;
|
||||
const inSeg = isCur && segTimer;
|
||||
let timeLabel = '';
|
||||
if (inSeg) timeLabel = (segMode === 'interlude' ? '✨ ' : '⏸ ') + Math.ceil(segRemain) + 's';
|
||||
else if (tr.isInterlude) timeLabel = '✨ ' + fmtTime(tr.duration || 15);
|
||||
else if (isCur && playDur > 0) timeLabel = '-' + fmtTime(Math.floor(Math.max(0, playDur - playCur)));
|
||||
else if (durations[tr.path] > 0) timeLabel = fmtTime(Math.floor(durations[tr.path]));
|
||||
const prog = (isCur && playDur > 0) ? Math.min(100, playCur / playDur * 100) : 0;
|
||||
const dur = durations[tr.path] || (isCur ? playDur : 0);
|
||||
const tStart = tr.start || 0;
|
||||
const tEnd = tr.end > 0 ? tr.end : dur;
|
||||
const total = Math.max(0, tEnd - tStart);
|
||||
const elapsed = isCur ? Math.max(0, Math.min(total, playCur - tStart)) : 0;
|
||||
const remaining = Math.max(0, total - elapsed);
|
||||
return { isCur, timeLabel, prog, tStart, total, elapsed, remaining };
|
||||
}
|
||||
|
||||
// Everything that determines the list's DOM SKELETON (nodes and handlers).
|
||||
// While a track plays only times/progress change, so the signature stays the
|
||||
// same and renderPlaylist patches in place instead of rebuilding: a rebuild
|
||||
// every 400ms progress tick replaced nodes between mousedown and mouseup and
|
||||
// row buttons stopped receiving clicks.
|
||||
let plStamp = null;
|
||||
function plSignature() {
|
||||
return playlist.map(t =>
|
||||
t.name + '' + t.path + '' + (t.key || '') + '' + (t.gap || 0) +
|
||||
'' + (t.start || 0) + '' + (t.end || 0) + '' + (t.duration || 0) +
|
||||
'' + (t.isInterlude ? 1 : 0) + (t.isVideo ? 1 : 0) + (hasScene(t) ? 1 : 0)
|
||||
).join('') +
|
||||
'' + currentIndex + '' + (isPlaying ? 1 : 0) + '' + sceneEditing +
|
||||
'' + capturingFor +
|
||||
'' + (sceneEditing >= 0 && playlist[sceneEditing] ? JSON.stringify(playlist[sceneEditing].cues || []) : '');
|
||||
}
|
||||
|
||||
// In-place refresh of the dynamic bits of each row; never touches the skeleton.
|
||||
function patchPlaylistDynamics(ol) {
|
||||
ol.querySelectorAll('li.track').forEach(li => {
|
||||
const i = parseInt(li.dataset.index, 10);
|
||||
const tr = playlist[i];
|
||||
if (!tr) return;
|
||||
const d = trackDynamics(tr, i);
|
||||
const tt = li.querySelector('.ttime'); if (tt && tt.textContent !== d.timeLabel) tt.textContent = d.timeLabel;
|
||||
const el = li.querySelector('.tt-el'); if (el) el.textContent = '▶ ' + fmtTime(Math.floor(d.elapsed));
|
||||
const tot = li.querySelector('.tt-tot'); if (tot) tot.textContent = '⏱ ' + fmtTime(Math.floor(d.total));
|
||||
const rem = li.querySelector('.tt-rem'); if (rem) rem.textContent = '⧗ -' + fmtTime(Math.ceil(d.remaining));
|
||||
const pr = li.querySelector('.tprog'); if (pr) pr.style.width = d.prog.toFixed(1) + '%';
|
||||
const canvas = li.querySelector('.wave'); if (canvas) drawWave(canvas, tr, d.isCur);
|
||||
});
|
||||
}
|
||||
|
||||
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
|
||||
@@ -1161,6 +1214,9 @@ function renderPlaylist() {
|
||||
dragFrom = -1; cueDragging = false; clearDropMarks();
|
||||
}
|
||||
const ol = $('#playlist');
|
||||
const sig = plSignature();
|
||||
if (sig === plStamp && ol.querySelector('li.track')) { patchPlaylistDynamics(ol); return; }
|
||||
plStamp = sig;
|
||||
ol.innerHTML = '';
|
||||
playlist.forEach((tr, i) => {
|
||||
const li = document.createElement('li');
|
||||
@@ -1168,26 +1224,11 @@ function renderPlaylist() {
|
||||
li.draggable = true;
|
||||
li.dataset.index = i;
|
||||
|
||||
const isCur = i === currentIndex;
|
||||
const inSeg = isCur && segTimer;
|
||||
const { isCur, timeLabel, prog, tStart, total, elapsed, remaining } = trackDynamics(tr, i);
|
||||
const playIcon = (isCur && isPlaying) ? '⏸' : '▶';
|
||||
let timeLabel = '';
|
||||
if (inSeg) timeLabel = (segMode === 'interlude' ? '✨ ' : '⏸ ') + Math.ceil(segRemain) + 's';
|
||||
else if (tr.isInterlude) timeLabel = '✨ ' + fmtTime(tr.duration || 15);
|
||||
else if (isCur && playDur > 0) timeLabel = '-' + fmtTime(Math.floor(Math.max(0, playDur - playCur)));
|
||||
else if (durations[tr.path] > 0) timeLabel = fmtTime(Math.floor(durations[tr.path]));
|
||||
const prog = (isCur && playDur > 0) ? Math.min(100, playCur / playDur * 100) : 0;
|
||||
const gapBadge = (tr.gap > 0 && !tr.isInterlude) ? '<span class="gap-badge" title="Pausa di ' + tr.gap + 's dopo questo brano">⏸' + tr.gap + 's</span>' : '';
|
||||
const icon = tr.isInterlude ? '✨ ' : (tr.isVideo ? '🎞 ' : '');
|
||||
|
||||
// Total / elapsed / remaining for the trimmed range.
|
||||
const dur = durations[tr.path] || (isCur ? playDur : 0);
|
||||
const tStart = tr.start || 0;
|
||||
const tEnd = tr.end > 0 ? tr.end : dur;
|
||||
const total = Math.max(0, tEnd - tStart);
|
||||
const elapsed = isCur ? Math.max(0, Math.min(total, playCur - tStart)) : 0;
|
||||
const remaining = Math.max(0, total - elapsed);
|
||||
|
||||
const body = (tr.isInterlude || !tr.path) ? '' :
|
||||
'<div class="track-body">' +
|
||||
'<div class="track-times">' +
|
||||
|
||||
Reference in new issue
Block a user