Files
Maxed-Out-99-ComfyUI-MaxedOut/web/vendor/zip_loader/zip_loader.js
T
Maxed-Out-99andClaude Fable 5 4cb19be2e0 Restructure web/ into lib+nodes+vendor; merge power lora loader forks
- web/lib/: the shared rgthree-derived layer (mxd_* utils, dialogs, api,
  runtime) + its CSS, moved as a unit.
- web/nodes/: one extension file per node/feature, _mxd suffix dropped.
- web/vendor/zip_loader/: vendored zip import addon.
- index.js explicitly imports every module; loraloader_mxd_entry.js
  folded into it and deleted.

Power Lora Loader merge: new web/nodes/power_lora_base.js holds the
shared node machinery, header row, and single-strength lora row widget.
power_lora_loader.js (dual model/clip strengths) and
ltx2_power_lora_loader.js (per-layer strength rows) are now thin
subclasses (~450 duplicated lines removed). Serialization formats,
widget names, extension names, and node targets unchanged.

better_combos.js scoped to MXD latent loaders only: removed the
pysssss CheckpointLoader/LoraLoader patching (already disabled behind
ENABLE_PYSSSS_NODE_ENHANCEMENTS=false, so provably inert), the dead
' MXD' alias config entries, and the no-op refreshComboInNodes wrapper.

scripts/check_web.py clean; node schema unchanged (68 identical).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 06:41:35 -07:00

224 lines
9.8 KiB
JavaScript

