335 lines
9.2 KiB
JavaScript
335 lines
9.2 KiB
JavaScript
import { app } from "../../scripts/app.js";
|
|
import { api } from "../../scripts/api.js";
|
|
|
|
const NODE_NAME = "FunPackTemplateManager";
|
|
const NONE_TEMPLATE = "-None-";
|
|
const BUTTON_NAMES = new Set([
|
|
"funpack_template_save",
|
|
"funpack_template_update",
|
|
"funpack_template_delete",
|
|
"funpack_template_export",
|
|
"funpack_template_import",
|
|
"funpack_template_refresh",
|
|
]);
|
|
|
|
let cachedTemplates = [NONE_TEMPLATE];
|
|
let latestNodeData = null;
|
|
const trackedNodes = new Set();
|
|
let pendingRefresh = null;
|
|
|
|
function widgetByName(node, name) {
|
|
return (node.widgets || []).find((widget) => widget.name === name);
|
|
}
|
|
|
|
function hideWidget(widget) {
|
|
if (!widget || widget.__funpackHidden) {
|
|
return;
|
|
}
|
|
widget.__funpackHidden = true;
|
|
widget.computeSize = () => [0, -4];
|
|
widget.type = "hidden";
|
|
}
|
|
|
|
function templateWidget(node) {
|
|
return widgetByName(node, "template");
|
|
}
|
|
|
|
function actionWidget(node) {
|
|
return widgetByName(node, "action");
|
|
}
|
|
|
|
function fitString(ctx, text, maxWidth) {
|
|
text = String(text ?? "");
|
|
if (ctx.measureText(text).width <= maxWidth) {
|
|
return text;
|
|
}
|
|
const ellipsis = "...";
|
|
let low = 0;
|
|
let high = text.length;
|
|
while (low < high) {
|
|
const mid = Math.ceil((low + high) / 2);
|
|
if (ctx.measureText(text.slice(0, mid) + ellipsis).width <= maxWidth) {
|
|
low = mid;
|
|
} else {
|
|
high = mid - 1;
|
|
}
|
|
}
|
|
return text.slice(0, low) + ellipsis;
|
|
}
|
|
|
|
function updateTemplateWidget(node, values = cachedTemplates) {
|
|
const widget = templateWidget(node);
|
|
if (!widget) {
|
|
return;
|
|
}
|
|
const current = widget.value;
|
|
const nextValues = current && !values.includes(current) ? [...values, current] : values;
|
|
if (widget.options) {
|
|
widget.options.values = nextValues;
|
|
}
|
|
if (!widget.value || !nextValues.includes(widget.value)) {
|
|
widget.value = nextValues[0] || NONE_TEMPLATE;
|
|
}
|
|
}
|
|
|
|
async function fetchTemplates() {
|
|
try {
|
|
const response = await api.fetchApi(`/funpack/templates?cache_bust=${Date.now()}`, {
|
|
cache: "no-store",
|
|
});
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP ${response.status}`);
|
|
}
|
|
const data = await response.json();
|
|
const values = Array.isArray(data.templates) && data.templates.length ? data.templates : [NONE_TEMPLATE];
|
|
cachedTemplates = values;
|
|
return values;
|
|
} catch (error) {
|
|
console.warn("FunPack: failed to refresh template list", error);
|
|
return cachedTemplates;
|
|
}
|
|
}
|
|
|
|
async function refreshNode(node) {
|
|
const values = await fetchTemplates();
|
|
updateTemplateWidget(node, values);
|
|
node.setDirtyCanvas(true, true);
|
|
}
|
|
|
|
async function refreshTrackedNodes() {
|
|
if (pendingRefresh) {
|
|
return pendingRefresh;
|
|
}
|
|
pendingRefresh = (async () => {
|
|
const values = await fetchTemplates();
|
|
for (const node of [...trackedNodes]) {
|
|
if (!node?.graph) {
|
|
trackedNodes.delete(node);
|
|
continue;
|
|
}
|
|
updateTemplateWidget(node, values);
|
|
node.setDirtyCanvas(true, true);
|
|
}
|
|
})().finally(() => {
|
|
pendingRefresh = null;
|
|
});
|
|
return pendingRefresh;
|
|
}
|
|
|
|
function scheduleRefresh() {
|
|
window.setTimeout(() => {
|
|
void refreshTrackedNodes();
|
|
}, 100);
|
|
}
|
|
|
|
function wrapRefreshFunction(functionName) {
|
|
const original = app[functionName];
|
|
if (typeof original !== "function" || original.__funpackTemplateWrapped) {
|
|
return;
|
|
}
|
|
const wrapped = function () {
|
|
const result = original.apply(this, arguments);
|
|
Promise.resolve(result).finally(() => scheduleRefresh());
|
|
return result;
|
|
};
|
|
wrapped.__funpackTemplateWrapped = true;
|
|
app[functionName] = wrapped;
|
|
}
|
|
|
|
function hookComfyRefreshControls() {
|
|
for (const functionName of ["refreshComboInNodes", "refreshComboInNode", "refreshNodeDefs"]) {
|
|
wrapRefreshFunction(functionName);
|
|
}
|
|
}
|
|
|
|
function isRefreshControl(element) {
|
|
for (let current = element; current && current !== document.body; current = current.parentElement) {
|
|
const text = [
|
|
current.title,
|
|
current.ariaLabel,
|
|
current.textContent,
|
|
].filter(Boolean).join(" ").toLowerCase();
|
|
if (text.includes("refresh")) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
async function queueGraph() {
|
|
if (typeof app.queuePrompt === "function") {
|
|
return await app.queuePrompt(0);
|
|
}
|
|
const button = document.querySelector("#queue-button, .queue-button, button[title='Queue Prompt']");
|
|
if (button) {
|
|
button.click();
|
|
}
|
|
}
|
|
|
|
async function runTemplateAction(node, action) {
|
|
const widget = actionWidget(node);
|
|
if (!widget) {
|
|
return;
|
|
}
|
|
widget.value = action;
|
|
node.setDirtyCanvas(true, true);
|
|
try {
|
|
await queueGraph();
|
|
} finally {
|
|
window.setTimeout(() => {
|
|
widget.value = "load";
|
|
node.setDirtyCanvas(true, true);
|
|
scheduleRefresh();
|
|
}, 250);
|
|
}
|
|
}
|
|
|
|
async function exportTemplates() {
|
|
const response = await api.fetchApi(`/funpack/templates/export?cache_bust=${Date.now()}`, {
|
|
cache: "no-store",
|
|
});
|
|
if (!response.ok) {
|
|
throw new Error(`Export failed with HTTP ${response.status}`);
|
|
}
|
|
const blob = await response.blob();
|
|
const url = URL.createObjectURL(blob);
|
|
const link = document.createElement("a");
|
|
link.href = url;
|
|
link.download = "funpack_templates.json";
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
link.remove();
|
|
URL.revokeObjectURL(url);
|
|
}
|
|
|
|
function importTemplates() {
|
|
const input = document.createElement("input");
|
|
input.type = "file";
|
|
input.accept = ".json,application/json";
|
|
input.onchange = async () => {
|
|
const file = input.files?.[0];
|
|
if (!file) {
|
|
return;
|
|
}
|
|
try {
|
|
const data = JSON.parse(await file.text());
|
|
const response = await api.fetchApi("/funpack/templates/import", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(data),
|
|
});
|
|
if (!response.ok) {
|
|
const error = await response.json().catch(() => ({}));
|
|
throw new Error(error.error || `Import failed with HTTP ${response.status}`);
|
|
}
|
|
await refreshTrackedNodes();
|
|
} catch (error) {
|
|
console.warn("FunPack: template import failed", error);
|
|
app.canvas?.prompt?.("Template import failed", error.message || String(error), () => {});
|
|
}
|
|
};
|
|
input.click();
|
|
}
|
|
|
|
class FunPackTemplateButton {
|
|
constructor(name, label, callback) {
|
|
this.name = name;
|
|
this.type = "custom";
|
|
this.options = { serialize: false };
|
|
this.value = "";
|
|
this.label = label;
|
|
this.callback = callback;
|
|
}
|
|
|
|
computeSize(width) {
|
|
return [width, LiteGraph.NODE_WIDGET_HEIGHT + 2];
|
|
}
|
|
|
|
draw(ctx, _node, width, y, height) {
|
|
const x = 20;
|
|
const w = width - 40;
|
|
const h = height - 2;
|
|
ctx.save();
|
|
ctx.beginPath();
|
|
ctx.roundRect(x, y + 1, w, h, [3]);
|
|
ctx.fillStyle = LiteGraph.WIDGET_BGCOLOR;
|
|
ctx.strokeStyle = LiteGraph.WIDGET_OUTLINE_COLOR;
|
|
ctx.fill();
|
|
ctx.stroke();
|
|
ctx.fillStyle = LiteGraph.WIDGET_TEXT_COLOR;
|
|
ctx.textAlign = "center";
|
|
ctx.textBaseline = "middle";
|
|
ctx.fillText(fitString(ctx, this.label, w - 12), width / 2, y + height / 2);
|
|
ctx.restore();
|
|
}
|
|
|
|
mouse(event, _pos, node) {
|
|
if (event.type === "pointerup") {
|
|
this.callback(node);
|
|
return true;
|
|
}
|
|
return event.type === "pointerdown";
|
|
}
|
|
}
|
|
|
|
function removeTemplateButtons(node) {
|
|
node.widgets = (node.widgets || []).filter((widget) => !BUTTON_NAMES.has(widget.name));
|
|
}
|
|
|
|
function addTemplateButtons(node) {
|
|
removeTemplateButtons(node);
|
|
const buttons = [
|
|
new FunPackTemplateButton("funpack_template_save", "Save", (currentNode) => void runTemplateAction(currentNode, "save")),
|
|
new FunPackTemplateButton("funpack_template_update", "Update", (currentNode) => void runTemplateAction(currentNode, "update")),
|
|
new FunPackTemplateButton("funpack_template_delete", "Delete", (currentNode) => void runTemplateAction(currentNode, "delete")),
|
|
new FunPackTemplateButton("funpack_template_export", "Export", () => void exportTemplates()),
|
|
new FunPackTemplateButton("funpack_template_import", "Import", () => importTemplates()),
|
|
new FunPackTemplateButton("funpack_template_refresh", "Refresh", (currentNode) => void refreshNode(currentNode)),
|
|
];
|
|
for (const button of buttons) {
|
|
node.addCustomWidget(button);
|
|
}
|
|
}
|
|
|
|
function setupTemplateNode(node, nodeData) {
|
|
latestNodeData = nodeData;
|
|
trackedNodes.add(node);
|
|
hideWidget(actionWidget(node));
|
|
updateTemplateWidget(node);
|
|
addTemplateButtons(node);
|
|
void refreshNode(node);
|
|
}
|
|
|
|
app.registerExtension({
|
|
name: "funpack.templateManager",
|
|
setup() {
|
|
hookComfyRefreshControls();
|
|
window.setTimeout(hookComfyRefreshControls, 0);
|
|
window.setTimeout(hookComfyRefreshControls, 1000);
|
|
document.addEventListener("click", (event) => {
|
|
if (isRefreshControl(event.target)) {
|
|
scheduleRefresh();
|
|
}
|
|
}, true);
|
|
},
|
|
beforeRegisterNodeDef(nodeType, nodeData) {
|
|
if (nodeData.name !== NODE_NAME) {
|
|
return;
|
|
}
|
|
latestNodeData = nodeData;
|
|
hookComfyRefreshControls();
|
|
|
|
const originalOnNodeCreated = nodeType.prototype.onNodeCreated;
|
|
nodeType.prototype.onNodeCreated = function () {
|
|
originalOnNodeCreated?.apply(this, arguments);
|
|
setupTemplateNode(this, nodeData);
|
|
};
|
|
|
|
const originalConfigure = nodeType.prototype.configure;
|
|
nodeType.prototype.configure = function (info) {
|
|
originalConfigure?.apply(this, arguments);
|
|
setupTemplateNode(this, nodeData, info);
|
|
};
|
|
},
|
|
});
|