- );
-}
-
-function EditorTimeline({ currentMs, total = 60, clips = [] }) {
- const playheadPct = total > 0 ? ((currentMs / 1000) / total) * 100 : 0;
- return (
-
-
- Timeline
-
-
-
- {Array.from({ length: 13 }).map((_, i) => (
-
- {i % 2 === 0 &&
{`00:${String(i * 5).padStart(2, '0')}`}}
+ {/* ── New sequence modal ── */}
+ {showNewSeq && (
+
setShowNewSeq(false)}>
+
e.stopPropagation()}>
+
New sequence
+
setNewSeqName(e.target.value)}
+ placeholder="Sequence name"
+ style={{ width: '100%', height: 32, padding: '0 10px', background: 'var(--bg-deep)', border: '1px solid var(--border)', borderRadius: 4, color: 'var(--text-primary)', fontSize: 13, marginBottom: 12 }}
+ onKeyDown={e => { if (e.key === 'Enter') createNewSequence(); }} />
+
+
+
+
- ))}
-
- {clips.length === 0 && (
-
Drop assets from the bin to build a sequence.
+
)}
-
+
+ {/* ── Overlay keyboard handler ── */}
+
);
}
-window.Editor = Editor;
+function ToolBtn({ active, label, title, onClick }) {
+ return (
+
+ );
+}
+
+function ProgramMonitor({ videoRef, currentSeq, playheadFrames, setPlayheadFrames, streamCacheRef }) {
+ const [pgmPlaying, setPgmPlaying] = React.useState(false);
+ const [pgmClipIdx, setPgmClipIdx] = React.useState(-1);
+ const [pgmClips, setPgmClips] = React.useState([]);
+
+ 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);
+ }
+
+ function togglePlay() {
+ if (pgmPlaying) {
+ stopPgm();
+ return;
+ }
+ const v1 = getV1Clips();
+ if (!v1.length) return;
+ setPgmClips(v1);
+ const idx = v1.findIndex(c => playheadFrames >= c.timeline_in_frames && playheadFrames < c.timeline_out_frames);
+ setPgmClipIdx(idx >= 0 ? idx : 0);
+ setPgmPlaying(true);
+ }
+
+ function stopPgm() {
+ setPgmPlaying(false);
+ setPgmClipIdx(-1);
+ const vid = videoRef.current;
+ if (vid) vid.pause();
+ }
+
+ React.useEffect(() => {
+ if (!pgmPlaying || pgmClipIdx < 0 || pgmClipIdx >= pgmClips.length) return;
+ const clip = pgmClips[pgmClipIdx];
+ if (!clip) { stopPgm(); return; }
+ const vid = videoRef.current;
+ if (!vid) return;
+ const cache = streamCacheRef.current;
+ (async () => {
+ let url = cache[clip.asset_id];
+ if (!url) {
+ try {
+ const r = await window.ZAMPP_API.fetch('/assets/' + clip.asset_id + '/stream');
+ if (r && r.url) { url = r.url; cache[clip.asset_id] = url; }
+ } catch (e) { console.error('Stream fetch failed', e); return; }
+ }
+ if (vid.src !== url) { vid.src = url; await vid.load(); }
+ const srcInSecs = clip.source_in_frames / (window.TC ? window.TC.FPS : 59.94);
+ vid.currentTime = srcInSecs;
+ vid.play().catch(() => {});
+ })();
+ }, [pgmPlaying, pgmClipIdx]);
+
+ function advanceClip() {
+ const next = pgmClipIdx + 1;
+ if (next >= pgmClips.length) { stopPgm(); return; }
+ setPgmClipIdx(next);
+ }
+
+ return (
+
+
+ );
+}
+
+function EditorKeyboard({ onUndo, onRedo, onSave, onMarkIn, onMarkOut, currentSeq }) {
+ React.useEffect(() => {
+ function handler(e) {
+ const tag = document.activeElement.tagName;
+ if (['INPUT', 'TEXTAREA', 'SELECT'].includes(tag)) return;
+ if ((e.ctrlKey || e.metaKey) && e.shiftKey && (e.key === 'z' || e.key === 'Z')) { e.preventDefault(); onRedo(); return; }
+ if ((e.ctrlKey || e.metaKey) && (e.key === 'z' || e.key === 'Z')) { e.preventDefault(); onUndo(); return; }
+ if ((e.ctrlKey || e.metaKey) && (e.key === 's' || e.key === 'S')) { e.preventDefault(); onSave(); return; }
+ if (e.key === 'i' || e.key === 'I') { onMarkIn(); return; }
+ if (e.key === 'o' || e.key === 'O') { onMarkOut(); return; }
+ if (e.key === 'v' || e.key === 'V') { if (window.Timeline) window.Timeline.setTool('select'); return; }
+ if (e.key === 'c' || e.key === 'C') { if (window.Timeline) window.Timeline.setTool('razor'); return; }
+ if (e.key === 'h' || e.key === 'H') { if (window.Timeline) window.Timeline.setTool('hand'); return; }
+ if (e.code === 'Space' && !currentSeq) { e.preventDefault(); /* handled by PGM */ }
+ }
+ document.addEventListener('keydown', handler);
+ return () => document.removeEventListener('keydown', handler);
+ }, [onUndo, onRedo, onSave, onMarkIn, onMarkOut, currentSeq]);
+ return null;
+}
+
+window.Editor = Editor;
\ No newline at end of file
diff --git a/services/web-ui/public/shell.jsx b/services/web-ui/public/shell.jsx
index a565fec..392de0e 100644
--- a/services/web-ui/public/shell.jsx
+++ b/services/web-ui/public/shell.jsx
@@ -17,7 +17,7 @@ const NAV_TREE = [
],
},
{ id: "jobs", label: "Jobs", icon: "jobs", badge: { kind: "neutral", text: "3" } },
- { id: "editor", label: "Editor", icon: "editor", badge: { kind: "dev", text: "DEV" } },
+ { id: "editor", label: "Editor", icon: "editor" },
];
const ADMIN_TREE = [