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, - ); - }, -});