Further optimizations
This commit is contained in:
Vendored
+66
-56
@@ -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);
|
||||
}
|
||||
@@ -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"];
|
||||
|
||||
@@ -3,7 +3,6 @@ export const cardElements = new Map();
|
||||
|
||||
// Global shared variables
|
||||
export const State = {
|
||||
currentDraggedImgData: null,
|
||||
currentSearchQuery: "",
|
||||
globalOrderCounter: 0,
|
||||
sidebarContainer: null,
|
||||
|
||||
@@ -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 <img> 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 <img> 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;
|
||||
|
||||
Reference in New Issue
Block a user