Files
olivv-cs-ComfyUI-FunPack/movie_editor/frontend/settings_window.js
T
digital-garbage 9791cb8ccd Add FunPack Easy Gen (simplified single-scene UI) and a /funpack Hub
Easy Gen is a new Fooocus-style frontend at /funpack/easy — big preview,
one prompt box, Upload, Generate, and an Advanced settings panel — built
entirely on the Movie Editor's existing project format and API (no new
backend endpoints), so projects stay interchangeable between the two UIs.

The Hub at /funpack links to both UIs and surfaces version/branch/commit
plus an Update/Restart action reusing the Editor's git_update.py flow.

Startup now prints full host:port URLs for all three (Hub, Editor, Easy
Gen) via a new config.comfy_display_url() helper.

Also: settings_window.js's About section now labels itself via
window.FunPackAppName instead of hardcoding "Cutting Room", and Easy
Gen's boot-error banner accumulates distinct errors instead of
overwriting, so a cascading failure (e.g. dom.js not loading) surfaces
its root cause instead of just the last script in load order.
2026-07-23 16:37:26 +03:00

338 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Unified Settings window, macOS System Settings style: a sidebar of grouped,
// searchable sections (colored icon chips) + one content pane. Sections self-register
// via SettingsWindow.register(); the legacy per-modal facades (EditorSettingsModal,
// EngineSettingsModal, ModelsModal, TempBrowserModal) deep-link into their section,
// so every existing entry point (menus, inspector, workflow import) keeps working.
(function () {
const { el, clear } = window.dom;
const GROUPS = ["", "Generation", "Learning", "System"];
const registry = [];
let overlay = null, activeId = null, cleanup = null, query = "";
let navEl, bodyEl, titleEl, subEl, actionsEl;
function register(spec) {
registry.push(spec);
if (overlay) renderNav();
}
// Sidebar order: group order, then explicit spec.order, then registration order.
function orderedSections() {
const out = [];
GROUPS.forEach((g) => {
out.push(...registry
.filter((s) => (s.group || "") === g)
.sort((a, b) => (a.order ?? 99) - (b.order ?? 99)));
});
return out;
}
function matches(spec) {
if (!query) return true;
return (spec.title + " " + (spec.subtitle || "") + " " + (spec.keywords || ""))
.toLowerCase().includes(query);
}
function iconEl(spec) {
const w = el("span", "sw-icon");
if (spec.iconBg) w.style.background = spec.iconBg;
w.innerHTML = spec.icon || "";
return w;
}
function renderNav() {
clear(navEl);
const ordered = orderedSections();
GROUPS.forEach((g) => {
const items = ordered.filter((s) => (s.group || "") === g && matches(s));
if (!items.length) return;
if (g) navEl.append(el("div", "sw-group-title", g));
items.forEach((s) => {
const it = el("div", "sw-item" + (s.id === activeId ? " active" : ""));
it.append(iconEl(s), el("span", "sw-item-label", s.title));
it.onclick = () => show(s.id);
navEl.append(it);
});
});
if (!navEl.children.length) navEl.append(el("div", "sw-nav-empty", "No matches"));
}
function teardownSection() {
if (typeof cleanup === "function") { try { cleanup(); } catch (_) {} }
cleanup = null;
}
function show(id) {
const spec = registry.find((s) => s.id === id) || orderedSections()[0];
if (!spec || !overlay) return;
teardownSection();
activeId = spec.id;
titleEl.textContent = spec.title;
subEl.textContent = spec.subtitle || "";
subEl.hidden = !spec.subtitle;
clear(actionsEl);
clear(bodyEl);
// flush sections manage their own inner layout/scrolling (e.g. Models' sidebar)
bodyEl.classList.toggle("sw-flush", !!spec.flush);
bodyEl.scrollTop = 0;
const ctx = {
setActions: (nodes) => { clear(actionsEl); (nodes || []).forEach((n) => actionsEl.append(n)); },
openSection: (sid) => show(sid),
};
cleanup = spec.mount(bodyEl, ctx) || null;
renderNav();
}
function close() {
teardownSection();
if (overlay) overlay.remove();
overlay = null; activeId = null;
}
function open(id) {
// Recover if something removed our overlay from the DOM directly
// (e.g. the tour's closeModalOverlay sweep) without calling close().
if (overlay && !overlay.isConnected) { close(); }
if (overlay) { show(id || activeId); return; }
query = "";
overlay = el("div", "modal-overlay sw-overlay");
const win = el("div", "settings-win");
const side = el("div", "sw-side");
const search = el("input", "sw-search");
search.type = "search"; search.placeholder = "Search"; search.autocomplete = "off";
search.oninput = () => { query = search.value.trim().toLowerCase(); renderNav(); };
navEl = el("div", "sw-nav");
side.append(search, navEl);
const main = el("div", "sw-main");
const head = el("div", "sw-head");
const ht = el("div", "sw-head-text");
titleEl = el("div", "sw-head-title");
subEl = el("div", "sw-head-sub");
ht.append(titleEl, subEl);
actionsEl = el("div", "sw-head-actions");
const x = el("button", "btn ghost tiny sw-close", "✕");
x.onclick = close;
head.append(ht, actionsEl, x);
bodyEl = el("div", "sw-body");
main.append(head, bodyEl);
win.append(side, main);
overlay.append(win);
overlay.addEventListener("click", (e) => { if (e.target === overlay) close(); });
document.body.append(overlay);
show(id);
}
window.addEventListener("keydown", (e) => {
if (overlay && e.key === "Escape") { close(); return; }
if ((e.metaKey || e.ctrlKey) && !e.altKey && e.key === ",") {
e.preventDefault();
overlay ? close() : open();
}
});
// Shared builder for inner-sidebar nav items (Models node list, Engine categories):
// { icon | dot, label, sub, badge, active, onClick } → .mn-item element.
function navItem(opts) {
const it = el("div", "mn-item" + (opts.active ? " active" : ""));
if (opts.dot) it.append(el("span", "mn-dot " + opts.dot));
else if (opts.icon) it.append(el("span", "mn-ico", opts.icon));
const lab = el("span", "mn-label");
lab.append(el("span", "mn-name", opts.label));
if (opts.sub) lab.append(el("span", "mn-sub", opts.sub));
it.append(lab);
if (opts.badge != null) it.append(el("span", "mn-badge", String(opts.badge)));
it.onclick = opts.onClick;
return it;
}
window.SettingsWindow = { open, close, register, navItem };
// ── shared row builders for settings panels ────────────────────────────
function actionRow(title, hint, btnLabel, onClick, opts = {}) {
const row = el("div", "sw-row");
const main = el("div", "sw-row-main");
main.append(el("div", "sw-row-title", title));
if (hint) main.append(el("div", "sw-row-hint", hint));
row.append(main);
const btn = el("button", "btn ghost tiny" + (opts.danger ? " danger" : ""), btnLabel);
btn.type = "button";
if (opts.disabled) btn.disabled = true;
btn.onclick = onClick;
row.append(btn);
return row;
}
function infoRow(title, value, ok) {
const row = el("div", "sw-row");
const main = el("div", "sw-row-main");
main.append(el("div", "sw-row-title", title));
row.append(main);
const val = el("span", "sw-row-value", value);
if (ok != null) {
const led = el("span", "sw-led " + (ok ? "ok" : "bad"));
row.append(led);
}
row.append(val);
return row;
}
// ── built-in section: About FunPack ────────────────────────────────────
register({
id: "about", group: "", order: 0, title: "About FunPack",
subtitle: "",
keywords: "about version commit branch copyright info funpack cutting room",
iconBg: "linear-gradient(180deg,#ffc36b,#e0891f)",
icon: '<svg viewBox="0 0 16 16" width="13" height="13" fill="none" stroke="#fff" stroke-width="1.5"><circle cx="8" cy="8" r="6.2"/><circle cx="8" cy="8" r="2.4" fill="#fff" stroke="none"/></svg>',
mount(body, ctx) {
const G = window.FunPackGit;
const wrap = el("div", "sw-about");
body.append(wrap);
function render() {
clear(wrap);
const git = G ? G.get() : null;
wrap.append(el("div", "sw-about-mark", "◉"));
wrap.append(el("div", "sw-about-name", "FunPack"));
// window.FunPackAppName lets a different frontend sharing this file (e.g. Easy
// Gen) relabel the app name without forking the whole section; Editor leaves it
// unset and keeps "Cutting Room".
wrap.append(el("div", "sw-about-sub", window.FunPackAppName || "Cutting Room"));
const facts = el("div", "sw-about-facts");
const fact = (k, v) => {
const r = el("div", "sw-about-fact");
r.append(el("span", "sw-about-k", k), el("span", "sw-about-v", v || "—"));
facts.append(r);
};
fact("Version", git?.version);
fact("Commit", git?.ok ? git.commit + (git.dirty ? " (local changes)" : "") : null);
fact("Branch", git?.ok ? git.branch : null);
wrap.append(facts);
const upd = el("button", "btn ghost tiny", "Software Update…");
upd.onclick = () => ctx.openSection("system");
wrap.append(upd);
wrap.append(el("div", "sw-about-copy", "© 2025–2026 DigitalGarbage"));
}
render();
if (G?.refresh) G.refresh().then(() => { if (wrap.isConnected) render(); }).catch(() => {});
},
});
// ── built-in section: Refinement & Taste (Learning) ────────────────────
// Buttons reuse the single implementations exported by menubar.js
// (window.FunPackMaintenance) — no duplicated logic, just a second door.
register({
id: "refinement", group: "Learning", title: "Refinement & Taste",
subtitle: "Learned-taste state: refinement keys and the Absolute global-taste store.",
keywords: "refinement key export import delete clear absolute taste store learning ratings",
iconBg: "linear-gradient(180deg,#ff8bc2,#d84f92)",
icon: '<svg viewBox="0 0 16 16" width="13" height="13"><path d="M8 1.5 9.6 6.4 14.5 8 9.6 9.6 8 14.5 6.4 9.6 1.5 8 6.4 6.4 8 1.5z" fill="#fff"/></svg>',
mount(body) {
const M = window.FunPackMaintenance || {};
const S = window.Store;
const wrap = el("div", "sw-stack");
body.append(wrap);
function render() {
clear(wrap);
wrap.append(el("div", "sw-hint",
"Refinement keys hold what FunPack has learned from your ratings for a named session. "
+ "They are stored on this ComfyUI instance as <key>.json plus sidecar banks."));
const st = S ? S.get() : {};
const studioOn = !!window.PipelineCaps?.usesFunpackStudio?.(st);
const armed = !!st.resetSessionArmed;
wrap.append(el("div", "sw-rows-label", "Studio session"));
const sess = el("div", "sw-rows");
sess.append(actionRow("Reset Studio session",
armed
? "Armed — the session's learned keys are wiped on the FIRST run of the next generation. Click to cancel."
: "Wipe this session's learned keys on the next generation (asks which keys first).",
armed ? "✓ Armed — cancel" : "Reset…",
() => S?.resetStudioSession?.(),
{ disabled: !st.project || !studioOn, danger: !armed }));
wrap.append(sess);
wrap.append(el("div", "sw-rows-label", "Refinement keys"));
const rows = el("div", "sw-rows");
rows.append(actionRow("Export refinement key", "Download a key as <key>.json.", "⬇ Export…", () => M.exportRefinementKey?.()));
rows.append(actionRow("Import refinement key", "Load a previously exported <key>.json onto this instance.", "Import…", () => M.importRefinementKeyFile?.()));
wrap.append(rows);
wrap.append(el("div", "sw-rows-label", "Danger zone"));
const danger = el("div", "sw-rows");
danger.append(actionRow("Delete refinement key",
"Removes a key AND all its sidecars (value function, blessed banks, creativity latent, velocity memory).",
"Delete…", () => M.deleteRefinementKey?.(), { danger: true }));
danger.append(actionRow("Clear global taste store",
"Wipes the Absolute store pooled from every rated run (applied in absolute/both steer mode).",
"Clear…", () => M.clearGlobalTaste?.(), { danger: true }));
wrap.append(danger);
}
const unsub = S ? S.subscribe(render) : null;
render();
return () => { if (unsub) unsub(); };
},
});
// ── built-in section: Updates & ComfyUI (System) ───────────────────────
register({
id: "system", group: "System", order: 1, title: "Updates & ComfyUI",
subtitle: "Server connection, FunPack code updates, pipeline health.",
keywords: "update git branch restart comfyui server connection health template version",
iconBg: "linear-gradient(180deg,#a7adb8,#6b7280)",
icon: '<svg viewBox="0 0 16 16" width="13" height="13" fill="none" stroke="#fff" stroke-width="1.5" stroke-linecap="round"><circle cx="8" cy="8" r="2.2"/><path d="M8 1.6v2M8 12.4v2M1.6 8h2M12.4 8h2M3.5 3.5l1.4 1.4M11.1 11.1l1.4 1.4M12.5 3.5l-1.4 1.4M4.9 11.1l-1.4 1.4"/></svg>',
mount(body) {
const S = window.Store, G = window.FunPackGit, M = window.FunPackMaintenance || {};
const wrap = el("div", "sw-stack");
body.append(wrap);
function render() {
clear(wrap);
const st = S ? S.get() : {};
const git = G ? G.get() : null;
const gitOk = !!(git && git.ok);
wrap.append(el("div", "sw-rows-label", "ComfyUI server"));
const srv = el("div", "sw-rows");
srv.append(infoRow("Connection", st.health?.ok ? "Connected · " + window.location.host : "Not reachable", !!st.health?.ok));
srv.append(actionRow("Open ComfyUI", "The node-graph UI, in a new tab.", "↗ Open", () => window.open("/", "_blank")));
srv.append(actionRow("Restart ComfyUI", "Down for ~10–40s; any running generation is lost. Reloads automatically.", "⟳ Restart…", () => M.restartComfy?.(), { danger: true }));
wrap.append(srv);
wrap.append(el("div", "sw-rows-label", "FunPack code"));
const code = el("div", "sw-rows");
code.append(infoRow("Branch", gitOk
? git.branch + (git.commit ? " @ " + git.commit : "") + (git.dirty ? " (local changes)" : "")
: (git?.detail || "unavailable")));
code.append(actionRow("Switch branch", "Check out another FunPack branch and reload.", "Switch…",
() => G?.switchBranch?.(), { disabled: !gitOk || !!git?.dirty }));
code.append(actionRow("Update FunPack",
gitOk && git.behind > 0 ? git.behind + " new commit(s) available." : "Pull the latest code and reload.",
gitOk && git.behind > 0 ? git.behind + "↓ Update" : "⬇⟳ Update",
() => G?.update?.(), { disabled: !gitOk || !!git?.dirty }));
wrap.append(code);
wrap.append(el("div", "sw-rows-label", "Pipeline health"));
const health = el("div", "sw-rows");
health.append(infoRow("Workflow template", st.health?.reference_loaded ? "loaded" : "missing", !!st.health?.reference_loaded));
health.append(infoRow("Configured nodes", String(st.health?.configured_slots ?? 0)));
wrap.append(health);
}
const unsub = S ? S.subscribe(render) : null;
render();
if (G?.refresh) G.refresh().then(() => { if (wrap.isConnected) render(); }).catch(() => {});
return () => { if (unsub) unsub(); };
},
});
})();