From 4673efac6a9267df676115d4147195e3de0a1a9c Mon Sep 17 00:00:00 2001 From: Zac Gaetano Date: Sun, 24 May 2026 21:03:12 -0400 Subject: [PATCH] fix(editor): setScale, hand pan, sort comparator, playhead sync, rename/delete, track selector --- services/web-ui/public/js/timeline.js | 41 ++++++++++++ services/web-ui/public/screens-editor.jsx | 77 ++++++++++++++++++++--- 2 files changed, 110 insertions(+), 8 deletions(-) diff --git a/services/web-ui/public/js/timeline.js b/services/web-ui/public/js/timeline.js index 64af11d..8598863 100644 --- a/services/web-ui/public/js/timeline.js +++ b/services/web-ui/public/js/timeline.js @@ -128,6 +128,7 @@ document.addEventListener('keydown', _onKeyDown); _renderRuler(); + _initHandPan(); } // ── Ruler ─────────────────────────────────────────────────────────────────── @@ -491,6 +492,45 @@ function getTool() { return s.activeTool; } + // ── Scale (zoom) ───────────────────────────────────────────────────────────── + + function setScale(px) { + s.scale = Math.min(500, Math.max(20, px)); + _renderClips(); + _renderRuler(); + } + + function getScale() { return s.scale; } + + // ── Hand tool: pan by dragging ─────────────────────────────────────────────── + + function _initHandPan() { + var dragging = false; + var startX = 0; + var startScroll = 0; + + s.tracksEl.addEventListener('mousedown', function (e) { + if (s.activeTool !== 'hand') return; + dragging = true; + startX = e.clientX; + startScroll = s.container.scrollLeft; + s.tracksEl.style.cursor = 'grabbing'; + e.preventDefault(); + }); + + document.addEventListener('mousemove', function (e) { + if (!dragging) return; + var dx = e.clientX - startX; + s.container.scrollLeft = Math.max(0, startScroll - dx); + }); + + document.addEventListener('mouseup', function () { + if (!dragging) return; + dragging = false; + if (s.activeTool === 'hand') s.tracksEl.style.cursor = 'grab'; + }); + } + // ── Add clip at playhead ───────────────────────────────────────────────────── function addClip(asset, srcIn, srcOut, track) { @@ -532,6 +572,7 @@ global.Timeline = { init, render, setTool, getTool, setPlayhead, getPlayhead, + setScale, getScale, addClip, }; diff --git a/services/web-ui/public/screens-editor.jsx b/services/web-ui/public/screens-editor.jsx index da6b1ef..e532141 100644 --- a/services/web-ui/public/screens-editor.jsx +++ b/services/web-ui/public/screens-editor.jsx @@ -24,6 +24,9 @@ function Editor() { const [showNewSeq, setShowNewSeq] = React.useState(false); const [newSeqName, setNewSeqName] = React.useState(''); const [isDirty, setIsDirty] = React.useState(false); + const [insertTrack, setInsertTrack] = React.useState(0); + const [renamingSeq, setRenamingSeq] = React.useState(false); + const [renameVal, setRenameVal] = React.useState(''); const srcVideoRef = React.useRef(null); const pgmVideoRef = React.useRef(null); const tlRef = React.useRef(null); @@ -95,6 +98,30 @@ function Editor() { } catch (e) { console.error('Failed to create sequence', e); } } + async function renameSequence() { + if (!currentSeq || !renameVal.trim()) { setRenamingSeq(false); return; } + try { + await window.ZAMPP_API.updateSequence(currentSeq.id, { name: renameVal.trim() }); + setSequences(prev => prev.map(s => s.id === currentSeq.id ? { ...s, name: renameVal.trim() } : s)); + setCurrentSeq(prev => prev ? { ...prev, name: renameVal.trim() } : prev); + } catch (e) { console.error('Rename failed', e); } + setRenamingSeq(false); + } + + async function deleteSequence() { + if (!currentSeq) return; + if (!window.confirm('Delete sequence "' + currentSeq.name + '"? This cannot be undone.')) return; + try { + await window.ZAMPP_API.deleteSequence(currentSeq.id); + const remaining = sequences.filter(s => s.id !== currentSeq.id); + setSequences(remaining); + setCurrentSeq(null); + setHistory([[]]); + setHistoryIdx(0); + if (remaining.length) openSequence(remaining[0].id); + } catch (e) { console.error('Delete failed', e); } + } + function renderTimelineClips(clips) { if (window.Timeline && tlRef.current) { window.Timeline.render(clips || [], { fps: 59.94 }); @@ -201,7 +228,7 @@ function Editor() { const sOut = srcOut != null ? srcOut : (vid && vid.duration ? vid.duration : ((sourceAsset.duration_ms || 10000) / 1000)); window.Timeline.addClip( { ...sourceAsset, streamUrl: streamCacheRef.current[sourceAsset.id] }, - sIn, sOut, 0 + sIn, sOut, insertTrack ); } @@ -219,14 +246,33 @@ function Editor() { {/* ── Top toolbar ── */}
- + {renamingSeq ? ( + setRenameVal(e.target.value)} + onKeyDown={e => { if (e.key === 'Enter') renameSequence(); if (e.key === 'Escape') setRenamingSeq(false); }} + onBlur={renameSequence} + style={{ fontSize: 12, height: 24, padding: '0 6px', background: 'var(--bg-surface)', border: '1px solid var(--accent)', borderRadius: 3, color: 'var(--text-primary)', maxWidth: 200 }} /> + ) : ( + + )} + {currentSeq && ( + + )} + {currentSeq && ( + + )} @@ -248,6 +294,14 @@ function Editor() {
+
@@ -355,7 +409,7 @@ function ProgramMonitor({ videoRef, currentSeq, playheadFrames, setPlayheadFrame function getV1Clips() { if (!currentSeq || !currentSeq.clips) return []; - return currentSeq.clips.filter(c => c.track === 0).sort((a, b) => a.timeline_in_frames - b.timeline_out_frames); + return currentSeq.clips.filter(c => c.track === 0).sort((a, b) => a.timeline_in_frames - b.timeline_in_frames); } function togglePlay() { @@ -414,8 +468,15 @@ function ProgramMonitor({ videoRef, currentSeq, playheadFrames, setPlayheadFrame const clip = pgmClips[pgmClipIdx]; if (!clip) return; const fps = window.TC ? window.TC.FPS : 59.94; + const vid = videoRef.current; + if (!vid) return; + // Sync timeline playhead to current program position + const elapsed = vid.currentTime - (clip.source_in_frames / fps); + const tlFrames = clip.timeline_in_frames + Math.round(elapsed * fps); + setPlayheadFrames(Math.max(0, tlFrames)); + if (window.Timeline) window.Timeline.setPlayhead(Math.max(0, tlFrames)); const srcOutSecs = clip.source_out_frames / fps; - if (videoRef.current && videoRef.current.currentTime >= srcOutSecs) { + if (vid.currentTime >= srcOutSecs) { advanceClip(); } }}