Files
relhamdi-ComfyUI-RE-CustomU…/web/style_loader.js
T
2026-05-31 16:14:45 +02:00

339 lines
10 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.
import { hideWidget, registerNode, waitForWidgets } from "./utils.js";
// --- Constants ---
const NODE_NAME = "StyleLoader";
const BASE_ENDPOINT = "/re-customutils/styles";
const NO_STYLE = "-- no styles found --";
const NO_VAE = "none";
const MAX_LORAS = 15;
// --- Assets ---
const ASSETS = {
checkpoints: [],
vae: [],
loras: [],
samplers: [],
schedulers: [],
};
const fetchAssets = async () => {
try {
const res = await fetch(`${BASE_ENDPOINT}/assets`);
const data = await res.json();
Object.assign(ASSETS, data);
} catch (e) {
console.warn(`[${NODE_NAME}] Failed to fetch assets:`, e);
}
};
fetchAssets();
// --- Widget helpers ---
const findWidget = (node, name) => node.widgets?.find((w) => w.name === name);
const setWidgetValue = (widget, value) => {
if (!widget) return;
widget.value = value;
widget.callback?.(value);
};
// --- LoRA management ---
// Each LoRA slot: { comboWidget, weightWidget, removeWidget, hidden }
const getLoraSlots = (node) => node._loraSlots ?? [];
const serializeLoras = (node) => {
return getLoraSlots(node)
.filter((slot) => !slot.hidden && slot.comboWidget.value)
.map((slot) => ({
name: slot.comboWidget.value,
weight: slot.weightWidget.value,
}));
};
const persistLoras = (node) => {
const lorasWidget = findWidget(node, "loras_data");
if (lorasWidget) {
lorasWidget.value = JSON.stringify(serializeLoras(node));
}
};
const hideSlot = (node, slot) => {
slot.hidden = true;
hideWidget(slot.comboWidget);
hideWidget(slot.weightWidget);
hideWidget(slot.removeWidget);
persistLoras(node);
if (node.graph) node.graph.setDirtyCanvas(true, true);
};
const addLoraSlot = (node, addButtonWidget, name = "", weight = 0.8) => {
const slots = node._loraSlots ?? [];
// Reuse a hidden slot if available
const reusable = slots.find((s) => s.hidden);
if (reusable) {
reusable.hidden = false;
reusable.comboWidget.value = name;
reusable.comboWidget.type = "combo";
reusable.comboWidget.computeSize = null;
reusable.weightWidget.value = weight;
reusable.weightWidget.type = "number";
reusable.weightWidget.computeSize = null;
reusable.removeWidget.type = "button";
reusable.removeWidget.computeSize = null;
persistLoras(node);
if (node.graph) node.graph.setDirtyCanvas(true, true);
return;
}
if (slots.length >= MAX_LORAS) {
alert(`[${NODE_NAME}] Maximum ${MAX_LORAS} LoRAs reached.`);
return;
}
// Insert before the Add button
const addBtnIdx = node.widgets.indexOf(addButtonWidget);
const comboWidget = node.addWidget(
"combo",
`lora_name_${slots.length}`,
name || ASSETS.loras[0] || "",
() => persistLoras(node),
{ values: ASSETS.loras },
);
if (name) comboWidget.value = name;
const weightWidget = node.addWidget(
"number",
`lora_weight_${slots.length}`,
weight,
() => persistLoras(node),
{ min: 0.0, max: 2.0, step: 0.5, precision: 2 },
);
const removeWidget = node.addWidget("button", `✕ Remove`, null, () =>
hideSlot(node, slot),
);
// Move the three new widgets before the Add button
if (addBtnIdx !== -1) {
const w = node.widgets;
const combo = w.pop();
const weight_ = w.pop();
const remove = w.pop();
w.splice(addBtnIdx, 0, combo, weight_, remove);
}
const slot = { comboWidget, weightWidget, removeWidget, hidden: false };
slots.push(slot);
node._loraSlots = slots;
persistLoras(node);
if (node.graph) node.graph.setDirtyCanvas(true, true);
};
const rebuildLoraSlots = (node, loras, addButtonWidget) => {
// Hide all existing slots first
for (const slot of getLoraSlots(node)) {
hideSlot(node, slot);
}
// Rebuild from data
for (const { name, weight } of loras) {
addLoraSlot(node, addButtonWidget, name, weight ?? 0.8);
}
};
// --- JSON <-> Widgets sync ---
const buildJsonFromWidgets = (node) => {
return JSON.stringify(
{
checkpoint: findWidget(node, "checkpoint")?.value ?? "",
vae: findWidget(node, "vae")?.value ?? NO_VAE,
clip_skip: findWidget(node, "clip_skip")?.value ?? -2,
quality_tags: findWidget(node, "quality_tags")?.value ?? "",
negative_tags: findWidget(node, "negative_tags")?.value ?? "",
steps: findWidget(node, "steps")?.value ?? 15,
refiner_step: findWidget(node, "refiner_step")?.value ?? 24,
cfg: findWidget(node, "cfg")?.value ?? 4.0,
loras: serializeLoras(node),
},
null,
2,
);
};
const pushJsonToWidgets = (node, data, addButtonWidget) => {
const safe = (key, fallback) =>
data[key] !== undefined ? data[key] : fallback;
setWidgetValue(findWidget(node, "checkpoint"), safe("checkpoint", ""));
setWidgetValue(findWidget(node, "vae"), safe("vae", NO_VAE));
setWidgetValue(findWidget(node, "clip_skip"), safe("clip_skip", -2));
setWidgetValue(findWidget(node, "quality_tags"), safe("quality_tags", ""));
setWidgetValue(
findWidget(node, "negative_tags"),
safe("negative_tags", ""),
);
setWidgetValue(findWidget(node, "steps"), safe("steps", 15));
setWidgetValue(findWidget(node, "refiner_step"), safe("refiner_step", 24));
setWidgetValue(findWidget(node, "cfg"), safe("cfg", 4.0));
// Rebuild LoRA slots
const lorasDataWidget = findWidget(node, "loras_data");
const hasPersistedLoras =
lorasDataWidget?.value && JSON.parse(lorasDataWidget.value).length > 0;
if (!hasPersistedLoras) {
rebuildLoraSlots(node, safe("loras", []), addButtonWidget);
}
if (node.graph) node.graph.setDirtyCanvas(true, true);
};
// --- File loading ---
const loadFileIntoWidgets = async (file, node, addButtonWidget) => {
if (!file || file === NO_STYLE) return;
try {
const res = await fetch(
`${BASE_ENDPOINT}/load?file=${encodeURIComponent(file)}`,
);
const data = await res.json();
if (data.error) {
console.warn(`[${NODE_NAME}] Load error:`, data.error);
return;
}
const parsed = JSON.parse(data.content);
pushJsonToWidgets(node, parsed, addButtonWidget);
} catch (e) {
console.warn(`[${NODE_NAME}] Failed to load or parse file:`, e);
}
};
// --- Buttons ---
const flashButton = (node, buttonName, flashLabel) => {
const btn = findWidget(node, buttonName);
if (!btn) return;
btn.name = flashLabel;
if (node.graph) node.graph.setDirtyCanvas(true, true);
setTimeout(() => {
btn.name = buttonName;
if (node.graph) node.graph.setDirtyCanvas(true, true);
}, 1500);
};
const addButtons = (node, styleFileWidget, addButtonWidget) => {
// Save
node.addWidget("button", "💾 Save", null, async () => {
const file = styleFileWidget.value;
if (!file || file === NO_STYLE) {
alert(`[${NODE_NAME}] No style file selected.`);
return;
}
const content = buildJsonFromWidgets(node);
const res = await fetch(`${BASE_ENDPOINT}/save`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ file, content }),
});
const data = await res.json();
if (data.ok) {
flashButton(node, "💾 Save", "✅ Saved!");
} else {
alert(`[${NODE_NAME}] Save failed: ${data.error}`);
}
});
// New
node.addWidget("button", "➕ New", null, async () => {
const name = prompt("New style file name (e.g. anime/illustrious):");
if (!name?.trim()) return;
const res = await fetch(`${BASE_ENDPOINT}/new`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ file: name.trim() }),
});
const data = await res.json();
if (data.ok) {
const values = styleFileWidget.options?.values ?? [];
const placeholderIdx = values.indexOf(NO_STYLE);
if (placeholderIdx !== -1) values.splice(placeholderIdx, 1);
if (!values.includes(data.file)) {
values.push(data.file);
values.sort();
styleFileWidget.options.values = values;
}
styleFileWidget.value = data.file;
const parsed = JSON.parse(data.content);
pushJsonToWidgets(node, parsed, addButtonWidget);
if (node.graph) node.graph.setDirtyCanvas(true, true);
} else {
alert(`[${NODE_NAME}] ${data.error}`);
}
});
};
// --- Attach ---
const attachStyleLoader = (node) => {
const styleFileWidget = findWidget(node, "style_file");
if (!styleFileWidget) return;
// Add LoRA button (added before Save/New so slots insert above it)
const addButtonWidget = node.addWidget(
"button",
"➕ Add LoRA",
null,
() => {
addLoraSlot(node, addButtonWidget);
},
);
// Save / New buttons
addButtons(node, styleFileWidget, addButtonWidget);
// Restore LoRA slots from persisted loras_data
const lorasDataWidget = findWidget(node, "loras_data");
if (lorasDataWidget?.value) {
try {
const loras = JSON.parse(lorasDataWidget.value);
if (loras.length) rebuildLoraSlots(node, loras, addButtonWidget);
} catch (e) {
console.warn(`[${NODE_NAME}] Failed to restore LoRA slots:`, e);
}
}
if (lorasDataWidget) {
lorasDataWidget.hidden = true;
}
// Load initial file
loadFileIntoWidgets(styleFileWidget.value, node, addButtonWidget);
// Reload on file change
const originalCallback = styleFileWidget.callback;
styleFileWidget.callback = function (value) {
if (originalCallback) originalCallback.call(this, value);
loadFileIntoWidgets(value, node, addButtonWidget);
};
};
// --- Registration ---
registerNode(NODE_NAME, (node) => waitForWidgets(node, attachStyleLoader));