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;