532 lines
22 KiB
JavaScript
532 lines
22 KiB
JavaScript
import { app } from "../../scripts/app.js";
|
|
import { $el } from "../../scripts/ui.js";
|
|
import { api } from "../../scripts/api.js";
|
|
|
|
const NODE_TYPES = {
|
|
"Efficient Loader 💬ED": "checkpoints",
|
|
"LoRA Stacker 💬ED": "loras",
|
|
"Embedding Stacker 💬ED": "embeddings"
|
|
};
|
|
|
|
const WIDGET_NAMES = {
|
|
checkpoints: "ckpt_name",
|
|
loras: "lora_name",
|
|
embeddings: "embedding_",
|
|
};
|
|
|
|
const IMAGE_WIDTH = 384;
|
|
const IMAGE_HEIGHT = 384;
|
|
|
|
const NO_IMAGE_ICON = `url("data:image/svg+xml,%3C%3Fxml version='1.0'%3F%3E%3Csvg xmlns='http://www.w3.org/2000/svg' height='300px' width='300px' version='1.1' viewBox='-300 -300 600 600' font-family='Bitstream Vera Sans,Liberation Sans, Arial, sans-serif' font-size='72' text-anchor='middle'%3E%3Ccircle stroke='%23AAA' stroke-width='10' r='280' fill='%23FFF'/%3E%3Cswitch style='fill:%23444;'%3E%3Ctext id='trsvg3-bn' systemLanguage='bn'%3E%3Ctspan x='0' y='-8' id='trsvg1-bn'%3E&%23x99B;&%23x9AC;&%23x9BF; &%23x989;&%23x9AA;&%23x9B2;&%23x9AD;&%23x9CD;&%23x9AF;%3C/tspan%3E%3Ctspan x='0' y='80' id='trsvg2-bn'%3E&%23x9A8;&%23x9DF;%3C/tspan%3E%3C/text%3E%3Ctext id='trsvg3-de' systemLanguage='de'%3E%3Ctspan x='0' y='-8' id='trsvg1-de'%3EKEIN BILD%3C/tspan%3E%3Ctspan x='0' y='80' id='trsvg2-de'%3EVERF&%23220;GBAR%3C/tspan%3E%3C/text%3E%3Ctext id='trsvg3-en' systemLanguage='en'%3E%3Ctspan x='0' y='-8' id='trsvg1-en'%3ENO IMAGE%3C/tspan%3E%3Ctspan x='0' y='80' id='trsvg2-en'%3EAVAILABLE%3C/tspan%3E%3C/text%3E%3Ctext id='trsvg3-es' systemLanguage='es'%3E%3Ctspan x='0' y='-8' id='trsvg1-es'%3EIMAGEN NO%3C/tspan%3E%3Ctspan x='0' y='80' id='trsvg2-es'%3EDISPONIBLE%3C/tspan%3E%3C/text%3E%3Ctext id='trsvg3-id' systemLanguage='id'%3E%3Ctspan x='0' y='-8' id='trsvg1-id'%3EGAMBAR TAK%3C/tspan%3E%3Ctspan x='0' y='80' id='trsvg2-id'%3ETERSEDIA%3C/tspan%3E%3C/text%3E%3Ctext id='trsvg3-ko' systemLanguage='ko'%3E%3Ctspan x='0' y='-8' id='trsvg1-ko'%3E&%23xC774;&%23xBBF8;&%23xC9C0;&%23xAC00;%3C/tspan%3E%3Ctspan x='0' y='80' id='trsvg2-ko'%3E&%23xC5C6;&%23xC2B5;&%23xB2C8;&%23xB2E4;%3C/tspan%3E%3C/text%3E%3Ctext id='trsvg3-ro' systemLanguage='ro'%3E%3Ctspan x='0' y='-8' id='trsvg1-ro'%3EIMAGINE%3C/tspan%3E%3Ctspan x='0' y='80' id='trsvg2-ro'%3EINDISPONIBIL&%23x102;%3C/tspan%3E%3C/text%3E%3Ctext id='trsvg3-tr' systemLanguage='tr'%3E%3Ctspan x='0' y='-8' id='trsvg1-tr'%3EG&%23xD6;R&%23xDC;NT&%23xDC;%3C/tspan%3E%3Ctspan x='0' y='80' id='trsvg2-tr'%3EYOK%3C/tspan%3E%3C/text%3E%3Ctext id='trsvg3-vi' systemLanguage='vi'%3E%3Ctspan x='0' y='-8' id='trsvg1-vi'%3EKH&%23xD4;NG%3C/tspan%3E%3Ctspan x='0' y='80' id='trsvg2-vi'%3EC&%23xD3; &%23x1EA2;NH%3C/tspan%3E%3C/text%3E%3Ctext id='trsvg3-ba' systemLanguage='ba'%3E%3Ctspan x='0' y='-8' id='trsvg1-ba'%3E&%23x4BA;&%23x4AE;&%23x420;&%23x4D8;&%23x422;&%23x422;&%23x4D8;&%23x420;%3C/tspan%3E%3Ctspan x='0' y='80' id='trsvg2-ba'%3E&%23x42E;&%23x4A0;%3C/tspan%3E%3C/text%3E%3Ctext id='trsvg3-ang' systemLanguage='ang'%3E%3Ctspan x='0' y='-8' id='trsvg1-ang'%3EN&%23x100; BILDE%3C/tspan%3E%3Ctspan x='0' y='80' id='trsvg2-ang'%3E&%23x100;F&%23x112;R%3C/tspan%3E%3C/text%3E%3Ctext id='trsvg3-fr' systemLanguage='fr'%3E%3Ctspan x='0' y='-8' id='trsvg1-fr'%3EAUCUNE IMAGE%3C/tspan%3E%3Ctspan x='0' y='80' id='trsvg2-fr'%3EDISPONIBLE%3C/tspan%3E%3C/text%3E%3Ctext id='trsvg3'%3E%3Ctspan x='0' y='-8' id='trsvg1'%3ENO IMAGE%3C/tspan%3E%3Ctspan x='0' y='80' id='trsvg2'%3EAVAILABLE%3C/tspan%3E%3C/text%3E%3C/switch%3E%3C/svg%3E%0A")`
|
|
|
|
const FOLDER_ICON = `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' version='1.1' width='256' height='256' viewBox='0 0 256 256' xml:space='preserve'%3E%3Cdefs%3E%3C/defs%3E%3Cg style='stroke: none; stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: none; fill-rule: nonzero; opacity: 1;' transform='translate(1.4065934065934016 1.4065934065934016) scale(2.81 2.81)' %3E%3Cpath d='M 73.538 35.162 l -52.548 1.952 c -1.739 0 -2.753 0.651 -3.232 2.323 L 6.85 76.754 c -0.451 1.586 -2.613 2.328 -4.117 2.328 h 0 C 1.23 79.082 0 77.852 0 76.349 l 0 -10.458 V 23.046 v -2.047 v -6.273 c 0 -2.103 1.705 -3.808 3.808 -3.808 h 27.056 c 1.01 0 1.978 0.401 2.692 1.115 l 7.85 7.85 c 0.714 0.714 1.683 1.115 2.692 1.115 H 69.73 c 2.103 0 3.808 1.705 3.808 3.808 v 1.301 C 73.538 26.106 73.538 35.162 73.538 35.162 z' style='stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(224,173,49); fill-rule: nonzero; opacity: 1;' transform=' matrix(1 0 0 1 0 0) ' stroke-linecap='round' /%3E%3Cpath d='M 2.733 79.082 L 2.733 79.082 c 1.503 0 2.282 -1.147 2.733 -2.733 l 10.996 -38.362 c 0.479 -1.672 2.008 -2.824 3.748 -2.824 h 67.379 c 1.609 0 2.765 1.546 2.311 3.09 L 79.004 75.279 c -0.492 1.751 -1.571 3.818 -3.803 3.803 C 75.201 79.082 2.733 79.082 2.733 79.082 z' style='stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(255,200,67); fill-rule: nonzero; opacity: 1;' transform=' matrix(1 0 0 1 0 0) ' stroke-linecap='round' /%3E%3C/g%3E%3C/svg%3E")`
|
|
|
|
const GO_BACK_ICON = `url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='utf-8'%3F%3E%3C!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --%3E%3Csvg width='100px' height='100px' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg' fill='none'%3E%3Cpath stroke='%23a3a3a3' opacity='0.9' stroke-linecap='square' stroke-linejoin='round' stroke-width='0.8' d='M11 18h3.75a5.25 5.25 0 100-10.5H5M7.5 4L4 7.5 7.5 11'/%3E%3C/svg%3E")`
|
|
|
|
const NONE_ICON = `url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='utf-8'%3F%3E%3C!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --%3E%3Csvg width='100px' height='100px' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg' fill='none' opacity='0.9' stroke='%23a3a3a3' stroke-width='0.95' stroke-linecap='round' stroke-linejoin='miter'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Ccircle cx='12' cy='12' r='10' fill='' opacity='0.1'%3E%3C/circle%3E%3Cline x1='5' y1='5' x2='19' y2='19'%3E%3C/line%3E%3C/svg%3E")`
|
|
|
|
const PLUG_ICON = `url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='utf-8'%3F%3E%3C!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'%3E%3C!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --%3E%3Csvg width='100px' height='100px' viewBox='-2.5 0 32 32' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Cg id='icomoon-ignore'%3E%3C/g%3E%3Cpath d='M18.804 18.457l-8.268 8.268-0.004-0.005c-0.031 0.032-0.056 0.067-0.088 0.097-2.147 2.147-5.642 2.147-7.789 0-2.085-2.085-2.139-5.435-0.176-7.593l1.103 1.102 1.743-1.742c2.339 1.116 5.221 0.712 7.159-1.225l2.968-2.967 0.742 0.742 0.742-0.742-2.968-2.967 4.451-4.451-0.742-0.742-4.451 4.451-2.967-2.967 4.45-4.451-0.741-0.742-4.451 4.451-2.967-2.967-0.742 0.742 0.742 0.742-2.968 2.967c-1.937 1.937-2.341 4.82-1.224 7.159l-1.743 1.742 1.122 1.123c-2.372 2.568-2.317 6.583 0.176 9.076 2.556 2.557 6.716 2.557 9.272 0l8.361-8.36c1.832-1.832 4.813-1.832 6.645 0l0.742-0.742c-2.241-2.241-5.889-2.241-8.129 0zM4.324 9.2l2.968-2.967 7.417 7.417-2.967 2.967c-2.045 2.045-5.372 2.045-7.418 0s-2.045-5.372-0-7.418zM2.907 16.551c0.202 0.283 0.422 0.555 0.675 0.808s0.527 0.474 0.808 0.675l-0.808 0.808-1.483-1.483 0.808-0.808z' fill='%23a3a3a3' fill-opacity='0.9'%3E%3C/path%3E%3C/svg%3E")`
|
|
|
|
function getType(node) {
|
|
return NODE_TYPES[node.comfyClass] || "error";
|
|
}
|
|
|
|
function getWidgetName(type) {
|
|
return WIDGET_NAMES[type] || "error";
|
|
}
|
|
|
|
function encodeRFC3986URIComponent(str) {
|
|
return encodeURIComponent(str).replace(/[!'()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);
|
|
}
|
|
|
|
const calculateImagePosition = (el, bodyRect) => {
|
|
let { top, left, right } = el.getBoundingClientRect();
|
|
const { width: bodyWidth, height: bodyHeight } = bodyRect;
|
|
|
|
const isSpaceRight = right + IMAGE_WIDTH <= bodyWidth;
|
|
if (isSpaceRight) {
|
|
left = right;
|
|
} else {
|
|
left -= IMAGE_WIDTH;
|
|
}
|
|
|
|
top = top - IMAGE_HEIGHT / 2;
|
|
if (top + IMAGE_HEIGHT > bodyHeight) {
|
|
top = bodyHeight - IMAGE_HEIGHT;
|
|
}
|
|
if (top < 0) {
|
|
top = 0;
|
|
}
|
|
|
|
return { left: Math.round(left), top: Math.round(top), isLeft: !isSpaceRight };
|
|
};
|
|
|
|
function showImage(relativeToEl, imageEl) {
|
|
const bodyRect = document.body.getBoundingClientRect();
|
|
if (!bodyRect) return;
|
|
|
|
const { left, top, isLeft } = calculateImagePosition(relativeToEl, bodyRect);
|
|
|
|
imageEl.style.left = `${left}px`;
|
|
imageEl.style.top = `${top}px`;
|
|
|
|
if (isLeft) {
|
|
imageEl.classList.add("left");
|
|
} else {
|
|
imageEl.classList.remove("left");
|
|
}
|
|
|
|
document.body.appendChild(imageEl);
|
|
}
|
|
|
|
let imagesByType = {};
|
|
const loadImageList = async (type) => {
|
|
imagesByType[type] = await (await api.fetchApi(`/ed_nodes/images/${type}`)).json();
|
|
};
|
|
|
|
app.registerExtension({
|
|
name: "ED.BetterCombos",
|
|
init() {
|
|
app.ui.settings.addSetting({
|
|
"id": "ed.Thumbnails.DisplayMode",
|
|
"category": [
|
|
"Appearance",
|
|
"Thumbnails",
|
|
"DisplayMode"
|
|
],
|
|
"name": "💬ED Thumbnails display mode",
|
|
"type": "combo",
|
|
"options": [
|
|
"List (Normal)",
|
|
"Tree (Subfolders)",
|
|
"Thumbnails (grid)"
|
|
],
|
|
"defaultValue": "Thumbnails (grid)"
|
|
});
|
|
|
|
$el("style", {
|
|
textContent: `
|
|
.ed_nodes-combo-image {
|
|
position: absolute;
|
|
left: 0;
|
|
top: 0;
|
|
width: ${IMAGE_WIDTH}px;
|
|
height: ${IMAGE_HEIGHT}px;
|
|
object-fit: contain;
|
|
object-position: top left;
|
|
z-index: 9999;
|
|
}
|
|
.ed_nodes-combo-image.left {
|
|
object-position: top right;
|
|
}
|
|
.ed_nodes-combo-folder { opacity: 0.7 }
|
|
.ed_nodes-combo-folder-arrow { display: inline-block; width: 15px; }
|
|
.ed_nodes-combo-folder:hover { background-color: rgba(255, 255, 255, 0.1); }
|
|
.ed_nodes-combo-prefix { display: none }
|
|
|
|
/* Special handling for when the filter input is populated to revert to normal */
|
|
.litecontextmenu:has(input:not(:placeholder-shown)) .ed_nodes-combo-folder-contents {
|
|
display: block !important;
|
|
}
|
|
.litecontextmenu:has(input:not(:placeholder-shown)) .ed_nodes-combo-folder {
|
|
display: none;
|
|
}
|
|
.litecontextmenu:has(input:not(:placeholder-shown)) .ed_nodes-combo-prefix {
|
|
display: inline;
|
|
}
|
|
.litecontextmenu:has(input:not(:placeholder-shown)) .litemenu-entry {
|
|
padding-left: 2px !important;
|
|
}
|
|
|
|
/* Grid mode */
|
|
.ed_nodes-combo-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|
gap: 10px;
|
|
overflow-x: hidden;
|
|
max-width: 60vw;
|
|
}
|
|
.ed_nodes-combo-grid .comfy-context-menu-filter {
|
|
grid-column: 1 / -1;
|
|
position: sticky;
|
|
top: 0;
|
|
}
|
|
.ed_nodes-combo-grid .litemenu-entry {
|
|
word-break: break-word;
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
.ed_nodes-combo-grid .litemenu-entry:before {
|
|
content: "";
|
|
display: block;
|
|
width: 100%;
|
|
height: 250px;
|
|
background-size: contain;
|
|
background-position: center;
|
|
background-repeat: no-repeat;
|
|
/* No-image image attribution: Picture icons created by Pixel perfect - Flaticon */
|
|
background-image: var(--background-image, ${NO_IMAGE_ICON}) !important;
|
|
}
|
|
|
|
`,
|
|
parent: document.body,
|
|
});
|
|
const p1 = loadImageList("checkpoints");
|
|
const p2 = loadImageList("loras");
|
|
const p3 = loadImageList("embeddings");
|
|
|
|
const refreshComboInNodes = app.refreshComboInNodes;
|
|
app.refreshComboInNodes = async function () {
|
|
const r = await Promise.all([
|
|
refreshComboInNodes.apply(this, arguments),
|
|
loadImageList("checkpoints").catch(() => {}),
|
|
loadImageList("loras").catch(() => {}),
|
|
loadImageList("embeddings").catch(() => {}),
|
|
]);
|
|
return r[0];
|
|
};
|
|
|
|
const imageHost = $el("img.ed_nodes-combo-image");
|
|
|
|
const positionMenu = (menu, fillWidth) => {
|
|
// compute best position
|
|
let left = app.canvas.last_mouse[0] - 10;
|
|
let top = app.canvas.last_mouse[1] - 10;
|
|
|
|
const body_rect = document.body.getBoundingClientRect();
|
|
const root_rect = menu.getBoundingClientRect();
|
|
|
|
if (body_rect.width && left > body_rect.width - root_rect.width - 10) left = body_rect.width - root_rect.width - 10;
|
|
if (body_rect.height && top > body_rect.height - root_rect.height - 10) top = body_rect.height - root_rect.height - 10;
|
|
|
|
menu.style.left = `${left}px`;
|
|
menu.style.top = `${top}px`;
|
|
if (fillWidth) {
|
|
menu.style.right = "10px";
|
|
}
|
|
};
|
|
|
|
const updateMenu = async (menu, type, overWidget) => {
|
|
try {
|
|
await p1;
|
|
await p2;
|
|
await p3;
|
|
} catch (error) {
|
|
console.error(error);
|
|
console.error("Error loading ed.betterCombos data")
|
|
}
|
|
|
|
// Clamp max height so it doesn't overflow the screen
|
|
const position = menu.getBoundingClientRect();
|
|
const maxHeight = window.innerHeight - position.top - 20;
|
|
menu.style.maxHeight = `${maxHeight}px`;
|
|
|
|
const images = imagesByType[type];
|
|
const items = menu.querySelectorAll(".litemenu-entry");
|
|
|
|
// Add image handler to items
|
|
const addImageHandler = (item) => {
|
|
const text = item.getAttribute("data-value").trim();
|
|
if (images[text]) {
|
|
const textNode = document.createTextNode("*");
|
|
item.appendChild(textNode);
|
|
|
|
item.addEventListener(
|
|
"mouseover",
|
|
() => {
|
|
imageHost.src = `/ed_nodes/view/${encodeRFC3986URIComponent(images[text])}?${+new Date()}`;
|
|
document.body.appendChild(imageHost);
|
|
showImage(item, imageHost);
|
|
},
|
|
{ passive: true }
|
|
);
|
|
item.addEventListener(
|
|
"mouseout",
|
|
() => {
|
|
imageHost.remove();
|
|
},
|
|
{ passive: true }
|
|
);
|
|
item.addEventListener(
|
|
"click",
|
|
() => {
|
|
imageHost.remove();
|
|
},
|
|
{ passive: true }
|
|
);
|
|
}
|
|
};
|
|
|
|
const createTree = () => {
|
|
// Create a map to store folder structures
|
|
const folderMap = new Map();
|
|
const rootItems = [];
|
|
const splitBy = (navigator.platform || navigator.userAgent).includes("Win") ? /\/|\\/ : /\//;
|
|
const itemsSymbol = Symbol("items");
|
|
|
|
// First pass - organize items into folder structure
|
|
for (const item of items) {
|
|
const path = item.getAttribute("data-value").split(splitBy);
|
|
|
|
// Remove path from visible text
|
|
item.textContent = path[path.length - 1];
|
|
if (path.length > 1) {
|
|
// Add the prefix path back in so it can be filtered on
|
|
const prefix = $el("span.ed_nodes-combo-prefix", {
|
|
textContent: path.slice(0, -1).join("/") + "/",
|
|
});
|
|
item.prepend(prefix);
|
|
}
|
|
|
|
addImageHandler(item);
|
|
|
|
if (path.length === 1) {
|
|
rootItems.push(item);
|
|
continue;
|
|
}
|
|
|
|
// Temporarily remove the item from current position
|
|
item.remove();
|
|
|
|
// Create folder hierarchy
|
|
let currentLevel = folderMap;
|
|
for (let i = 0; i < path.length - 1; i++) {
|
|
const folder = path[i];
|
|
if (!currentLevel.has(folder)) {
|
|
currentLevel.set(folder, new Map());
|
|
}
|
|
currentLevel = currentLevel.get(folder);
|
|
}
|
|
|
|
// Store the actual item in the deepest folder
|
|
if (!currentLevel.has(itemsSymbol)) {
|
|
currentLevel.set(itemsSymbol, []);
|
|
}
|
|
currentLevel.get(itemsSymbol).push(item);
|
|
}
|
|
|
|
const createFolderElement = (name) => {
|
|
const folder = $el("div.litemenu-entry.ed_nodes-combo-folder", {
|
|
innerHTML: `<span class="ed_nodes-combo-folder-arrow">▶</span> ${name}`,
|
|
style: { paddingLeft: "5px" },
|
|
});
|
|
return folder;
|
|
};
|
|
|
|
const insertFolderStructure = (parentElement, map, level = 0) => {
|
|
for (const [folderName, content] of map.entries()) {
|
|
if (folderName === itemsSymbol) continue;
|
|
|
|
const folderElement = createFolderElement(folderName);
|
|
folderElement.style.paddingLeft = `${level * 10 + 5}px`;
|
|
parentElement.appendChild(folderElement);
|
|
|
|
const childContainer = $el("div.ed_nodes-combo-folder-contents", {
|
|
style: { display: "none" },
|
|
});
|
|
|
|
// Add items in this folder
|
|
const items = content.get(itemsSymbol) || [];
|
|
for (const item of items) {
|
|
item.style.paddingLeft = `${(level + 1) * 10 + 14}px`;
|
|
childContainer.appendChild(item);
|
|
}
|
|
|
|
// Recursively add subfolders
|
|
insertFolderStructure(childContainer, content, level + 1);
|
|
parentElement.appendChild(childContainer);
|
|
|
|
// Add click handler for folder
|
|
folderElement.addEventListener("click", (e) => {
|
|
e.stopPropagation();
|
|
const arrow = folderElement.querySelector(".ed_nodes-combo-folder-arrow");
|
|
const contents = folderElement.nextElementSibling;
|
|
if (contents.style.display === "none") {
|
|
contents.style.display = "block";
|
|
arrow.textContent = "▼";
|
|
} else {
|
|
contents.style.display = "none";
|
|
arrow.textContent = "▶";
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
insertFolderStructure(items[0].parentElement, folderMap);
|
|
positionMenu(menu);
|
|
};
|
|
|
|
const addImageData = (item) => {
|
|
const text = item.getAttribute("data-value").trim();
|
|
if (images[text]) {
|
|
item.style.setProperty("--background-image", `url(/ed_nodes/view/${encodeRFC3986URIComponent(images[text])}${dateNow})`);
|
|
}
|
|
};
|
|
|
|
// 현재 레벨의 리스트가 존재하지 않으면 생성하고, 뒤로 가기 아이콘을 추가
|
|
const checkCurrentLevelList = (currentLevel, item, folderDepth, path) => {
|
|
if (!currentLevel.list) {
|
|
currentLevel.list = [];
|
|
createGoBackIcon(currentLevel, item, folderDepth, path);
|
|
}
|
|
};
|
|
|
|
// 뒤로 가기 아이콘을 생성하는 함수
|
|
const createGoBackIcon = (currentLevel, item, folderDepth, path) => {
|
|
if (!folderDepth) return;
|
|
|
|
const value = path.slice(0, folderDepth - 1).join(splitBy);
|
|
const goBackIcon = createMenuEntry('Back to ' + (value || 'root'), value, GO_BACK_ICON);
|
|
|
|
insertMenuEntry(goBackIcon, item, currentLevel);
|
|
};
|
|
|
|
// 폴더 아이콘을 생성하는 함수
|
|
const createFolderIcon = (currentLevel, item, folderDepth, path) => {
|
|
const value = path.slice(0, folderDepth + 1).join(splitBy);
|
|
const folderIcon = createMenuEntry(value, value, FOLDER_ICON);
|
|
|
|
insertMenuEntry(folderIcon, item, currentLevel);
|
|
};
|
|
|
|
// 메뉴 엔트리를 생성하는 함수
|
|
const createMenuEntry = (text, value, icon) => {
|
|
const entry = $el("div.litemenu-entry.submenu", {
|
|
innerHTML: text,
|
|
value: value,
|
|
});
|
|
entry.style.setProperty("--background-image", icon);
|
|
entry.addEventListener("click", (e) => {
|
|
e.stopPropagation();
|
|
toggleFolderIcons(entry.value);
|
|
});
|
|
return entry;
|
|
};
|
|
|
|
// 생성된 메뉴 엔트리를 삽입하는 함수
|
|
const insertMenuEntry = (entry, item, currentLevel) => {
|
|
menu.insertBefore(entry, item);
|
|
folderList.push(entry);
|
|
currentLevel.list.push(entry);
|
|
};
|
|
|
|
// 아이콘을 업데이트하는 함수
|
|
const updateIcon = (item) => {
|
|
item.style.removeProperty("color");
|
|
item.style.removeProperty("background-color");
|
|
|
|
const iconMapping = {
|
|
"None": NONE_ICON,
|
|
"🔌 ext model input": PLUG_ICON
|
|
};
|
|
|
|
if (iconMapping[item.value]) {
|
|
item.style.setProperty("--background-image", iconMapping[item.value]);
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
// 폴더 계층 구조를 생성하는 함수
|
|
const buildFolderHierarchy = (items) => {
|
|
const tree = {};
|
|
|
|
for (const item of items) {
|
|
const path = item.getAttribute("data-value").trim().split(splitBy);
|
|
let currentLevel = tree;
|
|
|
|
path.forEach((folder, index) => {
|
|
if (index === path.length - 1) {
|
|
// 마지막 요소(파일)일 경우 리스트에 추가
|
|
checkCurrentLevelList(currentLevel, item, index, path);
|
|
currentLevel.list.push(item);
|
|
if (!updateIcon(item)) addImageData(item);
|
|
} else {
|
|
// 폴더일 경우 계층 구조를 생성
|
|
if (!currentLevel[folder]) {
|
|
currentLevel[folder] = {};
|
|
checkCurrentLevelList(currentLevel, item, index, path);
|
|
createFolderIcon(currentLevel, item, index, path);
|
|
}
|
|
currentLevel = currentLevel[folder];
|
|
}
|
|
});
|
|
}
|
|
return tree;
|
|
};
|
|
|
|
// 특정 폴더의 아이콘 목록을 가져오는 함수
|
|
const getFolderHierarchy = (folderPath, tree) => {
|
|
if (folderPath[0] && !tree[folderPath[0]]) {
|
|
console.log(">> folder Path (%s) is not exist!!", folderPath[0]);
|
|
folderPath = [];
|
|
}
|
|
if (!folderPath.length) return tree.list || [];
|
|
return getFolderHierarchy(folderPath.slice(1), tree[folderPath[0]] || {});
|
|
};
|
|
|
|
// 폴더 아이콘을 표시하거나 숨기는 함수
|
|
const toggleFolderIcons = (folderName) => {
|
|
const folderPath = folderName ? folderName.split(splitBy) : [];
|
|
|
|
[...items, ...folderList].forEach(el => el.style.display = "none");
|
|
getFolderHierarchy(folderPath, folderTree).forEach(icon => icon.style.display = "block");
|
|
};
|
|
|
|
const displaySetting = app.ui.settings.getSettingValue("ed.Thumbnails.DisplayMode");
|
|
const splitBy = (navigator.platform || navigator.userAgent).includes("Win") ? '\\' : '/';
|
|
const dateNow = "?t=" + Date.now();
|
|
let folderTree = {};
|
|
let folderList = [];
|
|
|
|
if (displaySetting === "List (Normal)") {
|
|
for (const item of items) {
|
|
addImageHandler(item);
|
|
}
|
|
} else if (displaySetting === "Tree (Subfolders)") {
|
|
createTree();
|
|
} else {
|
|
const widget_folder = overWidget.value.substring(0, overWidget.value.lastIndexOf(splitBy));
|
|
menu.classList.add("ed_nodes-combo-grid");
|
|
folderTree = buildFolderHierarchy(items);
|
|
toggleFolderIcons(widget_folder);
|
|
positionMenu(menu, true);
|
|
}
|
|
};
|
|
|
|
const mutationObserver = new MutationObserver((mutations) => {
|
|
const node = app.canvas.node_over;
|
|
|
|
if (!node || !NODE_TYPES[node.comfyClass]) {
|
|
return;
|
|
}
|
|
|
|
for (const mutation of mutations) {
|
|
for (const removed of mutation.removedNodes) {
|
|
if (removed.classList?.contains("litecontextmenu")) {
|
|
imageHost.remove();
|
|
}
|
|
}
|
|
|
|
for (const added of mutation.addedNodes) {
|
|
if (added.classList?.contains("litecontextmenu")) {
|
|
const overWidget = app.canvas.getWidgetAtCursor();
|
|
const type = getType(node);
|
|
if (overWidget?.name.includes(getWidgetName(type))) {
|
|
requestAnimationFrame(() => {
|
|
// Bad hack to prevent showing on right click menu by checking for the filter input
|
|
if (!added.querySelector(".comfy-context-menu-filter")) return;
|
|
updateMenu(added, type, overWidget);
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
mutationObserver.observe(document.body, { childList: true, subtree: false });
|
|
},
|
|
});
|