diff --git a/js/dragdrop.js b/js/dragdrop.js index 50a89e2..f8346a6 100644 --- a/js/dragdrop.js +++ b/js/dragdrop.js @@ -1,5 +1,4 @@ import { app } from "/scripts/app.js"; -import { State } from "./state.js"; async function uploadDroppedImageToInput(imageObj) { const src = `/view?filename=${encodeURIComponent(imageObj.filename)}&type=${imageObj.type || 'output'}&subfolder=${encodeURIComponent(imageObj.subfolder || '')}`; @@ -22,72 +21,83 @@ async function uploadDroppedImageToInput(imageObj) { export function setupDragAndDrop() { document.addEventListener("dragover", (e) => { - if (State.currentDraggedImgData) { - e.preventDefault(); - e.dataTransfer.dropEffect = "copy"; - } + e.preventDefault(); + e.dataTransfer.dropEffect = "copy"; }); document.addEventListener("drop", async (e) => { - if (!State.currentDraggedImgData) return; - const canvas = app.canvas; - if (canvas && canvas.graph) { - let targetNode = null; - if (canvas.convertEventToCanvasOffset) { - const pos = canvas.convertEventToCanvasOffset(e); - targetNode = canvas.graph.getNodeOnPos(pos[0], pos[1]); - } else { - const rect = canvas.canvas.getBoundingClientRect(); - targetNode = canvas.graph.getNodeOnPos((e.clientX - rect.left - canvas.ds.offset[0]) / canvas.ds.scale, (e.clientY - rect.top - canvas.ds.offset[1]) / canvas.ds.scale); - } + if (!canvas || !canvas.graph) return; - let droppedOnImageNode = false; - - if (targetNode && (targetNode.type.includes("LoadImage") || targetNode.widgets?.some(w => w.name === "image"))) { - if (State.currentDraggedImgData.filename) { + // 1. Check for standard workflow JSON data drop + const jsonStr = e.dataTransfer.getData("application/json") || e.dataTransfer.getData("text/plain"); + if (jsonStr) { + try { + const workflow = JSON.parse(jsonStr); + if (workflow && workflow.nodes) { e.preventDefault(); e.stopPropagation(); - droppedOnImageNode = true; - const widget = targetNode.widgets.find(w => w.name === "image"); - if (widget) { - const newFilename = await uploadDroppedImageToInput(State.currentDraggedImgData); - if (newFilename) { - widget.value = newFilename; - if (widget.callback) widget.callback(widget.value); - targetNode.imgs = null; - app.graph.setDirtyCanvas(true, true); + if (app.loadGraphData) { + app.loadGraphData(workflow); + } else if (app.handleFile) { + const file = new File([jsonStr], "workflow.json", { type: "application/json" }); + await app.handleFile(file); + } + return; + } + } catch (err) {} + } + + // 2. Check for native image URL drop + const url = e.dataTransfer.getData("text/uri-list") || e.dataTransfer.getData("text/plain"); + if (url) { + try { + const urlObj = new URL(url, window.location.origin); + if (urlObj.pathname === "/view") { + const filename = urlObj.searchParams.get("filename"); + const type = urlObj.searchParams.get("type") || "output"; + const subfolder = urlObj.searchParams.get("subfolder") || ""; + + if (filename) { + e.preventDefault(); + e.stopPropagation(); + + let targetNode = null; + if (canvas.convertEventToCanvasOffset) { + const pos = canvas.convertEventToCanvasOffset(e); + targetNode = canvas.graph.getNodeOnPos(pos[0], pos[1]); + } else { + const rect = canvas.canvas.getBoundingClientRect(); + targetNode = canvas.graph.getNodeOnPos((e.clientX - rect.left - canvas.ds.offset[0]) / canvas.ds.scale, (e.clientY - rect.top - canvas.ds.offset[1]) / canvas.ds.scale); + } + + if (targetNode && (targetNode.type.includes("LoadImage") || targetNode.widgets?.some(w => w.name === "image"))) { + // Dropped on a node with image input: Copy/Upload image to inputs + const widget = targetNode.widgets.find(w => w.name === "image"); + if (widget) { + const newFilename = await uploadDroppedImageToInput({ filename, type, subfolder }); + if (newFilename) { + widget.value = newFilename; + if (widget.callback) widget.callback(widget.value); + targetNode.imgs = null; + app.graph.setDirtyCanvas(true, true); + } + } + } else { + // Dropped on empty canvas: Load workflow natively + const src = `/view?filename=${encodeURIComponent(filename)}&type=${type}&subfolder=${encodeURIComponent(subfolder)}`; + const res = await fetch(src); + const blob = await res.blob(); + const file = new File([blob], filename || "workflow.png", { type: blob.type }); + + if (app.handleFile) await app.handleFile(file); + else if (app.canvas?.handleDropItem) app.canvas.handleDropItem({ getAsFile: () => file }); } } } - } - - if (!droppedOnImageNode) { - e.preventDefault(); - e.stopPropagation(); - try { - if (State.currentDraggedImgData.workflow) { - if (app.loadGraphData) { - app.loadGraphData(State.currentDraggedImgData.workflow); - } else if (app.handleFile) { - const jsonStr = JSON.stringify(State.currentDraggedImgData.workflow); - const file = new File([jsonStr], "workflow.json", { type: "application/json" }); - await app.handleFile(file); - } - } else if (State.currentDraggedImgData.filename) { - const src = State.currentDraggedImgData.url || `/view?filename=${encodeURIComponent(State.currentDraggedImgData.filename)}&type=${State.currentDraggedImgData.type || 'output'}&subfolder=${encodeURIComponent(State.currentDraggedImgData.subfolder || '')}`; - const res = await fetch(src); - const blob = await res.blob(); - const file = new File([blob], State.currentDraggedImgData.filename || "workflow.png", { type: blob.type }); - - if (app.handleFile) await app.handleFile(file); - else if (app.canvas?.handleDropItem) app.canvas.handleDropItem({ getAsFile: () => file }); - } - } catch (err) { - console.error("Comfy Sidebar: Failed to synthesize workflow file drop:", err); - } + } catch (err) { + console.error("Comfy Sidebar: Failed to handle dropped native URL:", err); } } - State.currentDraggedImgData = null; }, true); } \ No newline at end of file diff --git a/js/sidebar.js b/js/sidebar.js index 28677d6..fc1bc6b 100644 --- a/js/sidebar.js +++ b/js/sidebar.js @@ -17,20 +17,6 @@ app.registerExtension({ app.ui.settings.addSetting({ id: "Comfy Sidebar.Show Working Node Name", name: "Shows the name of the node which is currently in the process", type: "boolean", defaultValue: true }); app.ui.settings.addSetting({ id: "Comfy Sidebar.Auto Clear Interrupted", name: "Auto-clear cancelled & failed jobs on new generation", type: "boolean", defaultValue: false }); - // --- THE "MOON PROOF" MONKEYPATCH --- - // Intercepts ui.js reading the old settings group so we don't get flashing buttons - const originalGetSetting = app.ui.settings.getSettingValue; - if (originalGetSetting && !originalGetSetting.__patchedBySilver) { - const patchedGetter = function(id) { - if (id === "Comfy Sidebar.Hide Sidebar Tabs.Override Stock Job History Tab") { - return originalGetSetting.call(app.ui.settings, "Comfy Sidebar.Hide Junk.Override Stock Job History Tab"); - } - return originalGetSetting.call(app.ui.settings, id); - }; - patchedGetter.__patchedBySilver = true; - app.ui.settings.getSettingValue = patchedGetter; - } - // Add settings to hide individual sidebar tabs under the "Hide Junk" sub-category // We use setTimeout to let Comfy finish writing the setting value before reading it const sidebarTabs = ["Assets", "Nodes", "Models", "Workflows", "Apps", "NodesMap", "Templates"]; diff --git a/js/state.js b/js/state.js index 2dccd18..4f47bbf 100644 --- a/js/state.js +++ b/js/state.js @@ -3,7 +3,6 @@ export const cardElements = new Map(); // Global shared variables export const State = { - currentDraggedImgData: null, currentSearchQuery: "", globalOrderCounter: 0, sidebarContainer: null, diff --git a/js/ui.js b/js/ui.js index 46113b0..20abadf 100644 --- a/js/ui.js +++ b/js/ui.js @@ -85,7 +85,7 @@ export const findStandardQueueButton = () => { }; export const applySidebarOverride = () => { - const overrideStock = app.ui.settings.getSettingValue("Comfy Sidebar.Hide Sidebar Tabs.Override Stock Job History Tab") ?? false; + const overrideStock = app.ui.settings.getSettingValue("Comfy Sidebar.Hide Junk.Override Stock Job History Tab") ?? false; const stdBtn = findStandardQueueButton(); const ourBtn = findOurSidebarButton(); @@ -326,24 +326,31 @@ export function renderDOM() { card.addEventListener("mouseleave", () => cardObj.hoverPanel.style.display = "none"); card.addEventListener("dragstart", (e) => { - if (cardObj.firstImgElement) e.dataTransfer.setDragImage(cardObj.firstImgElement, 15, 15); - if (state.images && state.images[0]) { - State.currentDraggedImgData = { ...state.images[0], workflow: state.workflow }; - const img = state.images[0]; - const fileUrl = img.url ? img.url : window.location.origin + `/view?filename=${encodeURIComponent(img.filename)}&type=${img.type}&subfolder=${encodeURIComponent(img.subfolder)}`; - try { e.dataTransfer.setData("DownloadURL", `image/png:${img.filename || "output.png"}:${fileUrl}`); e.dataTransfer.setData("text/uri-list", fileUrl); e.dataTransfer.setData("text/plain", fileUrl); e.dataTransfer.items.add(new File([""], img.filename || "output.png", { type: "image/png" })); } catch (err) {} - } else if (state.workflow) { - State.currentDraggedImgData = { workflow: state.workflow }; + // Custom dragstart is only used for JSON-only workflows to supply a local DownloadURL. + // For image cards, dragging is handled natively by the browser on the child tag. + if (state.workflow && (!state.images || state.images.length === 0)) { + if (cardObj.firstImgElement) e.dataTransfer.setDragImage(cardObj.firstImgElement, 15, 15); const jsonStr = JSON.stringify(state.workflow, null, 2); - try { e.dataTransfer.setData("DownloadURL", `application/json:workflow_${state.pid}.json:data:application/json;base64,` + btoa(unescape(encodeURIComponent(jsonStr)))); e.dataTransfer.setData("text/plain", jsonStr); e.dataTransfer.setData("application/json", jsonStr); } catch (err) {} + try { + e.dataTransfer.setData("DownloadURL", `application/json:workflow_${state.pid}.json:data:application/json;base64,` + btoa(unescape(encodeURIComponent(jsonStr)))); + e.dataTransfer.setData("text/plain", jsonStr); + e.dataTransfer.setData("application/json", jsonStr); + } catch (err) {} + e.dataTransfer.effectAllowed = "copy"; } - e.dataTransfer.effectAllowed = "copy"; }); cardElements.set(state.pid, cardObj); } cardObj.element.className = `comfy-sidebar-card ${state.status}`; - cardObj.element.setAttribute("draggable", "true"); + + // Native drag isolation: completely remove draggable flags on image containers. + // This exposes the child tag directly to Chrome's native filesystem drag APIs. + if (state.images && state.images.length > 0) { + cardObj.element.removeAttribute("draggable"); + } else { + cardObj.element.setAttribute("draggable", "true"); + } if (state.status === "active") { cardObj.timerEl.textContent = state.startTime ? ((Date.now() - state.startTime) / 1000).toFixed(2) + "s" : "..."; @@ -430,7 +437,7 @@ export function renderDOM() { let canReuse = cardObj.grid.children.length === state.images.length; if (canReuse) { state.images.forEach((img, idx) => { - const src = img.url ? img.url : `/view?filename=${encodeURIComponent(img.filename)}&type=${img.type}&subfolder=${encodeURIComponent(img.subfolder)}`; + const src = img.url ? img.url : window.location.origin + `/view?filename=${encodeURIComponent(img.filename)}&type=${img.type}&subfolder=${encodeURIComponent(img.subfolder)}`; const wrapper = cardObj.grid.children[idx]; // Fallback in case the DOM still has old unwrapped elements const mediaEl = (wrapper.tagName === "IMG" || wrapper.tagName === "VIDEO") ? wrapper : wrapper.querySelector("img, video"); @@ -440,7 +447,7 @@ export function renderDOM() { if (canReuse) { state.images.forEach((img, idx) => { - let src = img.url ? img.url : `/view?filename=${encodeURIComponent(img.filename)}&type=${img.type}&subfolder=${encodeURIComponent(img.subfolder)}`; + let src = img.url ? img.url : window.location.origin + `/view?filename=${encodeURIComponent(img.filename)}&type=${img.type}&subfolder=${encodeURIComponent(img.subfolder)}`; const isVideo = isVideoFormat(src); if (isVideo) src = src + "#t=0.001"; // Keep the first-frame trick on update @@ -464,7 +471,7 @@ export function renderDOM() { } else { cardObj.grid.innerHTML = ""; cardObj.firstImgElement = null; state.images.forEach(img => { - const src = img.url ? img.url : `/view?filename=${encodeURIComponent(img.filename)}&type=${img.type}&subfolder=${encodeURIComponent(img.subfolder)}`; + const src = img.url ? img.url : window.location.origin + `/view?filename=${encodeURIComponent(img.filename)}&type=${img.type}&subfolder=${encodeURIComponent(img.subfolder)}`; const isVideo = isVideoFormat(src); // Container to hold both the media and the play icon @@ -472,7 +479,15 @@ export function renderDOM() { Object.assign(wrapper.style, { position: "relative", width: "100%", display: "block" }); const thumb = isVideo ? document.createElement("video") : document.createElement("img"); - Object.assign(thumb.style, { width: "100%", borderRadius: "2px", display: "block", cursor: "zoom-in" }); + + // Fully-qualified absolute same-origin HTTP URLs mapped alongside webkitUserDrag force flags + Object.assign(thumb.style, { + width: "100%", + borderRadius: "2px", + display: "block", + cursor: "zoom-in", + webkitUserDrag: "element" + }); if (isVideo) { thumb.muted = true; @@ -503,7 +518,13 @@ export function renderDOM() { } if (!keepAspect) { thumb.style.aspectRatio = "1 / 1"; thumb.style.objectFit = "cover"; } - thumb.setAttribute("draggable", "false"); + + // Keep native image tags naturally draggable + if (!isVideo) { + thumb.setAttribute("draggable", "true"); + } else { + thumb.setAttribute("draggable", "false"); + } if (!isVideo) { thumb.onload = () => { if (keepAspect && thumb.naturalWidth && thumb.naturalHeight) thumb.style.aspectRatio = `${thumb.naturalWidth} / ${thumb.naturalHeight}`; }; @@ -512,7 +533,10 @@ export function renderDOM() { thumb.onloadedmetadata = () => { if (keepAspect && thumb.videoWidth && thumb.videoHeight) thumb.style.aspectRatio = `${thumb.videoWidth} / ${thumb.videoHeight}`; }; thumb.src = src + "#t=0.001"; } - thumb.onclick = (ev) => { ev.stopPropagation(); showFullscreenPreview(state.images.map(i => i.url ? i.url : `/view?filename=${encodeURIComponent(i.filename)}&type=${i.type}&subfolder=${encodeURIComponent(i.subfolder)}`)); }; + thumb.onclick = (ev) => { + ev.stopPropagation(); + showFullscreenPreview(state.images.map(i => i.url ? i.url : window.location.origin + `/view?filename=${encodeURIComponent(i.filename)}&type=${encodeURIComponent(i.type || 'output')}&subfolder=${encodeURIComponent(i.subfolder || '')}`)); + }; wrapper.insertBefore(thumb, wrapper.firstChild); if (!cardObj.firstImgElement) cardObj.firstImgElement = thumb;