Files
tritant-ComfyUI_CreaPrompt/js/dynamique-ui.js
T
2026-07-14 12:41:10 +02:00

439 lines
18 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 { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
// ============================================================================
// 🛠️ FONCTIONS UTILITAIRES (Helpers)
// ============================================================================
function updateNodeSize(node) {
const [minWidth, minHeight] = node.computeSize();
const currentWidth = (node.size && node.size[0] > 10) ? node.size[0] : 220;
node.setSize([currentWidth, minHeight]);
node.graph?.setDirtyCanvas(true, true);
}
async function getCsvMapping() {
try {
const res = await fetch("/custom_nodes/creaprompt/csv_list");
if (!res.ok) throw new Error("Erreur liste CSV");
const files = await res.json();
const fileMap = {};
for (const f of files) {
const base = f.replace(/^\d+_\d*/, "").replace(/\.csv$/, "");
fileMap[base] = f;
}
return { fileMap, allFiles: files };
} catch (e) {
return { fileMap: {}, allFiles: [] };
}
}
async function fetchCsvValues(filename) {
try {
const res = await fetch(`/custom_nodes/creaprompt/csv/${filename}`);
if (!res.ok) return ["disabled", "🎲random"];
const text = await res.text();
const lines = text.split("\n").map(l => l.trim()).filter(Boolean);
return ["disabled", "🎲random", ...lines];
} catch (e) {
return ["disabled", "🎲random"];
}
}
async function syncComboWidget(node, label, selectedValue, csvFilename) {
const values = await fetchCsvValues(csvFilename);
if (!values.includes(selectedValue) && selectedValue !== "disabled" && selectedValue !== "🎲random") {
values.push(selectedValue);
}
const existingWidget = node.widgets ? node.widgets.find(w => w.name === label) : null;
if (existingWidget) {
existingWidget.options.values = values;
existingWidget.value = selectedValue;
node._crea_dynamicValues[label] = selectedValue;
} else {
const widget = node.addWidget("combo", label, selectedValue, (val) => {
node._crea_dynamicValues[label] = val;
node._crea_updateCsvJson();
}, { values: values, serialize: false });
}
node._crea_dynamicValues[label] = selectedValue;
}
function cleanupWidgets(node, targetConfigKeys) {
if (!node.widgets) return;
const protectedWidgets = [
"__csv_json",
"control_after_generate",
"preview_method",
"seed",
"Prompt_count",
"CreaPrompt_Collection",
"Choose_collection",
"Enhancer",
"Enhancer_precision",
"Enhancer_preset",
"separator_top",
"separator_bottom"
];
for (let i = node.widgets.length - 1; i >= 0; i--) {
const w = node.widgets[i];
if (w.type === "CUSTOM_SPACER") continue;
if (protectedWidgets.includes(w.name)) continue;
if (w.type === "combo") {
if (!targetConfigKeys.includes(w.name)) {
node.widgets.splice(i, 1);
}
}
}
}
async function loadDefaultConfig(node) {
if (node._crea_is_restored || Object.keys(node._crea_dynamicValues).length > 0) return;
try {
const res = await fetch("/custom_nodes/creaprompt/presets/default_combos.txt");
if (!res.ok) return;
const text = await res.text();
const labels = text.split("\n").map(l => l.trim()).filter(Boolean);
const { fileMap } = await getCsvMapping();
cleanupWidgets(node, labels);
for (const label of labels) {
if (node._crea_is_restored) break;
const csvFile = fileMap[label];
if (csvFile) {
await syncComboWidget(node, label, "disabled", csvFile);
}
}
node._crea_updateCsvJson();
updateNodeSize(node);
} catch (e) { console.warn(e); }
}
// Position du dernier clic : fallback pour les menus en Nodes 2.0,
// où les callbacks de boutons ne reçoivent pas l'événement souris.
let _creaLastPointer = { x: 100, y: 100 };
document.addEventListener("pointerdown", (ev) => {
_creaLastPointer = { x: ev.clientX, y: ev.clientY };
}, true);
function showFloatingMenu(items, onClickItem, clickX, clickY, title = "Menu") {
const oldMenu = document.getElementById("crea_prompt_floating_menu");
if (oldMenu) oldMenu.remove();
const menu = document.createElement("div");
menu.id = "crea_prompt_floating_menu";
Object.assign(menu.style, {
position: "fixed", left: `${clickX}px`, top: `${clickY}px`,
background: "#222", color: "#fff", border: "1px solid #666",
padding: "0", zIndex: 9999, borderRadius: "4px",
boxShadow: "0 4px 12px rgba(0,0,0,0.5)",
maxHeight: "60vh", overflowY: "auto", minWidth: "200px",
fontFamily: "sans-serif", fontSize: "13px"
});
const header = document.createElement("div");
header.innerHTML = `<b>${title}</b> <span style="float:right; cursor:pointer">❌</span>`;
Object.assign(header.style, {
padding: "6px 10px", borderBottom: "1px solid #444", background: "#333",
position: "sticky", top: "0"
});
header.querySelector("span").onclick = () => menu.remove();
menu.appendChild(header);
if (items.length === 0) {
menu.appendChild(Object.assign(document.createElement("div"), {textContent: "Vide", style:"padding:10px"}));
} else {
items.forEach(item => {
const div = document.createElement("div");
div.textContent = item.label || item;
Object.assign(div.style, { padding: "5px 10px", cursor: "pointer" });
div.onmouseover = () => div.style.background = "#444";
div.onmouseout = () => div.style.background = "transparent";
div.onclick = () => { onClickItem(item); menu.remove(); };
menu.appendChild(div);
});
}
document.body.appendChild(menu);
}
function addCustomSpacer(node, name, title) {
node.widgets.push({
name: name,
type: "CUSTOM_SPACER",
serialize: false,
draw: (ctx, node, width, y) => {
const rectHeight = 20, marginY = 5, x_padding = 10;
ctx.fillStyle = "#272";
ctx.fillRect(x_padding, y + marginY, width - (x_padding * 2), rectHeight);
ctx.fillStyle = "#CCC";
ctx.font = "bold 12px Arial";
ctx.textAlign = "center";
const textY = y + marginY + rectHeight / 2 + 4;
ctx.fillText(title, width / 2, textY);
},
computeSize: () => [0, 30]
});
}
// ============================================================================
// 🧩 EXTENSION PRINCIPALE
// ============================================================================
app.registerExtension({
name: "CreaPrompt_UI",
beforeRegisterNodeDef(nodeType, nodeData) {
if (nodeData.name !== "CreaPrompt_0") return;
const origCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
origCreated?.apply(this, arguments);
const node = this;
node._crea_dynamicValues = {};
node._crea_is_restored = false;
node._crea_load_timer = null;
// --- WIDGET JSON ---
const jsonWidget = node.widgets.find(w => w.name === "__csv_json");
node._crea_updateCsvJson = function() {
if (jsonWidget) jsonWidget.value = JSON.stringify(node._crea_dynamicValues);
};
// ⚡⚡ CORRECTION DU BUG D'AFFICHAGE AU SCROLL ⚡⚡
// Au lieu de le cacher une seule fois, on force le masquage à chaque redessin du node.
// Cela résout le problème où ComfyUI recrée le DOM quand le node revient à l'écran.
if (jsonWidget) {
jsonWidget.computeSize = () => [0, -4]; // Réduit la taille logique
const origDrawForeground = node.onDrawForeground;
node.onDrawForeground = function (ctx) {
if (origDrawForeground) origDrawForeground.apply(this, arguments);
// On vérifie et on cache l'élément DOM s'il est visible
if (jsonWidget.inputEl) {
if (jsonWidget.inputEl.style.display !== "none") {
jsonWidget.inputEl.style.display = "none";
}
// Parfois ComfyUI met le widget dans un parent qui a des marges/bordures
if (jsonWidget.inputEl.parentElement && jsonWidget.inputEl.parentElement.style.display !== "none") {
jsonWidget.inputEl.parentElement.style.display = "none";
}
}
};
}
// Masquage compatible Nodes 2.0 (Vue)
_creaRegisterJsonHide(node, jsonWidget);
// 🕒 TIMEOUT
node._crea_load_timer = setTimeout(() => {
if (node._crea_is_restored) return;
const rawJson = jsonWidget ? jsonWidget.value : null;
if (rawJson && typeof rawJson === "string" && rawJson.trim().startsWith("{") && rawJson.trim() !== "{}") {
return;
}
loadDefaultConfig(node);
}, 100);
// ================= SPACER ENHANCER =================
addCustomSpacer(node, "separator_enhancer", "Enhancer");
{
const spIdx = node.widgets.findIndex(w => w.name === "separator_enhancer");
const enhIdx = node.widgets.findIndex(w => w.name === "Enhancer");
if (spIdx > -1 && enhIdx > -1) {
const [sp] = node.widgets.splice(spIdx, 1);
// Sérialisation SYMÉTRIQUE : le spacer est un widget normal avec une valeur.
// Il occupe le même slot dans widgets_values au serialize ET au configure,
// donc aucun décalage positionnel des widgets Enhancer.
delete sp.serialize;
delete sp.options;
sp.value = "";
node.widgets.splice(enhIdx, 0, sp);
}
}
// ================= 1. SPACER HAUT =================
addCustomSpacer(node, "separator_top", "Preset Actions");
// ================= BUTTONS =================
node.addWidget("button", "💾 Save Categories Preset", "", async () => {
const name = prompt("Name:");
if (!name || name.length < 2) return;
try {
await fetch("/custom_nodes/creaprompt/save_preset", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: name.trim(), content: JSON.stringify(node._crea_dynamicValues, null, 2) })
});
} catch (e) { alert("Error " + e.message); }
});
node.addWidget("button", "📂 Load Categories Preset", "", async (v, a, n, p, e) => {
const cx = e?.clientX ?? _creaLastPointer.x; const cy = e?.clientY ?? _creaLastPointer.y;
try {
const r = await fetch("/custom_nodes/creaprompt/presets_list");
const f = await r.json();
const menuItems = f
.filter(x => x.endsWith(".txt") && x !== "default_combos.txt")
.map(filename => ({
label: filename.replace(/\.txt$/, ""),
filename: filename
}));
showFloatingMenu(menuItems, async (item) => {
const r2 = await fetch(`/custom_nodes/creaprompt/presets/${item.filename}`);
const parsed = JSON.parse(await r2.text());
const targetKeys = Object.keys(parsed);
node._crea_dynamicValues = {};
cleanupWidgets(node, targetKeys);
const { fileMap } = await getCsvMapping();
for (const [k, val] of Object.entries(parsed)) {
if(fileMap[k]) await syncComboWidget(node, k, val, fileMap[k]);
}
node._crea_updateCsvJson();
updateNodeSize(node);
}, cx, cy, "📂 Load");
} catch (err) { alert("Error"); }
});
node.addWidget("button", "🗑️ Delete Categories Preset", "", async (v, a, n, p, e) => {
const cx = e?.clientX ?? _creaLastPointer.x; const cy = e?.clientY ?? _creaLastPointer.y;
try {
const r = await fetch("/custom_nodes/creaprompt/presets_list");
const f = await r.json();
const menuItems = f
.filter(x => x.endsWith(".txt") && x !== "default_combos.txt")
.map(filename => ({
label: filename.replace(/\.txt$/, ""),
filename: filename
}));
showFloatingMenu(menuItems, async (item) => {
if(confirm(`Delete ${item.label}?`)) await fetch(`/custom_nodes/creaprompt/delete_preset/${item.filename}`, {method:"DELETE"});
}, cx, cy, "🗑️ Delete");
} catch (err) {}
});
node.addWidget("button", "➖ Remove Category", "", (v, a, n, p, e) => {
const keys = Object.keys(node._crea_dynamicValues);
if(!keys.length) return;
showFloatingMenu(keys, (k) => {
const idx = node.widgets.findIndex(w => w.name === k);
if(idx>-1) node.widgets.splice(idx, 1);
delete node._crea_dynamicValues[k];
node._crea_updateCsvJson();
updateNodeSize(node);
}, e?.clientX ?? _creaLastPointer.x, e?.clientY ?? _creaLastPointer.y, "➖ Remove");
});
node.addWidget("button", "🧹 Remove All", "", () => {
if(!confirm("Remove all?")) return;
cleanupWidgets(node, []);
node._crea_dynamicValues = {};
node._crea_updateCsvJson();
updateNodeSize(node);
});
node.addWidget("button", "➕ Add Category", "", async (v, a, n, p, e) => {
const cx = e?.clientX ?? _creaLastPointer.x; const cy = e?.clientY ?? _creaLastPointer.y;
const { fileMap } = await getCsvMapping();
const used = Object.keys(node._crea_dynamicValues);
const items = Object.keys(fileMap).filter(k => !used.includes(k)).map(k => ({label: k, file: fileMap[k]}));
showFloatingMenu(items, async (i) => {
await syncComboWidget(node, i.label, "disabled", i.file);
node._crea_updateCsvJson();
updateNodeSize(node);
}, cx, cy, "➕ Add");
});
// ================= 2. SPACER BAS =================
addCustomSpacer(node, "separator_bottom", "Dynamic Categories");
};
},
async loadedGraphNode(node, def) {
if (node.type !== "CreaPrompt_0") return;
if (node._crea_load_timer) {
clearTimeout(node._crea_load_timer);
node._crea_load_timer = null;
}
node._crea_is_restored = true;
const jsonWidget = node.widgets.find(w => w.name === "__csv_json");
if (jsonWidget && jsonWidget.value && jsonWidget.value !== "{}") {
try {
const savedConfig = JSON.parse(jsonWidget.value);
const targetKeys = Object.keys(savedConfig);
cleanupWidgets(node, targetKeys);
node._crea_dynamicValues = savedConfig;
const { fileMap } = await getCsvMapping();
for (const [label, val] of Object.entries(savedConfig)) {
if (fileMap[label]) await syncComboWidget(node, label, val, fileMap[label]);
}
updateNodeSize(node);
} catch (e) { console.error("JSON error", e); }
} else {
await loadDefaultConfig(node);
}
}
});
// ============================================================================
// Nodes 2.0 (Vue) : masquage du widget __csv_json via CSS injecte.
// Le widget __csv_json est le PREMIER widget declare du node, donc sa row Vue
// est le :first-child de .lg-node-widgets. Une regle CSS par node id masque
// la row AVANT son rendu : aucun observer, aucun re-masquage apres coup,
// donc aucun clignotement pendant le pan (le DOM virtualise monte deja cache).
// En 1.0 il n'existe aucun element [node-id] : les regles sont inertes, et le
// masquage canvas (computeSize/onDrawForeground) continue de faire le travail.
// ============================================================================
const _creaHiddenIds = new Set();
let _creaStyleEl = null;
function _creaUpdateHideCss() {
if (!_creaStyleEl) {
_creaStyleEl = document.createElement("style");
_creaStyleEl.id = "creaprompt-hide-csv-json";
document.head.appendChild(_creaStyleEl);
}
const rules = [..._creaHiddenIds].map(id =>
`.lg-node-widgets:has([node-id="${id}"]) > .lg-node-widget:first-child { display: none !important; }`
).join("\n");
if (_creaStyleEl.textContent !== rules) _creaStyleEl.textContent = rules;
}
function _creaRegisterJsonHide(node, _jsonWidget) {
// Au nodeCreated l'id n'est pas encore attribue (-1) : retries courts.
const tryAdd = (attempt = 0) => {
if (node.id != null && node.id !== -1) {
_creaHiddenIds.add(node.id);
_creaUpdateHideCss();
} else if (attempt < 50) {
setTimeout(() => tryAdd(attempt + 1), 100);
}
};
tryAdd();
}