diff --git a/movie_editor/frontend/actionbar.js b/movie_editor/frontend/actionbar.js new file mode 100644 index 00000000..cec76ecd --- /dev/null +++ b/movie_editor/frontend/actionbar.js @@ -0,0 +1,39 @@ +// Main transport actions: Generate / Generate selection / Render (timeline header). +(function () { + const { el, clear } = window.dom; + const S = window.Store; + const mount = document.getElementById("transport-actions"); + if (!mount) return; + + function selCount() { return S.selectedSceneCount ? S.selectedSceneCount() : (S.get().selectedSceneId ? 1 : 0); } + function hasProject() { return !!S.get().project; } + function busy(st) { return ["queuing", "running", "pending"].includes(st.gen?.state); } + + function render(st) { + clear(mount); + const genAll = el("button", "btn primary compact", "▶ Generate"); + genAll.title = "Generate the whole montage"; + genAll.disabled = !hasProject() || busy(st); + genAll.onclick = () => S.generate(null); + mount.append(genAll); + + const n = selCount(); + const selLabel = n > 1 ? `Selected (${n})` : "Selected"; + const genSel = el("button", "btn ghost compact", selLabel); + genSel.title = n > 1 + ? `Generate ${n} selected scenes (one chain run per segment)` + : "Generate the selected scene"; + genSel.disabled = n === 0 || busy(st); + genSel.onclick = () => S.generateSelected(); + mount.append(genSel); + + const renderBtn = el("button", "btn render compact", "⧉ Render"); + renderBtn.title = "Stitch generated clips into a final video"; + renderBtn.disabled = !hasProject() || busy(st); + renderBtn.onclick = () => S.renderFinal(); + mount.append(renderBtn); + } + + S.subscribe(render); + render(S.get()); +})(); \ No newline at end of file diff --git a/movie_editor/frontend/index.html b/movie_editor/frontend/index.html index 4192ad59..0558fc41 100644 --- a/movie_editor/frontend/index.html +++ b/movie_editor/frontend/index.html @@ -87,6 +87,7 @@
Timeline +
@@ -104,6 +105,7 @@ + diff --git a/movie_editor/frontend/inspector.js b/movie_editor/frontend/inspector.js index e65aad51..c7d3f404 100644 --- a/movie_editor/frontend/inspector.js +++ b/movie_editor/frontend/inspector.js @@ -145,7 +145,7 @@ const selN = S.selectedSceneCount ? S.selectedSceneCount() : 1; if (selN > 1) { const mh = el("div", "insp-hint"); - mh.textContent = `${selN} clips selected — Generate → Generate Selected Scenes runs each chain segment that includes a selected clip (⌘/Ctrl-click toggle · Shift-click range).`; + mh.textContent = `${selN} clips selected — use Selected in the timeline header to generate each chain segment that includes a selected clip (⌘/Ctrl-click toggle · Shift-click range).`; body.append(mh); } if (unitScenes.length > 1) { diff --git a/movie_editor/frontend/menubar.js b/movie_editor/frontend/menubar.js index 0e84f4f2..da745110 100644 --- a/movie_editor/frontend/menubar.js +++ b/movie_editor/frontend/menubar.js @@ -1,4 +1,4 @@ -// Top menu bar: File / Edit / View / Generate / FunPack + status chips. +// Top menu bar: File / Edit / View / Settings / FunPack + status chips. (function () { const { el, clear } = window.dom; const S = window.Store; @@ -7,7 +7,6 @@ let openName = null; function sel() { return S.get().selectedSceneId; } - function selCount() { return S.selectedSceneCount ? S.selectedSceneCount() : (sel() ? 1 : 0); } function hasProject() { return !!S.get().project; } // hidden file input for importing a project file @@ -72,25 +71,17 @@ { sep: true }, { label: "Reset Layout", action: () => { const r = document.documentElement; r.style.removeProperty("--media-w"); r.style.removeProperty("--timeline-h"); } }, ], - Generate: [ + Settings: [ { label: "Engine settings…", disabled: !hasProject(), action: () => window.EngineSettingsModal.open() }, { sep: true }, - { label: "Generate Whole Montage", hint: "▶", disabled: !hasProject(), action: () => S.generate(null) }, - { label: selCount() > 1 ? `Generate Selected Scenes (${selCount()})` : "Generate Selected Scene", - hint: selCount() > 1 ? "⌘-click" : undefined, - disabled: selCount() === 0, action: () => S.generateSelected() }, - { sep: true }, - { label: "Render Final Video", hint: "⧉", disabled: !hasProject(), action: () => S.renderFinal() }, + { label: "Models…", action: () => window.ModelsModal.open() }, + { label: "Refresh model list", hint: "R", action: async () => { try { await window.MovieEditorAPI.refreshModels(); } catch (_) {} } }, { sep: true }, { label: `Conditioning: ${_roleLabel(st.project?.conditioning_slot, "FunPack Studio")}`, disabled: !hasProject(), action: () => _pickRole("conditioning_slot", "Conditioning node", st) }, { label: `Sampler: ${_roleLabel(st.project?.sampler_slot, "FunPack Chain Sampler")}`, disabled: !hasProject(), action: () => _pickRole("sampler_slot", "Sampler node", st) }, ], - Models: [ - { label: "Settings…", action: () => window.ModelsModal.open() }, - { label: "Refresh model list", hint: "R", action: async () => { try { await window.MovieEditorAPI.refreshModels(); } catch (_) {} } }, - ], FunPack: [ { label: st.resetSessionArmed ? "Reset Studio session ✓ armed — click to cancel" : "Reset Studio session", disabled: !hasProject(), action: () => S.resetStudioSession() }, diff --git a/movie_editor/frontend/player.js b/movie_editor/frontend/player.js index d09a492e..2682bec9 100644 --- a/movie_editor/frontend/player.js +++ b/movie_editor/frontend/player.js @@ -513,7 +513,7 @@ const empty = el("div", "program-empty"); empty.append(el("div", "reel", "🎬")); empty.append(el("div", null, p ? "No render yet" : "Open or create a project")); - empty.append(el("div", "pj-meta", "Generate from the menu or timeline")); + empty.append(el("div", "pj-meta", "Use Generate in the timeline header")); canvas.append(empty); } // generation progress overlay (visible even when there's already media). Built once per diff --git a/movie_editor/frontend/styles.css b/movie_editor/frontend/styles.css index 913a5ea4..5ed23652 100644 --- a/movie_editor/frontend/styles.css +++ b/movie_editor/frontend/styles.css @@ -179,6 +179,14 @@ button, input, select, textarea { font-family: inherit; color: var(--text); } font-family: var(--font-mono); font-size: 11px; letter-spacing: 2px; text-transform: uppercase; color: var(--muted); } .zone-title::before { content: ""; display: inline-block; width: 6px; height: 6px; margin-right: 9px; border-radius: 1px; background: var(--amber); transform: translateY(-1px); box-shadow: 0 0 7px var(--amber-soft); } +.transport-actions { display: flex; align-items: center; gap: 6px; flex: 0 0 auto; } +.transport-actions .btn.compact { padding: 3px 10px; font-size: 12px; font-family: var(--font-mono); line-height: 1.35; } +.transport-actions .btn:disabled { opacity: 0.42; pointer-events: none; cursor: default; } +.transport-actions .btn.render { + background: linear-gradient(#3ab3a8, #2d9a90); color: #0a1a18; border-color: #3ab3a8; + font-weight: 600; box-shadow: 0 4px 14px -6px color-mix(in srgb, var(--teal) 55%, transparent); +} +.transport-actions .btn.render:hover { filter: brightness(1.08); } .zone-head-right { margin-left: auto; font-family: var(--font-mono); font-size: 11px; color: var(--faint); display: flex; gap: 10px; align-items: center; flex-wrap: wrap; justify-content: flex-end; } .zone-body { flex: 1; overflow: auto; padding: 12px; min-height: 0; }