fix(editor): setScale, hand pan, sort comparator, playhead sync, rename/delete, track selector
This commit is contained in:
parent
721f847b28
commit
4673efac6a
2 changed files with 110 additions and 8 deletions
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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 ── */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '6px 12px', borderBottom: '1px solid var(--border)', background: 'var(--bg-panel)', flexShrink: 0, height: 40 }}>
|
||||
<Icon name="editor" size={14} />
|
||||
<select value={currentSeq ? currentSeq.id : ''} onChange={e => openSequence(e.target.value)}
|
||||
style={{ fontSize: 12, height: 24, background: 'var(--bg-surface)', border: '1px solid var(--border)', borderRadius: 3, color: 'var(--text-primary)', maxWidth: 200 }}>
|
||||
{sequences.length === 0 && <option value="">No sequences</option>}
|
||||
{sequences.map(s => <option key={s.id} value={s.id}>{s.name}</option>)}
|
||||
</select>
|
||||
{renamingSeq ? (
|
||||
<input value={renameVal} autoFocus onChange={e => 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 }} />
|
||||
) : (
|
||||
<select value={currentSeq ? currentSeq.id : ''} onChange={e => openSequence(e.target.value)}
|
||||
style={{ fontSize: 12, height: 24, background: 'var(--bg-surface)', border: '1px solid var(--border)', borderRadius: 3, color: 'var(--text-primary)', maxWidth: 200 }}>
|
||||
{sequences.length === 0 && <option value="">No sequences</option>}
|
||||
{sequences.map(s => <option key={s.id} value={s.id}>{s.name}</option>)}
|
||||
</select>
|
||||
)}
|
||||
<button className="btn ghost sm" style={{ width: 22, height: 22, padding: 0 }} onClick={() => setShowNewSeq(true)} title="New sequence">
|
||||
<Icon name="plus" size={12} />
|
||||
</button>
|
||||
{currentSeq && (
|
||||
<button className="btn ghost sm" style={{ width: 22, height: 22, padding: 0 }} title="Rename sequence"
|
||||
onClick={() => { setRenameVal(currentSeq.name); setRenamingSeq(true); }}>
|
||||
<Icon name="edit" size={12} />
|
||||
</button>
|
||||
)}
|
||||
{currentSeq && (
|
||||
<button className="btn ghost sm" style={{ width: 22, height: 22, padding: 0 }} title="Delete sequence"
|
||||
onClick={deleteSequence}>
|
||||
<Icon name="trash" size={12} />
|
||||
</button>
|
||||
)}
|
||||
<span style={{ width: 1, height: 16, background: 'var(--border)', margin: '0 4px' }} />
|
||||
<button className="btn ghost sm" onClick={undo} disabled={historyIdx <= 0} title="Undo (Ctrl+Z)">\u21A9</button>
|
||||
<button className="btn ghost sm" onClick={redo} disabled={historyIdx >= history.length - 1} title="Redo (Ctrl+Shift+Z)">\u21AA</button>
|
||||
|
|
@ -248,6 +294,14 @@ function Editor() {
|
|||
<button className="btn ghost sm" style={{ fontSize: 10, padding: '2px 6px' }} onClick={markSrcIn} title="Mark In (I)">{srcInLabel}</button>
|
||||
<button className="btn ghost sm" style={{ fontSize: 10, padding: '2px 6px' }} onClick={markSrcOut} title="Mark Out (O)">{srcOutLabel}</button>
|
||||
<div style={{ flex: 1 }} />
|
||||
<select value={insertTrack} onChange={e => setInsertTrack(Number(e.target.value))}
|
||||
title="Target track"
|
||||
style={{ fontSize: 10, height: 20, background: 'var(--bg-surface)', border: '1px solid var(--border)', borderRadius: 2, color: 'var(--text-secondary)', marginRight: 4 }}>
|
||||
<option value={0}>V1</option>
|
||||
<option value={1}>V2</option>
|
||||
<option value={100}>A1</option>
|
||||
<option value={101}>A2</option>
|
||||
</select>
|
||||
<button className="btn primary sm" style={{ fontSize: 10, padding: '2px 8px' }} onClick={addToTimeline} title="Insert at playhead">Insert</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -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();
|
||||
}
|
||||
}}
|
||||
|
|
|
|||
Loading…
Reference in a new issue