import { app } from "../../../../scripts/app.js";
function get_ext(filename) {
const ext = filename.split(".").pop();
if (!ext) {
return "";
}
return ext.toLowerCase();
}
function get_common_top_folder(paths) {
let common = null;
for (const p of paths) {
const parts = p.split("/").filter(Boolean);
if (parts.length === 0) {
return "";
}
const first = parts[0];
if (common === null) {
common = first;
} else if (common !== first) {
return "";
}
}
return common || "";
}
app.registerExtension({
name: "Comfy.ZipLoader",
init() {
// Use capture phase to intercept the event before ComfyUI's default handler (which likely listens on bubbling phase on document/body)
document.addEventListener("drop", async (event) => {
if (!event.dataTransfer || !event.dataTransfer.files || event.dataTransfer.files.length === 0) {
return;
}
const files = Array.from(event.dataTransfer.files);
let zipFiles = files.filter(f => get_ext(f.name) === "zip");
let nonZipFiles = files.filter(f => get_ext(f.name) !== "zip");
// If all dropped files are zips, we handle it and stop others
if (zipFiles.length > 0 && nonZipFiles.length === 0) {
event.preventDefault();
event.stopPropagation();
// Load JSZip if not already loaded
if (!window.JSZip) {
try {
await import("./jszip.min.js");
} catch (e) {
console.error("Failed to load JSZip:", e);
alert("Failed to load JSZip library. Please check console.");
return;
}
}
let JSZip = window.JSZip;
if (!JSZip) {
const module = await import("./jszip.min.js");
JSZip = module.default || module;
}
if (!JSZip) {
console.error("JSZip not found after import");
return;
}
let totalCount = 0;
let workflowCount = 0;
const workflowPaths = [];
const workflowBlobs = [];
for (const file of zipFiles) {
console.log("Processing zip file:", file.name);
try {
const zip = await JSZip.loadAsync(file);
let count = 0;
const promises = [];
zip.forEach((relativePath, zipEntry) => {
if (zipEntry.dir) return;
if (relativePath.startsWith("__MACOSX")) return;
if (relativePath.includes("/.")) return;
const ext = get_ext(relativePath);
const promise = zipEntry.async("blob").then(async (blob) => {
if (ext === "json") {
workflowCount++;
workflowPaths.push(relativePath);
workflowBlobs.push(blob);
return;
}
const targetPath = "workflows/" + relativePath;
const url = `/api/userdata/${encodeURIComponent(targetPath)}?overwrite=true`;
const res = await fetch(url, {
method: "POST",
body: blob
});
if (res.ok) {
count++;
totalCount++;
} else {
console.error("Failed to upload:", relativePath, res.statusText);
}
});
promises.push(promise);
});
await Promise.all(promises);
} catch (err) {
console.error("Error processing zip:", err);
alert("Error processing zip file: " + err.message);
}
}
let loadedSingleInMemory = false;
if (workflowCount === 1 && typeof app.handleFile === "function") {
// Mirror vanilla ComfyUI's own json-drop behavior: load straight into the
// graph via app.handleFile instead of writing to disk and forcing a reload.
try {
const relativePath = workflowPaths[0];
const blob = workflowBlobs[0];
const filename = relativePath.split("/").pop();
const jsonFile = new File([blob], filename, { type: "application/json" });
await app.handleFile(jsonFile);
loadedSingleInMemory = true;
} catch (err) {
// app.handleFile is an internal API; fall back to the disk-upload
// path below if a future ComfyUI build changes/removes it.
console.error("app.handleFile failed, falling back to workflow upload:", err);
}
}
if (!loadedSingleInMemory && workflowCount > 0) {
await Promise.all(workflowPaths.map(async (relativePath, i) => {
const targetPath = "workflows/" + relativePath;
const url = `/api/userdata/${encodeURIComponent(targetPath)}?overwrite=true`;
const res = await fetch(url, {
method: "POST",
body: workflowBlobs[i]
});
if (res.ok) {
totalCount++;
} else {
console.error("Failed to upload:", relativePath, res.statusText);
}
}));
const sortedWorkflows = workflowPaths.slice().sort((a, b) => a.localeCompare(b));
const commonFolder = get_common_top_folder(sortedWorkflows) || "(Root)";
const modal = document.createElement("div");
Object.assign(modal.style, {
position: "fixed",
top: "0",
left: "0",
width: "100%",
height: "100%",
backgroundColor: "rgba(0,0,0,0.8)",
display: "flex",
justifyContent: "center",
alignItems: "center",
zIndex: "10000",
fontFamily: "sans-serif"
});
const dialog = document.createElement("div");
Object.assign(dialog.style, {
backgroundColor: "#222",
color: "white",
padding: "25px",
borderRadius: "10px",
width: "450px",
border: "1px solid #444",
boxShadow: "0 0 20px rgba(0,0,0,0.5)"
});
dialog.innerHTML = `
<h2 style="margin-top:0; color:#44cf7e;">Workflows Imported</h2>
<p><strong>Folder:</strong> <span style="color:#00bdff;">${commonFolder}</span></p>
<div style="background:#111; padding:10px; border-radius:5px; max-height:200px; overflow-y:auto; margin:15px 0;">
<ul id="workflowList" style="margin:0; padding-left:20px; font-size:14px; line-height:1.6;"></ul>
</div>
<p style="font-size:13px; color:#aaa;">
These will appear in the <strong>Workflows</strong> side panel (Press <strong>W</strong>) after a reload.
</p>
<div style="display:flex; gap:10px; margin-top:20px;">
<button id="reloadBtn" style="flex:1; padding:10px; background:#44cf7e; border:none; color:black; font-weight:bold; border-radius:4px; cursor:pointer;">Reload Now</button>
<button id="closeBtn" style="flex:1; padding:10px; background:#444; border:none; color:white; border-radius:4px; cursor:pointer;">Later</button>
</div>
`;
const workflowList = dialog.querySelector("#workflowList");
for (const p of sortedWorkflows) {
const li = document.createElement("li");
li.textContent = p.split("/").pop();
workflowList.appendChild(li);
}
modal.appendChild(dialog);
document.body.appendChild(modal);
dialog.querySelector("#reloadBtn").onclick = () => window.location.reload();
dialog.querySelector("#closeBtn").onclick = () => document.body.removeChild(modal);
}
}
}, true); // Capture = true
// We also need to prevent default dragover to allow drop
document.addEventListener("dragover", (event) => {
if (event.dataTransfer && event.dataTransfer.types && event.dataTransfer.types.includes("Files")) {
// event.preventDefault(); // This is needed to allow drop
}
}, true);
}
});