diff --git a/README.md b/README.md
index a843777..778ae92 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@
# 🏵️ Fill-Nodes
-#Fix image and workflow drag-and-drop on newer ComfyUI frontend#
+
If you enjoy this project, consider supporting me on Patreon!
@@ -572,17 +572,5 @@ Fill-Nodes is a versatile collection of custom nodes for ComfyUI that extends fu
| `FL_KartelJobOutput` | POSTs an image batch back to a Kartel callback URL as multipart PNGs with a JSON metadata field (job_id, user_id, user_email, app_name). Skips the POST silently when no callback URL is provided or the input is a 1×1 placeholder. |
-### Drag and Drop Support
-Fill Nodes includes a compatibility fix for drag-and-drop behavior on newer ComfyUI frontend versions.
-
-Supported drop actions:
-
-- Drop an image directly onto a `LoadImage` or `LoadImageMask` node
-- Drop an image onto empty canvas space to create a new `LoadImage` node
-- Drop JSON workflow files onto empty canvas space
-- Drop PNG/WebP/AVIF images with embedded workflow metadata
-- Drop MP4/MOV/M4V files with embedded workflow metadata
-
-The workflow loader also cleans up some stale preview references, such as temporary rgthree comparison images and duplicated media subfolder paths, before loading the graph.
---
diff --git a/__init__.py b/__init__.py
index 25902e9..24414d4 100644
--- a/__init__.py
+++ b/__init__.py
@@ -1,188 +1,7 @@
import logging
-import json
-import struct
-from io import BytesIO
-
-from aiohttp import web
-from PIL import Image
-import server
logger = logging.getLogger("fl_fill_nodes")
-
-def _fl_is_api_json(data):
- return isinstance(data, dict) and data and all(
- isinstance(v, dict) and "class_type" in v for v in data.values()
- )
-
-
-def _fl_decode_jsonish(value):
- if isinstance(value, (dict, list)):
- return value
- if isinstance(value, bytes):
- value = value.decode("utf-8", errors="ignore")
- if isinstance(value, str):
- return json.loads(value)
- return None
-
-
-def _fl_metadata_from_json(data):
- parsed = _fl_decode_jsonish(data)
- if not isinstance(parsed, dict):
- return {}
- if any(k in parsed for k in ("workflow", "Workflow", "prompt", "Prompt", "parameters", "templates")):
- return {
- "workflow": parsed.get("workflow", parsed.get("Workflow")),
- "prompt": parsed.get("prompt", parsed.get("Prompt")),
- "parameters": parsed.get("parameters"),
- "templates": parsed.get("templates"),
- }
- if parsed.get("templates"):
- return {"templates": parsed["templates"]}
- if _fl_is_api_json(parsed):
- return {"prompt": parsed}
- return {"workflow": parsed}
-
-
-def _fl_metadata_from_image(data):
- try:
- image = Image.open(BytesIO(data))
- info = dict(image.info or {})
- except Exception:
- return {}
-
- result = {}
- for source_key, target_key in (
- ("workflow", "workflow"),
- ("Workflow", "workflow"),
- ("prompt", "prompt"),
- ("Prompt", "prompt"),
- ("parameters", "parameters"),
- ("parametersText", "parameters"),
- ("templates", "templates"),
- ):
- if source_key not in info:
- continue
- value = info[source_key]
- if isinstance(value, bytes):
- value = value.decode("utf-8", errors="ignore")
- result[target_key] = value
- return result
-
-
-def _fl_read_u32(data, pos):
- if pos + 4 > len(data):
- return 0
- return struct.unpack(">I", data[pos:pos + 4])[0]
-
-
-def _fl_find_box(data, start, end, box_type):
- pos = start
- marker = box_type.encode("ascii")
- while pos < end - 8:
- size = _fl_read_u32(data, pos)
- if size < 8:
- pos += 1
- continue
- if data[pos + 4:pos + 8] == marker:
- return pos + 8, min(pos + size, len(data))
- if pos + size > end:
- return None
- pos += size
- return None
-
-
-def _fl_extract_json_from_range(data, start, end):
- json_start = data.find(b"{", start, end)
- if json_start < 0:
- return None
- try:
- return json.loads(data[json_start:end].decode("utf-8", errors="ignore"))
- except Exception:
- return None
-
-
-def _fl_parse_keys_box(data, start, end):
- keys = {}
- pos = start + 4
- if pos + 4 > end:
- return keys
- count = _fl_read_u32(data, pos)
- pos += 4
- for index in range(1, count + 1):
- if pos + 8 > end:
- break
- key_size = _fl_read_u32(data, pos)
- key_name_start = pos + 8
- key_name_end = pos + key_size
- if key_size < 8 or key_name_end > end:
- break
- keys[index] = data[key_name_start:key_name_end].decode("utf-8", errors="ignore")
- pos = key_name_end
- return keys
-
-
-def _fl_metadata_from_isobmff(data):
- data = data[:64 * 1024 * 1024]
- udta = _fl_find_box(data, 0, len(data), "udta")
- if not udta:
- moov = _fl_find_box(data, 0, len(data), "moov")
- if moov:
- udta = _fl_find_box(data, moov[0], moov[1], "udta")
- if not udta:
- return {}
-
- meta = _fl_find_box(data, udta[0], udta[1], "meta")
- if not meta:
- return {}
- meta_start = meta[0] + 4
- keys_box = _fl_find_box(data, meta_start, meta[1], "keys")
- ilst_box = _fl_find_box(data, meta_start, meta[1], "ilst")
- if not keys_box or not ilst_box:
- return {}
-
- keys = _fl_parse_keys_box(data, keys_box[0], keys_box[1])
- result = {}
- pos = ilst_box[0]
- while pos < ilst_box[1] - 8:
- item_size = _fl_read_u32(data, pos)
- if item_size <= 8 or pos + item_size > ilst_box[1]:
- break
- index = _fl_read_u32(data, pos + 4)
- key = (keys.get(index) or "").lower()
- data_box = _fl_find_box(data, pos + 8, pos + item_size, "data")
- if data_box and key in ("workflow", "prompt"):
- value = _fl_extract_json_from_range(data, data_box[0] + 8, data_box[1])
- if value is not None:
- result[key] = value
- pos += item_size
- return result
-
-
-@server.PromptServer.instance.routes.post("/load_image_drop_fix/workflow_metadata")
-async def _fl_load_image_drop_fix_workflow_metadata(request):
- post = await request.post()
- upload = post.get("file") or post.get("image")
- if upload is None or not hasattr(upload, "file"):
- return web.json_response({}, status=400)
-
- filename = (getattr(upload, "filename", "") or "").lower()
- data = upload.file.read()
- try:
- if filename.endswith(".json"):
- metadata = _fl_metadata_from_json(data)
- elif filename.endswith((".png", ".webp", ".avif")):
- metadata = _fl_metadata_from_image(data)
- elif filename.endswith((".mp4", ".mov", ".m4v")):
- metadata = _fl_metadata_from_isobmff(data)
- else:
- metadata = {}
- except Exception as exc:
- logger.warning("Load image drop metadata parse failed: %s", exc)
- metadata = {}
-
- return web.json_response(metadata)
-
# AI NODES
from .nodes.ai.FL_Fal_Gemini_ImageEdit import FL_Fal_Gemini_ImageEdit
from .nodes.ai.FL_Fal_GPTImage2_Edit import FL_Fal_GPTImage2_Edit
diff --git a/web/load_image_drop_fix.js b/web/load_image_drop_fix.js
deleted file mode 100644
index e33c557..0000000
--- a/web/load_image_drop_fix.js
+++ /dev/null
@@ -1,679 +0,0 @@
-import { app } from "/scripts/app.js";
-import {
- getAvifMetadata,
- getPngMetadata,
- getWebpMetadata,
-} from "/scripts/pnginfo.js";
-
-app.registerExtension({
- name: "Local.LoadImageDropFix.FillNodesPath",
- setup() {
- console.warn("[LoadImageDropFix] extension loaded v2026-07-20-workflow-sanitize");
- let lastDragOverLogKey = null;
- let patchedDragOverNode = null;
- let lastPatchedDrag = null;
-
- const findNodeById = (graph, id) => {
- if (!graph || id == null) return null;
-
- const node = graph.getNodeById?.(id);
- if (node) return node;
-
- for (const subgraph of graph.subgraphs?.values?.() ?? []) {
- const subgraphNode = findNodeById(subgraph, id);
- if (subgraphNode) return subgraphNode;
- }
-
- return null;
- };
-
- const getNodeFromDomEvent = (event) => {
- const element = event.target?.closest?.("[data-node-id]");
- if (!element) return null;
-
- return findNodeById(app.rootGraph ?? app.canvas?.graph, element.dataset.nodeId);
- };
-
- const getNodeUnderEvent = (event) => {
- const domNode = getNodeFromDomEvent(event);
- if (domNode) return domNode;
-
- const canvas = app.canvas;
- const graph = canvas?.graph;
- if (!canvas || !graph) return null;
-
- canvas.adjustMouseEvent?.(event);
- return graph.getNodeOnPos?.(event.canvasX, event.canvasY) ?? null;
- };
-
- const isLoadImageNode = (node) => {
- return node?.type === "LoadImage" || node?.type === "LoadImageMask";
- };
-
- const isImageFile = (file) => file?.type?.startsWith?.("image/");
-
- const isWorkflowFile = (file) => {
- return (
- file?.type === "application/json" ||
- file?.name?.toLowerCase?.().endsWith(".json")
- );
- };
-
- const isWorkflowMetadataCandidate = (file) => {
- const name = file?.name?.toLowerCase?.() ?? "";
- return (
- isWorkflowFile(file) ||
- isImageFile(file) ||
- file?.type === "video/mp4" ||
- file?.type === "video/quicktime" ||
- name.endsWith(".mp4") ||
- name.endsWith(".mov") ||
- name.endsWith(".m4v")
- );
- };
-
- const getFiles = (event) => {
- return Array.from(event.dataTransfer?.files ?? []);
- };
-
- const getItems = (event) => {
- return Array.from(event.dataTransfer?.items ?? []);
- };
-
- const isDraggingFiles = (event) => {
- return (
- getItems(event).some((item) => item.kind === "file") ||
- Array.from(event.dataTransfer?.types ?? []).includes("Files")
- );
- };
-
- const isDraggingImages = (event) => {
- const items = getItems(event).filter((item) => item.kind === "file");
- if (!items.length) return isDraggingFiles(event);
- return items.some(
- (item) => !item.type || item.type.startsWith("image/"),
- );
- };
-
- const hasWorkflowMetadata = async (file) => {
- try {
- const workflowData = await getWorkflowData(file);
- return !!(
- workflowData?.workflow ||
- workflowData?.Workflow ||
- workflowData?.prompt ||
- workflowData?.Prompt ||
- workflowData?.parameters ||
- workflowData?.templates
- );
- } catch (error) {
- console.warn("[LoadImageDropFix] workflow metadata check failed", error);
- return false;
- }
- };
-
- const getImageMetadata = async (file) => {
- let metadata = {};
- try {
- if (file.type === "image/png") {
- metadata = await getPngMetadata(file);
- } else if (file.type === "image/webp") {
- metadata = await getWebpMetadata(file);
- } else if (file.type === "image/avif") {
- metadata = await getAvifMetadata(file);
- }
- } catch (error) {
- console.warn("[LoadImageDropFix] metadata read failed", error);
- }
-
- return metadata;
- };
-
- const readTextFile = (file) => {
- return new Promise((resolve, reject) => {
- const reader = new FileReader();
- reader.onload = () => resolve(String(reader.result ?? ""));
- reader.onerror = () => reject(reader.error ?? new Error("read failed"));
- reader.onabort = () => reject(new Error("read aborted"));
- reader.readAsText(file);
- });
- };
-
- const getWorkflowDataFromServer = async (file) => {
- if (!isWorkflowMetadataCandidate(file)) return {};
-
- const body = new FormData();
- body.append("file", file);
-
- return await new Promise((resolve) => {
- const request = new XMLHttpRequest();
- request.open("POST", "/load_image_drop_fix/workflow_metadata");
- request.onload = () => {
- if (request.status < 200 || request.status >= 300) {
- resolve({});
- return;
- }
-
- try {
- resolve(JSON.parse(request.responseText || "{}"));
- } catch {
- resolve({});
- }
- };
- request.onerror = () => resolve({});
- request.onabort = () => resolve({});
- request.send(body);
- });
- };
-
- const parseJson = (value) => {
- if (!value) return undefined;
- if (typeof value === "object") return value;
- if (typeof value !== "string") return undefined;
- return JSON.parse(value);
- };
-
- const cloneJsonData = (value) => {
- if (!value || typeof value !== "object") return value;
- try {
- return structuredClone(value);
- } catch {
- return JSON.parse(JSON.stringify(value));
- }
- };
-
- const stripPreviewUrl = (value) => {
- if (typeof value !== "string") return value;
- return value.includes("rgthree.compare._temp_") ? "" : value;
- };
-
- const normalizeMediaReference = (value) => {
- if (!value || typeof value !== "object" || Array.isArray(value)) return 0;
-
- let changed = 0;
- const filename = value.filename;
- const subfolder = value.subfolder;
- if (typeof filename === "string" && typeof subfolder === "string" && subfolder) {
- const normalizedSubfolder = subfolder.replace(/^\/+|\/+$/g, "");
- const prefix = `${normalizedSubfolder}/`;
- if (filename.startsWith(prefix)) {
- value.filename = filename.slice(prefix.length);
- changed++;
- }
- }
-
- return changed;
- };
-
- const sanitizeWorkflowForDrop = (workflow) => {
- const sanitized = cloneJsonData(workflow);
- const stats = { normalizedMediaRefs: 0, clearedTempPreviews: 0 };
-
- const walk = (value) => {
- if (!value || typeof value !== "object") return value;
-
- stats.normalizedMediaRefs += normalizeMediaReference(value);
-
- if (Array.isArray(value)) {
- for (let i = value.length - 1; i >= 0; i--) {
- if (typeof value[i] === "string" && value[i].includes("rgthree.compare._temp_")) {
- value.splice(i, 1);
- stats.clearedTempPreviews++;
- } else {
- const next = walk(value[i]);
- if (next !== value[i]) value[i] = next;
- }
- }
- return value;
- }
-
- for (const [key, child] of Object.entries(value)) {
- if (typeof child === "string") {
- const stripped = stripPreviewUrl(child);
- if (stripped !== child) {
- value[key] = stripped;
- stats.clearedTempPreviews++;
- }
- } else {
- const next = walk(child);
- if (next !== child) value[key] = next;
- }
- }
-
- return value;
- };
-
- walk(sanitized);
-
- for (const node of sanitized?.nodes ?? []) {
- if (node?.type === "Image Comparer (rgthree)" && Array.isArray(node.widgets_values)) {
- for (let i = 0; i < node.widgets_values.length; i++) {
- const widgetValue = node.widgets_values[i];
- const asJson = JSON.stringify(widgetValue ?? "");
- if (asJson.includes("rgthree.compare._temp_")) {
- node.widgets_values[i] = [];
- stats.clearedTempPreviews++;
- }
- }
- }
- }
-
- if (stats.normalizedMediaRefs || stats.clearedTempPreviews) {
- console.warn("[LoadImageDropFix] sanitized dropped workflow", stats);
- }
-
- return sanitized;
- };
-
- const focusLoadedWorkflow = () => {
- requestAnimationFrame(() => {
- const canvas = app.canvas;
- const graph = canvas?.graph ?? app.rootGraph;
- const firstNode = graph?.nodes?.[0];
- if (firstNode && canvas?.centerOnNode) {
- canvas.centerOnNode(firstNode);
- }
- canvas?.setDirty?.(true, true);
- });
- };
-
- const isApiJson = (data) => {
- return (
- data &&
- typeof data === "object" &&
- !Array.isArray(data) &&
- Object.keys(data).length > 0 &&
- Object.values(data).every((node) => node?.class_type)
- );
- };
-
- const getWorkflowData = async (file) => {
- const serverWorkflowData = await getWorkflowDataFromServer(file);
- if (
- serverWorkflowData?.workflow ||
- serverWorkflowData?.prompt ||
- serverWorkflowData?.parameters ||
- serverWorkflowData?.templates
- ) {
- return serverWorkflowData;
- }
-
- if (isWorkflowFile(file)) {
- const data = parseJson(await readTextFile(file));
- if (data?.workflow || data?.Workflow || data?.prompt || data?.Prompt) {
- return {
- workflow: data.workflow ?? data.Workflow,
- prompt: data.prompt ?? data.Prompt,
- parameters: data.parameters,
- templates: data.templates,
- };
- }
- if (data?.templates) return { templates: data.templates };
- if (isApiJson(data)) return { prompt: data };
- return { workflow: data };
- }
-
- if (!isImageFile(file)) return {};
-
- const metadata = await getImageMetadata(file);
- return !!(
- metadata?.workflow ||
- metadata?.Workflow ||
- metadata?.prompt ||
- metadata?.Prompt ||
- metadata?.parameters ||
- metadata?.parametersText ||
- metadata?.templates
- )
- ? {
- workflow: metadata.workflow ?? metadata.Workflow,
- prompt: metadata.prompt ?? metadata.Prompt,
- parameters: metadata.parameters ?? metadata.parametersText,
- templates: metadata.templates,
- }
- : {};
- };
-
- const hasWorkflowData = (workflowData) => {
- return !!(
- workflowData?.workflow ||
- workflowData?.prompt ||
- workflowData?.parameters ||
- workflowData?.templates
- );
- };
-
- const loadWorkflowFromFile = async (file, workflowData) => {
- workflowData ??= await getWorkflowData(file);
- const fileName = file.name.replace(/\.\w+$/, "");
-
- if (workflowData?.templates && app.loadTemplateData) {
- app.loadTemplateData({ templates: workflowData.templates });
- return true;
- }
-
- if (workflowData?.workflow) {
- const workflow = parseJson(workflowData.workflow);
- if (workflow && typeof workflow === "object" && !Array.isArray(workflow)) {
- const sanitizedWorkflow = sanitizeWorkflowForDrop(workflow);
- console.warn("[LoadImageDropFix] workflow data parsed", {
- file: file.name,
- nodes: sanitizedWorkflow.nodes?.length ?? 0,
- version: sanitizedWorkflow.version,
- });
- await app.loadGraphData(sanitizedWorkflow, true, true, fileName, {
- openSource: "file_drop",
- deferWarnings: true,
- skipAssetScans: true,
- silentAssetErrors: true,
- });
- console.warn("[LoadImageDropFix] workflow loaded", file.name);
- focusLoadedWorkflow();
- return true;
- }
- }
-
- if (workflowData?.prompt && app.loadApiJson) {
- const prompt = parseJson(workflowData.prompt);
- if (prompt) {
- console.warn("[LoadImageDropFix] api json parsed", {
- file: file.name,
- nodes: Object.keys(prompt).length,
- });
- await app.loadApiJson(prompt, fileName);
- console.warn("[LoadImageDropFix] api json loaded", file.name);
- focusLoadedWorkflow();
- return true;
- }
- }
-
- if (workflowData?.prompt) {
- console.warn("[LoadImageDropFix] api json fallback to native loader", file.name);
- await app.handleFile(file, "file_drop", { deferWarnings: true });
- focusLoadedWorkflow();
- return true;
- }
-
- if (workflowData?.parameters) {
- await app.handleFile(file, "file_drop", { deferWarnings: true });
- return true;
- }
-
- return false;
- };
-
- const handleWorkflowFile = async (file, workflowData) => {
- try {
- if (await loadWorkflowFromFile(file, workflowData)) return true;
- } catch (error) {
- console.warn("[LoadImageDropFix] direct workflow load failed", error);
- }
-
- const originalLoadGraphData = app.loadGraphData;
- try {
- app.loadGraphData = function (
- graphData,
- clean,
- restoreView,
- workflow,
- options = {},
- ) {
- return originalLoadGraphData.call(
- this,
- graphData,
- clean,
- restoreView,
- workflow,
- {
- ...options,
- deferWarnings: true,
- skipAssetScans: true,
- silentAssetErrors: true,
- },
- );
- };
- await app.handleFile(file, "file_drop", { deferWarnings: true });
- return true;
- } catch (error) {
- console.warn("[LoadImageDropFix] native workflow load failed", error);
- return false;
- } finally {
- app.loadGraphData = originalLoadGraphData;
- }
- };
-
- const uploadImage = async (file) => {
- const body = new FormData();
- body.append("image", file);
- body.append("type", "input");
- body.append("overwrite", "true");
-
- const data = await new Promise((resolve, reject) => {
- const request = new XMLHttpRequest();
- request.open("POST", "/upload/image");
- request.onload = () => {
- if (request.status < 200 || request.status >= 300) {
- reject(new Error(`${request.status} ${request.statusText}`));
- return;
- }
-
- try {
- resolve(JSON.parse(request.responseText));
- } catch (error) {
- reject(error);
- }
- };
- request.onerror = () => reject(new Error("image upload failed"));
- request.onabort = () => reject(new Error("image upload aborted"));
- request.send(body);
- });
- return data.subfolder ? `${data.subfolder}/${data.name}` : data.name;
- };
-
- const addToComboValues = (widget, value) => {
- const values = widget?.options?.values;
- if (!Array.isArray(values)) return;
-
- const valuesToAdd = Array.isArray(value) ? value : [value];
- for (const item of valuesToAdd) {
- if (!values.includes(item)) values.push(item);
- }
- };
-
- const setNodeImage = (node, value) => {
- const widget = node?.widgets?.find?.((widget) => widget.name === "image");
- if (!widget) {
- throw new Error(`LoadImage node ${node?.id ?? ""} has no image widget`);
- }
-
- const oldValue = widget.value;
- addToComboValues(widget, value);
- widget.value = value;
- widget.callback?.(value);
- node.onWidgetChanged?.(widget.name, value, oldValue, widget);
- node.imgs = undefined;
- node.graph?.setDirtyCanvas?.(true, true);
- app.canvas?.setDirty?.(true, true);
- };
-
- const createLoadImageNode = (event) => {
- const liteGraph = window.LiteGraph;
- const graph = app.canvas?.graph ?? app.rootGraph;
- if (!liteGraph?.createNode || !graph?.add) {
- throw new Error("LiteGraph is not ready");
- }
-
- app.canvas?.adjustMouseEvent?.(event);
- const node = liteGraph.createNode("LoadImage");
- if (!node) throw new Error("Failed to create LoadImage node");
-
- node.pos = [
- event.canvasX ?? app.canvas?.graph_mouse?.[0] ?? 0,
- event.canvasY ?? app.canvas?.graph_mouse?.[1] ?? 0,
- ];
- graph.add(node);
- return node;
- };
-
- const uploadImagesToNode = async (files, node, event) => {
- const imageFiles = files.filter(isImageFile);
- if (!imageFiles.length) return false;
-
- node ??= createLoadImageNode(event);
- node.isUploading = true;
- try {
- node.imgs = undefined;
- node.graph?.setDirtyCanvas?.(true, true);
-
- const paths = [];
- for (const file of imageFiles) {
- paths.push(await uploadImage(file));
- }
-
- setNodeImage(node, paths[0]);
- return true;
- } finally {
- node.isUploading = false;
- node.graph?.setDirtyCanvas?.(true, true);
- }
- };
-
- const clearPatchedDragOverNode = () => {
- if (patchedDragOverNode && app.dragOverNode === patchedDragOverNode) {
- app.dragOverNode = null;
- }
- patchedDragOverNode = null;
- lastPatchedDrag = null;
- };
-
- const getDropNode = (event) => {
- const node = getNodeUnderEvent(event);
- if (isLoadImageNode(node)) return node;
-
- if (!patchedDragOverNode || !lastPatchedDrag) return null;
-
- const elapsed = performance.now() - lastPatchedDrag.time;
- const distance = Math.hypot(
- event.clientX - lastPatchedDrag.clientX,
- event.clientY - lastPatchedDrag.clientY,
- );
-
- if (elapsed < 1000 && distance < 48) return patchedDragOverNode;
- return null;
- };
-
- const handleDragOver = (event) => {
- const node = getNodeUnderEvent(event);
- const canDrop =
- (isLoadImageNode(node) && isDraggingImages(event)) ||
- (!node && isDraggingFiles(event));
-
- if (isLoadImageNode(node)) {
- const key = `${node.id}:${canDrop}`;
- if (key !== lastDragOverLogKey) {
- lastDragOverLogKey = key;
- console.warn("[LoadImageDropFix] dragover LoadImage", {
- id: node.id,
- canDrop,
- });
- }
- }
-
- if (!canDrop) {
- clearPatchedDragOverNode();
- return;
- }
-
- patchedDragOverNode = node;
- lastPatchedDrag = {
- clientX: event.clientX,
- clientY: event.clientY,
- time: performance.now(),
- };
- app.dragOverNode = node;
- event.preventDefault();
- requestAnimationFrame(() => app.canvas?.setDirty?.(false, true));
- };
-
- const handleDrop = async (event) => {
- const node = getDropNode(event);
- const files = getFiles(event);
-
- if (!files.length) {
- clearPatchedDragOverNode();
- return;
- }
-
- if (isLoadImageNode(node)) {
- event.preventDefault();
- event.stopPropagation();
-
- try {
- console.warn("[LoadImageDropFix] upload image to node", node.id);
- await uploadImagesToNode(files, node, event);
- } finally {
- clearPatchedDragOverNode();
- app.canvas?.setDirty?.(false, true);
- }
- return;
- }
-
- if (node) {
- clearPatchedDragOverNode();
- return;
- }
-
- event.preventDefault();
- event.stopPropagation();
-
- try {
- app.canvas?.adjustMouseEvent?.(event);
- if (app.canvas?.graph_mouse) {
- app.canvas.graph_mouse[0] = event.canvasX;
- app.canvas.graph_mouse[1] = event.canvasY;
- }
-
- for (const file of files) {
- const workflowData = isWorkflowMetadataCandidate(file)
- ? await getWorkflowData(file)
- : {};
-
- if (hasWorkflowData(workflowData)) {
- console.warn("[LoadImageDropFix] load workflow from drop", file.name);
- await handleWorkflowFile(file, workflowData);
- } else if (isImageFile(file)) {
- console.warn("[LoadImageDropFix] create LoadImage from drop", file.name);
- await uploadImagesToNode([file], createLoadImageNode(event), event);
- } else {
- await handleWorkflowFile(file, workflowData);
- }
- }
- } catch (error) {
- console.warn("[LoadImageDropFix] drop handling failed", error);
- for (const file of files.filter(isImageFile)) {
- try {
- await uploadImagesToNode([file], createLoadImageNode(event), event);
- } catch (uploadError) {
- console.warn("[LoadImageDropFix] image fallback failed", uploadError);
- }
- }
- } finally {
- clearPatchedDragOverNode();
- app.canvas?.setDirty?.(true, true);
- }
- };
-
- document.addEventListener("dragover", handleDragOver, true);
- document.addEventListener(
- "drop",
- (event) => {
- handleDrop(event).catch((error) => {
- console.warn("[LoadImageDropFix] unhandled drop failure", error);
- clearPatchedDragOverNode();
- app.canvas?.setDirty?.(true, true);
- });
- },
- true,
- );
- },
-});