Files

243 lines
8.7 KiB
JavaScript

import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
const waitingNodes = new Map();
const processedNodes = new Set();
function updateNodePreview(nodeId, fileInfo) {
const node = app.graph.getNodeById(parseInt(nodeId));
if (!node || !fileInfo) return;
const imageUrl = fileInfo.subfolder
? `/view?filename=${encodeURIComponent(fileInfo.filename)}&subfolder=${encodeURIComponent(fileInfo.subfolder)}&type=${fileInfo.type || 'input'}`
: `/view?filename=${encodeURIComponent(fileInfo.filename)}&type=${fileInfo.type || 'input'}`;
node.images = [{
filename: fileInfo.filename,
subfolder: fileInfo.subfolder || "",
type: fileInfo.type || "input",
url: imageUrl
}];
if (node.onDrawBackground) {
const img = new Image();
img.onload = () => {
node.imgs = [img];
app.graph.setDirtyCanvas(true);
};
img.src = imageUrl;
}
updateStatusWidget(node, `${nodeId}_${fileInfo.filename}`);
}
function updateStatusWidget(node, statusText) {
const statusWidget = node.widgets?.find(w => w.name === "file_info");
if (statusWidget) {
statusWidget.value = statusText;
app.graph.setDirtyCanvas(true);
app.graph.change();
}
}
api.addEventListener("bridge_preview_update", (event) => {
const { node_id, urls } = event.detail;
const node = app.graph._nodes_by_id[node_id];
if (!node || !urls?.length) return;
waitingNodes.set(node_id, { urls, timestamp: Date.now(), node });
const imageData = urls.map((url, index) => ({
index,
filename: url.filename,
subfolder: url.subfolder,
type: url.type
}));
node.imageData = imageData;
node.imgs = [];
imageData.forEach((imgData, i) => {
const img = new Image();
img.onload = () => {
app.graph.setDirtyCanvas(true);
if (i === imageData.length - 1) {
setupClipspace(node_id, urls);
}
};
img.src = `/view?filename=${encodeURIComponent(imgData.filename)}&type=${imgData.type}&subfolder=${imgData.subfolder || ''}&${app.getPreviewFormatParam()}`;
node.imgs.push(img);
});
node.setSizeForImage?.();
node.update?.();
});
function setupClipspace(nodeId, urls) {
const ComfyApp = app.constructor;
if (!ComfyApp.clipspace) ComfyApp.clipspace = {};
if (!app.clipspace) app.clipspace = {};
const images = urls.map(url => ({
filename: url.filename,
subfolder: url.subfolder || "",
type: url.type || "output"
}));
const imgs = urls.map(url => ({
src: `${window.location.origin}/view?filename=${encodeURIComponent(url.filename)}&type=${url.type}&subfolder=${encodeURIComponent(url.subfolder || '')}`,
filename: url.filename,
subfolder: url.subfolder || "",
type: url.type || "output"
}));
[ComfyApp.clipspace, app.clipspace].forEach(clipspace => {
clipspace.images = images;
clipspace.imgs = imgs;
clipspace.selectedIndex = 0;
});
setTimeout(() => {
const node = app.graph.getNodeById(parseInt(nodeId));
if (!node) return;
app.canvas.selectNode(node);
let success = false;
try {
if (app.extensionManager?.command?.execute) {
app.extensionManager.command.execute('Comfy.MaskEditor.OpenMaskEditor');
success = true;
}
} catch (error) {
// Silently fail and try next method
}
if (!success) {
try {
const ComfyApp = app.constructor;
const openMaskEditor = ComfyApp.open_maskeditor || app.open_maskeditor;
if (openMaskEditor && typeof openMaskEditor === 'function') {
openMaskEditor();
success = true;
}
} catch (error) {
// Silently fail
}
}
bindCancelButton();
}, 100);
}
function bindCancelButton() {
const checkInterval = setInterval(() => {
const maskEditor = findMaskEditor();
if (!maskEditor) return;
const cancelButtons = Array.from(maskEditor.querySelectorAll('button')).filter(btn => {
const text = btn.textContent.trim().toLowerCase();
return text === 'cancel' || text === '取消' || text.includes('cancel') || text.includes('取消');
});
if (cancelButtons.length > 0) {
cancelButtons.forEach(button => {
if (!button.hasAttribute('data-bridge-bound')) {
button.setAttribute('data-bridge-bound', 'true');
button.addEventListener('click', () => {
setTimeout(handleMaskEditorCancel, 50);
}, { capture: true });
}
});
clearInterval(checkInterval);
}
}, 300);
setTimeout(() => clearInterval(checkInterval), 10000);
}
function findMaskEditor() {
const newMaskEditor = document.querySelector('.mask-editor-dialog');
if (newMaskEditor) {
return newMaskEditor;
}
const modals = document.querySelectorAll('div.comfy-modal, .comfy-modal, [class*="modal"]');
for (const modal of modals) {
if (modal.querySelector('canvas') && modal.style.display !== 'none') {
return modal;
}
}
const elements = document.querySelectorAll('*');
for (const element of elements) {
const buttons = element.querySelectorAll('button');
if (buttons.length >= 2 && element.querySelector('canvas')) {
const buttonTexts = Array.from(buttons).map(btn => btn.textContent.trim().toLowerCase());
if (buttonTexts.some(text => text.includes('cancel') || text.includes('取消')) &&
buttonTexts.some(text => text.includes('save') || text.includes('保存'))) {
return element;
}
}
}
return null;
}
function handleMaskEditorCancel() {
waitingNodes.forEach((nodeInfo, nodeId) => {
sendCancelSignal(nodeId);
});
waitingNodes.clear();
processedNodes.clear();
}
async function sendCancelSignal(nodeId) {
try {
await api.fetchApi("/bridge_preview/cancel", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ node_id: String(nodeId) })
});
} catch (error) {
console.error(`[BridgePreview] Failed to send cancel signal:`, error);
}
}
const originalFetch = api.fetchApi;
api.fetchApi = async function(url, options) {
const result = await originalFetch.call(this, url, options);
if (url === "/upload/mask" && result.ok) {
await handleMaskUpload(result);
}
return result;
};
async function handleMaskUpload(result) {
try {
const responseData = await result.clone().json();
const fileInfo = responseData?.name ? {
filename: responseData.name,
subfolder: responseData.subfolder || "clipspace",
type: responseData.type || "input"
} : null;
if (!fileInfo) return;
let latestNodeId = null;
let latestTimestamp = 0;
for (const [nodeId, nodeInfo] of waitingNodes) {
if (nodeInfo.timestamp > latestTimestamp) {
latestTimestamp = nodeInfo.timestamp;
latestNodeId = nodeId;
}
}
if (!latestNodeId) return;
try {
updateNodePreview(latestNodeId, fileInfo);
const confirmResponse = await originalFetch.call(api, "/bridge_preview/confirm", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
node_id: String(latestNodeId),
file_info: fileInfo
})
});
if (confirmResponse.ok) {
processedNodes.add(String(latestNodeId));
setTimeout(() => {
waitingNodes.delete(latestNodeId);
}, 1000);
}
} catch (error) {
console.error(`[BridgePreview] Failed to process node ${latestNodeId}:`, error);
}
} catch (error) {
console.error(`[BridgePreview] Failed to handle mask result:`, error);
}
}
setInterval(() => {
const now = Date.now();
for (const [nodeId, nodeInfo] of waitingNodes) {
if (now - nodeInfo.timestamp > 30000) {
waitingNodes.delete(nodeId);
processedNodes.delete(String(nodeId));
}
}
}, 5000);
console.log("[BridgePreview] Bridge preview module loaded");