Multiple updates across the project: - .gitignore: ignore AGENTS.md - README: refreshed project README with badges, install instructions and node highlights - loraloader_mxd/server/utils_info.py: add structured local LoRA storage (more_info/<model>/lora-info.json) with README creation, legacy .rgthree-info.json fallback, and updated get/delete/save helpers to support LoRA-specific layout while preserving backward compatibility - maxedoutnodes.py: add SmartCropByMaskMXD node (square crop centered on mask), register it in NODE_CLASS_MAPPINGS and display name mappings - wan22nodes.py: add validation and resize helpers (_validate_image_batch_4d, _resize_to_explicit_resolution), FPS resampling (_resample_video_frames_to_fps), WAN22 match-resolution node (WAN22_I2V_Match_Resolution_MXD), expose fps_mode/target_fps in video prep nodes, keep legacy "Safe Auto" behavior while simplifying UI tier options - web/js/betterCombos.js: small UI/identifier changes (rename extension id/name to mxd, add alias for LoadLatent, disable legacy pysssss enhancements behind flag, tweak folder arrow glyphs) - web/mxd_utils_menu.js: replace LoRA chooser with a stylistic tree-style chooser (folder grouping, toggle behavior, styles injection, callback normalization) These changes introduce structured local LoRA metadata handling, new image/video utilities and nodes for WAN 2.2 workflows, and front-end improvements for LoRA choosing and loader UI.
184 lines
5.5 KiB
JavaScript
184 lines
5.5 KiB
JavaScript
import { app } from "../../scripts/app.js";
|
|
import { mxdApi } from "./mxd_api.js";
|
|
|
|
let chooserStylesInjected = false;
|
|
|
|
function splitPathSegments(value) {
|
|
return String(value)
|
|
.split(/[\\/]+/)
|
|
.map((s) => s.trim())
|
|
.filter(Boolean);
|
|
}
|
|
|
|
function ensureLoraChooserTreeStyles() {
|
|
if (chooserStylesInjected) return;
|
|
chooserStylesInjected = true;
|
|
|
|
const style = document.createElement("style");
|
|
style.textContent = `
|
|
.mxd-lora-chooser-folder { opacity: 0.8; }
|
|
.mxd-lora-chooser-folder-arrow { display: inline-block; width: 15px; }
|
|
.mxd-lora-chooser-folder:hover { background-color: rgba(255, 255, 255, 0.1); }
|
|
.mxd-lora-chooser-prefix { display: none; }
|
|
|
|
.litecontextmenu:has(input:not(:placeholder-shown)) .mxd-lora-chooser-folder-contents {
|
|
display: block !important;
|
|
}
|
|
.litecontextmenu:has(input:not(:placeholder-shown)) .mxd-lora-chooser-folder {
|
|
display: none;
|
|
}
|
|
.litecontextmenu:has(input:not(:placeholder-shown)) .mxd-lora-chooser-prefix {
|
|
display: inline;
|
|
}
|
|
.litecontextmenu:has(input:not(:placeholder-shown)) .litemenu-entry {
|
|
padding-left: 2px !important;
|
|
}
|
|
`;
|
|
document.body.appendChild(style);
|
|
}
|
|
|
|
function getContextMenuRootElement(menuInstance) {
|
|
const menus = [...document.querySelectorAll(".litecontextmenu")];
|
|
return (
|
|
menuInstance?.root ||
|
|
menuInstance?.rootElement ||
|
|
menuInstance?.element ||
|
|
menus[menus.length - 1] ||
|
|
null
|
|
);
|
|
}
|
|
|
|
function decorateLoraChooserAsTree(menuEl) {
|
|
if (!menuEl || menuEl.dataset.mxdLoraTreeApplied === "1") {
|
|
return;
|
|
}
|
|
menuEl.dataset.mxdLoraTreeApplied = "1";
|
|
|
|
const items = [...menuEl.querySelectorAll(".litemenu-entry")];
|
|
if (!items.length) {
|
|
return;
|
|
}
|
|
|
|
const folderMap = new Map();
|
|
const rootItems = [];
|
|
const itemsSymbol = Symbol("items");
|
|
|
|
for (const item of items) {
|
|
const dataValue = item.getAttribute("data-value") ?? item.textContent ?? "";
|
|
const path = splitPathSegments(dataValue);
|
|
if (!path.length) {
|
|
continue;
|
|
}
|
|
|
|
item.textContent = path[path.length - 1];
|
|
if (path.length > 1) {
|
|
const prefix = document.createElement("span");
|
|
prefix.className = "mxd-lora-chooser-prefix";
|
|
prefix.textContent = path.slice(0, -1).join("/") + "/";
|
|
item.prepend(prefix);
|
|
}
|
|
|
|
if (path.length === 1) {
|
|
rootItems.push(item);
|
|
continue;
|
|
}
|
|
|
|
item.remove();
|
|
|
|
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);
|
|
}
|
|
|
|
if (!currentLevel.has(itemsSymbol)) {
|
|
currentLevel.set(itemsSymbol, []);
|
|
}
|
|
currentLevel.get(itemsSymbol).push(item);
|
|
}
|
|
|
|
const createFolderElement = (name) => {
|
|
const folder = document.createElement("div");
|
|
folder.className = "litemenu-entry mxd-lora-chooser-folder";
|
|
folder.style.paddingLeft = "5px";
|
|
folder.innerHTML = `<span class="mxd-lora-chooser-folder-arrow">></span> ${name}`;
|
|
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 = document.createElement("div");
|
|
childContainer.className = "mxd-lora-chooser-folder-contents";
|
|
childContainer.style.display = "none";
|
|
|
|
const childItems = content.get(itemsSymbol) || [];
|
|
for (const item of childItems) {
|
|
item.style.paddingLeft = `${(level + 1) * 10 + 14}px`;
|
|
childContainer.appendChild(item);
|
|
}
|
|
|
|
insertFolderStructure(childContainer, content, level + 1);
|
|
parentElement.appendChild(childContainer);
|
|
|
|
folderElement.addEventListener("click", (e) => {
|
|
e.stopPropagation();
|
|
const arrow = folderElement.querySelector(".mxd-lora-chooser-folder-arrow");
|
|
const contents = folderElement.nextElementSibling;
|
|
if (!contents) return;
|
|
if (contents.style.display === "none") {
|
|
contents.style.display = "block";
|
|
if (arrow) arrow.textContent = "v";
|
|
} else {
|
|
contents.style.display = "none";
|
|
if (arrow) arrow.textContent = ">";
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
const parent = rootItems[0]?.parentElement || items[0]?.parentElement || menuEl;
|
|
if (!parent) return;
|
|
const folderFragment = document.createDocumentFragment();
|
|
insertFolderStructure(folderFragment, folderMap);
|
|
parent.insertBefore(folderFragment, rootItems[0] || null);
|
|
}
|
|
|
|
function normalizeMenuCallbackValue(value) {
|
|
if (typeof value === "object" && value && "content" in value) {
|
|
return value.content;
|
|
}
|
|
return value;
|
|
}
|
|
|
|
export async function showLoraChooser(event, callback, parentMenu, loras) {
|
|
const canvas = app.canvas;
|
|
if (!loras) {
|
|
loras = ["None", ...(await mxdApi.getLoras().then((items) => items.map((l) => l.file)))];
|
|
}
|
|
|
|
ensureLoraChooserTreeStyles();
|
|
|
|
const menu = new LiteGraph.ContextMenu(loras, {
|
|
event,
|
|
parentMenu: parentMenu != null ? parentMenu : undefined,
|
|
title: "Choose a LoRA",
|
|
scale: Math.max(1, canvas.ds?.scale ?? 1),
|
|
className: "dark",
|
|
callback: (value, ...rest) => callback?.(normalizeMenuCallbackValue(value), ...rest),
|
|
});
|
|
|
|
requestAnimationFrame(() => {
|
|
decorateLoraChooserAsTree(getContextMenuRootElement(menu));
|
|
});
|
|
}
|
|
|