Files
olivv-cs-ComfyUI-FunPack/app/legacy/custom_nodes.js
T
digital-garbageandClaude Sonnet 5 29ac44ffcd feat: v4 UI port Phase A start -- module categories, legacy frontend served live
Two pieces of the "port v4's exact UI onto v5's backend" plan:

- core/contract.py: new optional ModuleSpec.category field (closed set:
  continuity, guidance, conditioning, sampling, post, system), validated in
  core/schema.py, threaded through core/registry.py's FIELDS and
  to_manifest(). Presentation grouping, independent of STAGE (execution
  order) -- a module can be stage="sampling" and category="guidance" at
  once. Only the 4 existing modules that actually render a settings panel
  (alg, sharpen, appearance, full_control) got a category assigned; the
  other 12 have none to categorize (no SETTINGS block) and stay
  uncategorised, which is valid.

- v4's frontend (movie_editor/frontend/*, ~29k lines, 61 files) copied
  verbatim into app/legacy/. core/routes.py's "/" route now serves
  legacy/index.html instead of the composer entry point; a <base> tag in
  that file resolves its own relative script/link tags against
  /funpack/app/legacy/ regardless of what URL it's served at, so every
  file moved as a unit with zero per-file path edits. Verified live: all
  61 assets load 200 OK, the real v4 onboarding screen renders pixel-for-
  pixel (it's the actual DOM/CSS, not a reimplementation), and several v4
  API calls (/api/media, /api/projects, /api/git/status) already succeed
  against v5's real backend by coincidence of shared route naming -- the
  rest 404/405 as expected, since api.js's ~90 functions haven't been
  rewired to v5's actual routes yet (next piece of this plan).

The old composer entry point (app/index.html, app/composer/**) stays in
the tree, unreferenced, until a later cleanup pass once nothing imports it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-14 23:58:51 +03:00

235 lines
8.6 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.
// Settings ▸ Custom Nodes — install, update and remove ComfyUI node packs.
//
// Three git operations, not a catalogue: you supply the repository URL. Keeping it that
// plain is the honest shape — a curated list would imply vetting that is not happening.
//
// Every action ends in a ComfyUI restart being NEEDED, not performed: node packs register
// at import, so nothing that happens here takes effect until the server comes back. The
// panel says so rather than restarting under the user.
(function () {
const { el, clear } = window.dom;
const API = window.MovieEditorAPI;
let host = null;
let nodes = [];
let rootPath = "";
let busy = ""; // name (or "__install__") of whatever is running
let dirty = false; // something changed → a restart is needed
let error = "";
// name -> {checked, behind, ahead, reason}. Empty until the user asks: each entry costs a
// network round trip, and doing that on open would make the panel feel broken.
let checked = {};
let checking = false;
async function checkUpdates() {
if (checking || busy) return;
checking = true;
render();
try {
checked = (await API.customNodesCheck()).checked || {};
} catch (e) {
alert("Could not check for updates:\n\n" + (e?.message || e));
} finally {
checking = false;
render();
}
}
async function refresh() {
try {
const res = await API.customNodes();
nodes = res.nodes || [];
rootPath = res.root || "";
error = "";
} catch (e) {
nodes = [];
error = e?.message || String(e);
}
render();
}
// A pack's requirements install can fail while the pack itself is fine. That is worth
// interrupting for — it is the difference between "restart and use it" and "it will not
// import until you run this".
function reportRequirements(res, what) {
const r = res && res.requirements;
if (!r || !r.ran || r.ok) return;
alert(`${what}, but installing its dependencies failed.\n\n${r.detail || ""}`);
}
async function run(key, fn, what) {
if (busy) return;
busy = key;
render();
try {
const res = await fn();
reportRequirements(res, what);
dirty = true;
// Whatever this was, its behind-count is now stale. Dropping it beats showing a
// number that was true a moment ago.
if (key !== "__install__") delete checked[key];
} catch (e) {
alert(`${what} failed:\n\n${e?.message || e}`);
} finally {
busy = "";
await refresh();
}
}
function addDialog() {
const overlay = el("div", "modal-overlay");
const box = el("div", "modal cn-modal");
const head = el("div", "modal-head");
head.append(el("div", "modal-title", "Add a custom node"));
box.append(head);
const content = el("div", "modal-content");
content.append(el("div", "cn-lead", "Paste the repository's git URL."));
const input = el("input", "lib-in");
input.type = "text";
input.placeholder = "https://github.com/owner/repo";
input.spellcheck = false;
content.append(input);
content.append(el("div", "cn-hint",
"It is cloned into custom_nodes and its requirements.txt is installed, if it has one. "
+ "Nothing about the repository is checked first — install what you trust."));
const acts = el("div", "lib-form-actions");
const ok = el("button", "btn primary tiny", "Install");
ok.type = "button";
const cancel = el("button", "btn ghost tiny", "Cancel");
cancel.type = "button";
const close = () => overlay.remove();
cancel.onclick = close;
ok.onclick = () => {
const url = input.value.trim();
if (!url) return;
close();
run("__install__", () => API.customNodeInstall(url), `Installing ${url}`);
};
input.onkeydown = (e) => {
if (e.key === "Enter") { e.preventDefault(); ok.click(); }
if (e.key === "Escape") { e.preventDefault(); close(); }
};
acts.append(cancel, ok);
content.append(acts);
box.append(content);
overlay.append(box);
overlay.addEventListener("click", (e) => { if (e.target === overlay) close(); });
document.body.append(overlay);
setTimeout(() => input.focus(), 0);
}
function row(n) {
const r = el("div", "cn-row" + (n.is_funpack ? " self" : ""));
const left = el("div", "cn-row-text");
left.append(el("div", "cn-name", n.name));
const bits = [];
if (n.is_funpack) bits.push("FunPack itself");
if (n.git) bits.push(`${n.branch || "?"} · ${n.commit || "?"}`);
else bits.push("not a git checkout");
const chk = checked[n.name];
if (chk && chk.checked) {
if (chk.behind > 0) bits.push(`${chk.behind} behind`);
else if (chk.ahead > 0) bits.push(`${chk.ahead} ahead of origin`);
else bits.push("up to date");
} else if (chk && chk.reason) {
bits.push(`not compared — ${chk.reason}`);
}
left.append(el("div", "cn-sub", bits.join(" — ")));
r.append(left);
const acts = el("div", "cn-row-acts");
if (chk && chk.checked && chk.behind > 0) r.classList.add("behind");
if (!n.is_funpack) {
const label = busy === n.name ? "…"
: (chk && chk.checked && chk.behind > 0) ? `Update (${chk.behind})` : "Update";
const up = el("button", "btn ghost tiny" + (chk?.behind > 0 ? " primary" : ""), label);
up.type = "button";
up.disabled = !!busy || !n.git;
up.title = n.git ? `git pull in ${n.name}` : "Not a git checkout — nothing to pull";
up.onclick = () => run(n.name, () => API.customNodeUpdate(n.name), `Updating ${n.name}`);
const rm = el("button", "btn ghost tiny danger", "Remove");
rm.type = "button";
rm.disabled = !!busy;
// The path is in the prompt on purpose: this deletes a directory and cannot be undone.
rm.onclick = () => {
if (!confirm(`Delete this folder and everything in it?\n\n${rootPath}/${n.name}\n\n`
+ "This cannot be undone.")) return;
run(n.name, () => API.customNodeRemove(n.name), `Removing ${n.name}`);
};
acts.append(up, rm);
}
r.append(acts);
return r;
}
function render() {
if (!host) return;
clear(host);
const bar = el("div", "cn-bar");
const add = el("button", "btn primary tiny", "+ Add node");
add.type = "button";
add.disabled = !!busy;
add.onclick = addDialog;
bar.append(add);
const chkBtn = el("button", "btn ghost tiny", checking ? "Checking…" : "Check for updates");
chkBtn.type = "button";
chkBtn.disabled = checking || !!busy;
chkBtn.title = "Fetch each pack's origin and report how far behind it is";
chkBtn.onclick = checkUpdates;
bar.append(chkBtn);
if (busy) bar.append(el("span", "cn-busy", busy === "__install__"
? "Cloning and installing…" : `Working on ${busy}…`));
else if (checking) bar.append(el("span", "cn-busy",
"Fetching each pack — this talks to the network."));
host.append(bar);
if (dirty) {
const note = el("div", "cn-restart");
note.append(el("span", null,
"Node packs are registered when ComfyUI starts, so these changes are not live yet."));
const btn = el("button", "btn tiny", "Restart ComfyUI");
btn.type = "button";
// FunPackGit owns the restart: it flushes pending edits and puts up the
// wait-for-reload overlay, which a bare POST would skip.
btn.onclick = () => window.FunPackGit?.restartComfy?.();
note.append(btn);
host.append(note);
}
if (error) {
host.append(el("div", "pj-meta", "Could not read custom_nodes: " + error));
return;
}
if (!nodes.length) {
host.append(el("div", "pj-meta", "No custom node packs installed."));
return;
}
const list = el("div", "cn-list");
nodes.forEach((n) => list.append(row(n)));
host.append(list);
host.append(el("div", "cn-hint", rootPath));
}
function mount(body) {
host = el("div", "cn-mount");
body.append(host);
dirty = false;
checked = {};
render();
refresh();
return () => { host = null; };
}
window.SettingsWindow.register({
id: "customnodes", group: "System", order: 3, title: "Custom Nodes",
subtitle: "Install, update and remove ComfyUI node packs.",
keywords: "custom nodes packs manager install update remove git clone extension",
iconBg: "linear-gradient(180deg,#8fb8ff,#4a72c8)",
icon: '<svg viewBox="0 0 16 16" width="13" height="13" fill="none" stroke="#fff" stroke-width="1.4"><rect x="2.2" y="2.2" width="4.6" height="4.6" rx="1"/><rect x="9.2" y="2.2" width="4.6" height="4.6" rx="1"/><rect x="2.2" y="9.2" width="4.6" height="4.6" rx="1"/><path d="M11.5 9.6v4.6M9.2 11.9h4.6"/></svg>',
mount,
});
})();