277 lines
8.4 KiB
JavaScript
277 lines
8.4 KiB
JavaScript
import { app } from "/scripts/app.js";
|
|
import { applyAdaptiveCanvasOnly } from "../shared/nodes2.mjs";
|
|
import { listFolder, pickNativeFolder } from "./api.mjs";
|
|
import {
|
|
COMFY_CLASS,
|
|
HIDDEN_INPUT_NAME,
|
|
readState,
|
|
writeState,
|
|
} from "./state.mjs";
|
|
import {
|
|
buildRoot,
|
|
injectCSS,
|
|
openBrowsePopup,
|
|
openPickGallery,
|
|
} from "./ui.mjs";
|
|
|
|
const MIN_W = 300;
|
|
const DEFAULT_W = 370;
|
|
|
|
function hideJsonWidget(widgets, name) {
|
|
const widget = (widgets || []).find((entry) => entry?.name === name);
|
|
if (!widget) return;
|
|
widget.type = "hidden";
|
|
widget.computeSize = () => [0, 0];
|
|
}
|
|
|
|
function normalizePath(value) {
|
|
if (!value) return "";
|
|
let normalized = String(value).trim().replace(/\\/g, "/").replace(/\/+$/, "");
|
|
if (/^[A-Za-z]:$/.test(normalized)) normalized += "/";
|
|
return normalized;
|
|
}
|
|
|
|
function stripInputs(node) {
|
|
if (!node?.inputs?.length) return;
|
|
for (let index = node.inputs.length - 1; index >= 0; index -= 1) {
|
|
if (node.inputs[index]?.link != null) {
|
|
try {
|
|
node.disconnectInput(index);
|
|
} catch (_error) {
|
|
// Ignore failed disconnects and keep stripping.
|
|
}
|
|
}
|
|
node.removeInput(index);
|
|
}
|
|
}
|
|
|
|
function selectionSummary(state, total) {
|
|
const mode = state.selection_mode || "selected";
|
|
if (mode === "all") return `All ${total} / ${total}`;
|
|
if (mode === "first_n") return `First ${Math.min(Math.max(parseInt(state.first_n, 10) || 0, 0), total)} / ${total}`;
|
|
if (mode === "random") return `Random ${total ? 1 : 0} / ${total}`;
|
|
return `${(state.selected || []).length} / ${total}`;
|
|
}
|
|
|
|
function renderUI(node) {
|
|
const ui = node._dlfUI;
|
|
if (!ui) return;
|
|
const state = readState(node);
|
|
if (document.activeElement !== ui.folderInput) {
|
|
ui.folderInput.value = state.folder || "";
|
|
}
|
|
const total = (node._dlfFiles || []).length;
|
|
ui.pickBtn.textContent = `Pick images · ${selectionSummary(state, total)}`;
|
|
ui.pickBtn.classList.toggle("empty", total === 0);
|
|
ui.msgEl.textContent = node._dlfListError || "";
|
|
node.setDirtyCanvas?.(true, true);
|
|
}
|
|
|
|
async function refreshListing(node) {
|
|
const requestId = (node._dlfListReq = (node._dlfListReq || 0) + 1);
|
|
const state = readState(node);
|
|
if (!state.folder) {
|
|
node._dlfFiles = [];
|
|
node._dlfListError = "";
|
|
renderUI(node);
|
|
return;
|
|
}
|
|
|
|
const response = await listFolder(state.folder, state.recursive);
|
|
if (node._dlfListReq !== requestId || !node._dlfUI) return;
|
|
if (response?.ok) {
|
|
node._dlfFiles = response.files || [];
|
|
node._dlfListError = node._dlfFiles.length ? "" : "No images found in this folder.";
|
|
} else {
|
|
node._dlfFiles = [];
|
|
node._dlfListError = response?.message || "Folder not found.";
|
|
}
|
|
|
|
const present = new Set((node._dlfFiles || []).map((file) => file.file));
|
|
const nextState = readState(node);
|
|
const kept = (nextState.selected || []).filter((file) => present.has(file));
|
|
if (kept.length !== (nextState.selected || []).length) {
|
|
nextState.selected = kept;
|
|
writeState(node, nextState);
|
|
}
|
|
renderUI(node);
|
|
}
|
|
|
|
async function setFolder(node, folder) {
|
|
const normalized = normalizePath(folder);
|
|
const state = readState(node);
|
|
const changed = (state.folder || "") !== normalized;
|
|
state.folder = normalized;
|
|
if (changed) state.selected = [];
|
|
writeState(node, state);
|
|
await refreshListing(node);
|
|
}
|
|
|
|
function setupNode(node) {
|
|
injectCSS();
|
|
hideJsonWidget(node.widgets, HIDDEN_INPUT_NAME);
|
|
stripInputs(node);
|
|
|
|
const ui = buildRoot();
|
|
node._dlfUI = ui;
|
|
const widget = node.addDOMWidget("dumas_load_images_folder", "custom", ui.root, {
|
|
getValue: () => null,
|
|
setValue: () => {},
|
|
serialize: false,
|
|
getMinHeight: () => 96,
|
|
});
|
|
widget.computeLayoutSize = () => ({ minHeight: 96, minWidth: 1 });
|
|
applyAdaptiveCanvasOnly(widget);
|
|
|
|
ui.folderInput.addEventListener("keydown", (event) => {
|
|
event.stopImmediatePropagation();
|
|
if (event.key === "Enter") {
|
|
event.preventDefault();
|
|
ui.folderInput.blur();
|
|
}
|
|
});
|
|
ui.folderInput.addEventListener("change", () => setFolder(node, ui.folderInput.value.trim()));
|
|
ui.folderInput.addEventListener("paste", () => {
|
|
setTimeout(() => {
|
|
const value = ui.folderInput.value.trim();
|
|
if (normalizePath(value) !== (readState(node).folder || "")) setFolder(node, value);
|
|
}, 0);
|
|
});
|
|
ui.browseBtn.addEventListener("click", async () => {
|
|
const start = readState(node).folder || "";
|
|
const previous = ui.browseLbl.textContent;
|
|
ui.browseBtn.disabled = true;
|
|
ui.browseLbl.textContent = "Opening…";
|
|
let response;
|
|
try {
|
|
response = await pickNativeFolder(start);
|
|
} catch (_error) {
|
|
response = { ok: false };
|
|
}
|
|
ui.browseBtn.disabled = false;
|
|
ui.browseLbl.textContent = previous || "Browse";
|
|
if (response?.ok && response.path) {
|
|
await setFolder(node, response.path);
|
|
return;
|
|
}
|
|
if (response?.cancelled) return;
|
|
openBrowsePopup(node, ui.browseBtn, {
|
|
startPath: start,
|
|
onPick: (folder) => setFolder(node, folder),
|
|
});
|
|
});
|
|
ui.pickBtn.addEventListener("click", async () => {
|
|
const typed = ui.folderInput.value.trim();
|
|
if (typed !== (readState(node).folder || "")) await setFolder(node, typed);
|
|
const state = readState(node);
|
|
if (!state.folder) {
|
|
ui.folderInput.focus();
|
|
node._dlfListError = "Set a folder first.";
|
|
renderUI(node);
|
|
return;
|
|
}
|
|
ui.pickBtn.disabled = true;
|
|
try {
|
|
await refreshListing(node);
|
|
} finally {
|
|
ui.pickBtn.disabled = false;
|
|
}
|
|
openPickGallery(node, ui.pickBtn, {
|
|
onChange: renderUI,
|
|
refreshListing,
|
|
});
|
|
});
|
|
|
|
node.size[0] = Math.max(node.size[0] || 0, DEFAULT_W);
|
|
node.size[1] = Math.max(node.size[1] || 0, 110);
|
|
queueMicrotask(() => refreshListing(node));
|
|
}
|
|
|
|
function collectNodes(graph, out) {
|
|
if (!graph) return;
|
|
const nodes = graph._nodes || graph.nodes || [];
|
|
for (const node of nodes) {
|
|
if (node?.comfyClass === COMFY_CLASS) out.push(node);
|
|
const inner = node?.subgraph || node?.graph || node?._graph;
|
|
if (inner && inner !== graph) collectNodes(inner, out);
|
|
}
|
|
}
|
|
|
|
function matchNode(nodes, promptId) {
|
|
let node = nodes.find((entry) => String(entry.id) === String(promptId));
|
|
if (node) return node;
|
|
const tail = String(promptId).split(":").pop();
|
|
node = nodes.find((entry) => String(entry.id) === tail);
|
|
return node || null;
|
|
}
|
|
|
|
function injectState(result) {
|
|
const output = result?.output;
|
|
if (!output) return;
|
|
const nodes = [];
|
|
collectNodes(app.graph, nodes);
|
|
for (const id in output) {
|
|
const entry = output[id];
|
|
if (!entry || entry.class_type !== COMFY_CLASS) continue;
|
|
const node = matchNode(nodes, id);
|
|
if (!node) continue;
|
|
if (!entry.inputs) entry.inputs = {};
|
|
entry.inputs[HIDDEN_INPUT_NAME] = JSON.stringify(readState(node));
|
|
}
|
|
}
|
|
|
|
function installGraphToPromptHook() {
|
|
if (app._dlfGraphPatched) return;
|
|
app._dlfGraphPatched = true;
|
|
const original = app.graphToPrompt.bind(app);
|
|
app.graphToPrompt = async function graphToPromptPatched(...args) {
|
|
const result = await original(...args);
|
|
injectState(result);
|
|
return result;
|
|
};
|
|
}
|
|
|
|
app.registerExtension({
|
|
name: "Dumas.LoadImagesFolder",
|
|
setup() {
|
|
installGraphToPromptHook();
|
|
},
|
|
async beforeRegisterNodeDef(nodeType, nodeData) {
|
|
if (nodeData?.name !== COMFY_CLASS) return;
|
|
|
|
const originalNodeCreated = nodeType.prototype.onNodeCreated;
|
|
nodeType.prototype.onNodeCreated = function onNodeCreated() {
|
|
const result = originalNodeCreated?.apply(this, arguments);
|
|
setupNode(this);
|
|
return result;
|
|
};
|
|
|
|
const originalConfigure = nodeType.prototype.onConfigure;
|
|
nodeType.prototype.onConfigure = function onConfigure() {
|
|
const result = originalConfigure?.apply(this, arguments);
|
|
stripInputs(this);
|
|
queueMicrotask(() => refreshListing(this));
|
|
return result;
|
|
};
|
|
|
|
const originalDrawForeground = nodeType.prototype.onDrawForeground;
|
|
nodeType.prototype.onDrawForeground = function onDrawForeground(ctx) {
|
|
const result = originalDrawForeground?.call(this, ctx);
|
|
this.size[0] = Math.max(this.size[0], MIN_W);
|
|
return result;
|
|
};
|
|
|
|
nodeType.prototype.onConnectInput = function onConnectInput() {
|
|
return false;
|
|
};
|
|
|
|
const originalRemoved = nodeType.prototype.onRemoved;
|
|
nodeType.prototype.onRemoved = function onRemoved() {
|
|
this._dlfGallery?._dlfClose?.();
|
|
this._dlfBrowsePopup?._dlfClose?.();
|
|
document.querySelectorAll(".dlf-menu").forEach((menu) => menu._dlfClose?.());
|
|
return originalRemoved?.apply(this, arguments);
|
|
};
|
|
},
|
|
});
|