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 ── */}