Enhance clipboard and drag & drop image handling

Adds robust clipboard and drag & drop support for images, including ComfyUI Clipspace integration, system clipboard fallback, and improved user feedback. Clipboard and paste logic is centralized and clarified, with priority handling for internal clipboard, ComfyUI Clipspace, and system clipboard. Drag & drop is now handled at the canvas level, and tooltips and notifications provide clearer guidance for users.
This commit is contained in:
Dariusz L
2025-07-01 11:15:40 +02:00
parent 0f05e36333
commit 0512200b92
4 changed files with 505 additions and 262 deletions

View File

@@ -33,6 +33,9 @@ export class CanvasInteractions {
this.canvas.canvas.addEventListener('wheel', this.handleWheel.bind(this), {passive: false});
this.canvas.canvas.addEventListener('keydown', this.handleKeyDown.bind(this));
this.canvas.canvas.addEventListener('keyup', this.handleKeyUp.bind(this));
// Add paste event listener like ComfyUI does
document.addEventListener('paste', this.handlePasteEvent.bind(this));
this.canvas.canvas.addEventListener('mouseenter', (e) => {
this.canvas.isMouseOver = true;
@@ -42,6 +45,12 @@ export class CanvasInteractions {
this.canvas.isMouseOver = false;
this.handleMouseLeave(e);
});
// Add drag & drop support
this.canvas.canvas.addEventListener('dragover', this.handleDragOver.bind(this));
this.canvas.canvas.addEventListener('dragenter', this.handleDragEnter.bind(this));
this.canvas.canvas.addEventListener('dragleave', this.handleDragLeave.bind(this));
this.canvas.canvas.addEventListener('drop', this.handleDrop.bind(this));
}
resetInteractionState() {
@@ -343,16 +352,13 @@ export class CanvasInteractions {
}
if (e.key.toLowerCase() === 'c') {
if (this.canvas.selectedLayers.length > 0) {
e.preventDefault();
e.stopPropagation();
this.canvas.canvasLayers.copySelectedLayers();
}
return;
}
if (e.key.toLowerCase() === 'v') {
e.preventDefault();
e.stopPropagation();
this.canvas.canvasLayers.handlePaste('mouse');
// Don't prevent default - let the natural paste event fire
// which is handled by handlePasteEvent
return;
}
}
@@ -717,4 +723,137 @@ export class CanvasInteractions {
this.canvas.viewport.y -= rectY;
}
}
// Drag & Drop handlers
handleDragOver(e) {
e.preventDefault();
e.stopPropagation(); // Prevent ComfyUI from handling this event
e.dataTransfer.dropEffect = 'copy';
}
handleDragEnter(e) {
e.preventDefault();
e.stopPropagation(); // Prevent ComfyUI from handling this event
this.canvas.canvas.style.backgroundColor = 'rgba(45, 90, 160, 0.1)';
this.canvas.canvas.style.border = '2px dashed #2d5aa0';
}
handleDragLeave(e) {
e.preventDefault();
e.stopPropagation(); // Prevent ComfyUI from handling this event
// Only reset if we're actually leaving the canvas (not just moving between child elements)
if (!this.canvas.canvas.contains(e.relatedTarget)) {
this.canvas.canvas.style.backgroundColor = '';
this.canvas.canvas.style.border = '';
}
}
async handleDrop(e) {
e.preventDefault();
e.stopPropagation(); // CRITICAL: Prevent ComfyUI from handling this event and loading workflow
log.info("Canvas drag & drop event intercepted - preventing ComfyUI workflow loading");
// Reset visual feedback
this.canvas.canvas.style.backgroundColor = '';
this.canvas.canvas.style.border = '';
const files = Array.from(e.dataTransfer.files);
const worldCoords = this.canvas.getMouseWorldCoordinates(e);
log.info(`Dropped ${files.length} file(s) onto canvas at position (${worldCoords.x}, ${worldCoords.y})`);
for (const file of files) {
if (file.type.startsWith('image/')) {
try {
await this.loadDroppedImageFile(file, worldCoords);
log.info(`Successfully loaded dropped image: ${file.name}`);
} catch (error) {
log.error(`Failed to load dropped image ${file.name}:`, error);
}
} else {
log.warn(`Skipped non-image file: ${file.name} (${file.type})`);
}
}
}
async loadDroppedImageFile(file, worldCoords) {
const reader = new FileReader();
reader.onload = async (e) => {
const img = new Image();
img.onload = async () => {
// Check fit_on_add widget to determine add mode
const fitOnAddWidget = this.canvas.node.widgets.find(w => w.name === "fit_on_add");
const addMode = fitOnAddWidget && fitOnAddWidget.value ? 'fit' : 'center';
// Use the same method as "Add Image" button for consistency
await this.canvas.addLayer(img, {}, addMode);
};
img.onerror = () => {
log.error(`Failed to load dropped image: ${file.name}`);
};
img.src = e.target.result;
};
reader.onerror = () => {
log.error(`Failed to read dropped file: ${file.name}`);
};
reader.readAsDataURL(file);
}
// Paste event handler that respects clipboard preference
async handlePasteEvent(e) {
// Check if we should handle this paste event
const shouldHandle = this.canvas.isMouseOver ||
this.canvas.canvas.contains(document.activeElement) ||
document.activeElement === this.canvas.canvas ||
document.activeElement === document.body;
if (!shouldHandle) {
log.debug("Paste event ignored - not focused on canvas");
return;
}
log.info("Paste event detected, checking clipboard preference");
// Check clipboard preference first
const preference = this.canvas.canvasLayers.clipboardPreference;
if (preference === 'clipspace') {
// For clipspace preference, always delegate to ClipboardManager
log.info("Clipboard preference is clipspace, delegating to ClipboardManager");
e.preventDefault();
e.stopPropagation();
await this.canvas.canvasLayers.clipboardManager.handlePaste('mouse', preference);
return;
}
// For system preference, check direct image data first, then delegate
const clipboardData = e.clipboardData;
if (clipboardData && clipboardData.items) {
for (const item of clipboardData.items) {
if (item.type.startsWith('image/')) {
e.preventDefault();
e.stopPropagation();
const file = item.getAsFile();
if (file) {
log.info("Found direct image data in paste event");
const reader = new FileReader();
reader.onload = async (event) => {
const img = new Image();
img.onload = async () => {
await this.canvas.canvasLayers.addLayerWithImage(img, {}, 'mouse');
};
img.src = event.target.result;
};
reader.readAsDataURL(file);
return;
}
}
}
}
// If no direct image data, delegate to ClipboardManager with system preference
await this.canvas.canvasLayers.clipboardManager.handlePaste('mouse', preference);
}
}

View File

@@ -34,17 +34,68 @@ export class CanvasLayers {
async copySelectedLayers() {
if (this.canvas.selectedLayers.length === 0) return;
// Always copy to internal clipboard first
this.internalClipboard = this.canvas.selectedLayers.map(layer => ({...layer}));
log.info(`Copied ${this.internalClipboard.length} layer(s) to internal clipboard.`);
try {
const blob = await this.getFlattenedSelectionAsBlob();
if (blob) {
// Get flattened image
const blob = await this.getFlattenedSelectionAsBlob();
if (!blob) {
log.warn("Failed to create flattened selection blob");
return;
}
// Copy to external clipboard based on preference
if (this.clipboardPreference === 'clipspace') {
try {
// Copy to ComfyUI Clipspace
const dataURL = await new Promise((resolve) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.readAsDataURL(blob);
});
// Create temporary image for clipspace
const img = new Image();
img.onload = () => {
// Add to ComfyUI Clipspace
if (this.canvas.node.imgs) {
this.canvas.node.imgs = [img];
} else {
this.canvas.node.imgs = [img];
}
// Use ComfyUI's clipspace functionality
if (ComfyApp.copyToClipspace) {
ComfyApp.copyToClipspace(this.canvas.node);
log.info("Flattened selection copied to ComfyUI Clipspace.");
} else {
log.warn("ComfyUI copyToClipspace not available");
}
};
img.src = dataURL;
} catch (error) {
log.error("Failed to copy image to ComfyUI Clipspace:", error);
// Fallback to system clipboard
try {
const item = new ClipboardItem({'image/png': blob});
await navigator.clipboard.write([item]);
log.info("Fallback: Flattened selection copied to system clipboard.");
} catch (fallbackError) {
log.error("Failed to copy to system clipboard as fallback:", fallbackError);
}
}
} else {
// Copy to system clipboard (default behavior)
try {
const item = new ClipboardItem({'image/png': blob});
await navigator.clipboard.write([item]);
log.info("Flattened selection copied to the system clipboard.");
log.info("Flattened selection copied to system clipboard.");
} catch (error) {
log.error("Failed to copy image to system clipboard:", error);
}
} catch (error) {
log.error("Failed to copy image to system clipboard:", error);
}
}
@@ -89,55 +140,14 @@ export class CanvasLayers {
try {
log.info(`Paste operation started with preference: ${this.clipboardPreference}`);
if (this.internalClipboard.length > 0) {
log.info("Pasting from internal clipboard");
this.pasteLayers();
return;
}
if (this.clipboardPreference === 'clipspace') {
log.info("Attempting paste from ComfyUI Clipspace");
if (!await this.tryClipspacePaste(addMode)) {
log.info("No image found in ComfyUI Clipspace");
}
} else if (this.clipboardPreference === 'system') {
log.info("Attempting paste from system clipboard");
await this.trySystemClipboardPaste(addMode);
}
// Delegate all clipboard handling to ClipboardManager (it will check internal clipboard first)
await this.clipboardManager.handlePaste(addMode, this.clipboardPreference);
} catch (err) {
log.error("Paste operation failed:", err);
}
}
async tryClipspacePaste(addMode) {
try {
log.info("Attempting to paste from ComfyUI Clipspace");
const clipspaceResult = ComfyApp.pasteFromClipspace(this.canvas.node);
if (this.canvas.node.imgs && this.canvas.node.imgs.length > 0) {
const clipspaceImage = this.canvas.node.imgs[0];
if (clipspaceImage && clipspaceImage.src) {
log.info("Successfully got image from ComfyUI Clipspace");
const img = new Image();
img.onload = async () => {
await this.addLayerWithImage(img, {}, addMode);
};
img.src = clipspaceImage.src;
return true;
}
}
return false;
} catch (clipspaceError) {
log.warn("ComfyUI Clipspace paste failed:", clipspaceError);
return false;
}
}
async trySystemClipboardPaste(addMode) {
return await this.clipboardManager.trySystemClipboardPaste(addMode);
}
addLayerWithImage = withErrorHandling(async (image, layerProps = {}, addMode = 'default') => {
if (!image) {

View File

@@ -553,6 +553,7 @@ async function createCanvasWidget(node, widget, app) {
textContent: "Paste Image",
title: "Paste image from clipboard",
onclick: () => {
// Use the direct handlePaste method from CanvasLayers
const fitOnAddWidget = node.widgets.find(w => w.name === "fit_on_add");
const addMode = fitOnAddWidget && fitOnAddWidget.value ? 'fit' : 'center';
canvas.canvasLayers.handlePaste(addMode);
@@ -581,6 +582,77 @@ async function createCanvasWidget(node, widget, app) {
button.style.backgroundColor = "#4a4a4a";
}
log.info(`Clipboard preference toggled to: ${canvas.canvasLayers.clipboardPreference}`);
},
onmouseenter: (e) => {
const currentPreference = canvas.canvasLayers.clipboardPreference;
let tooltipContent = '';
if (currentPreference === 'system') {
tooltipContent = `
<h4>📋 System Clipboard Mode</h4>
<table>
<tr><td><kbd>Ctrl + C</kbd></td><td>Copy selected layers to internal clipboard + <strong>system clipboard</strong> as flattened image</td></tr>
<tr><td><kbd>Ctrl + V</kbd></td><td><strong>Priority:</strong></td></tr>
<tr><td></td><td>1⃣ Internal clipboard (copied layers)</td></tr>
<tr><td></td><td>2⃣ System clipboard (images, screenshots)</td></tr>
<tr><td></td><td>3⃣ System clipboard (file paths, URLs)</td></tr>
<tr><td><kbd>Paste Image</kbd></td><td>Same as Ctrl+V but respects fit_on_add setting</td></tr>
<tr><td><kbd>Drag & Drop</kbd></td><td>Load images directly from files</td></tr>
</table>
<div style="margin-top: 8px; padding: 6px; background: rgba(255,165,0,0.2); border: 1px solid rgba(255,165,0,0.4); border-radius: 4px; font-size: 11px;">
⚠️ <strong>Security Note:</strong> "Paste Image" button for external images may not work due to browser security restrictions. Use Ctrl+V instead or Drag & Drop.
</div>
<div style="margin-top: 8px; padding: 6px; background: rgba(0,0,0,0.2); border-radius: 4px; font-size: 11px;">
💡 <strong>Best for:</strong> Working with screenshots, copied images, file paths, and urls.
</div>
`;
} else {
tooltipContent = `
<h4>📋 ComfyUI Clipspace Mode</h4>
<table>
<tr><td><kbd>Ctrl + C</kbd></td><td>Copy selected layers to internal clipboard + <strong>ComfyUI Clipspace</strong> as flattened image</td></tr>
<tr><td><kbd>Ctrl + V</kbd></td><td><strong>Priority:</strong></td></tr>
<tr><td></td><td>1⃣ Internal clipboard (copied layers)</td></tr>
<tr><td></td><td>2⃣ ComfyUI Clipspace (workflow images)</td></tr>
<tr><td></td><td>3⃣ System clipboard (fallback)</td></tr>
<tr><td><kbd>Paste Image</kbd></td><td>Same as Ctrl+V but respects fit_on_add setting</td></tr>
<tr><td><kbd>Drag & Drop</kbd></td><td>Load images directly from files</td></tr>
</table>
<div style="margin-top: 8px; padding: 6px; background: rgba(0,0,0,0.2); border-radius: 4px; font-size: 11px;">
💡 <strong>Best for:</strong> ComfyUI workflow integration and node-to-node image transfer
</div>
`;
}
helpTooltip.innerHTML = tooltipContent;
helpTooltip.style.visibility = 'hidden';
helpTooltip.style.display = 'block';
const buttonRect = e.target.getBoundingClientRect();
const tooltipRect = helpTooltip.getBoundingClientRect();
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
let left = buttonRect.left;
let top = buttonRect.bottom + 5;
if (left + tooltipRect.width > viewportWidth) {
left = viewportWidth - tooltipRect.width - 10;
}
if (top + tooltipRect.height > viewportHeight) {
top = buttonRect.top - tooltipRect.height - 5;
}
if (left < 10) left = 10;
if (top < 10) top = 10;
helpTooltip.style.left = `${left}px`;
helpTooltip.style.top = `${top}px`;
helpTooltip.style.visibility = 'visible';
},
onmouseleave: () => {
helpTooltip.style.display = 'none';
}
}),
]),
@@ -989,53 +1061,8 @@ async function createCanvasWidget(node, widget, app) {
height: "100%"
}
}, [controlPanel, canvasContainer]);
const handleFileLoad = async (file) => {
log.info("File dropped:", file.name);
if (!file.type.startsWith('image/')) {
log.info("Dropped file is not an image.");
return;
}
const reader = new FileReader();
reader.onload = async (event) => {
log.debug("FileReader finished loading dropped file as data:URL.");
const img = new Image();
img.onload = async () => {
log.debug("Image object loaded from dropped data:URL.");
const fitOnAddWidget = node.widgets.find(w => w.name === "fit_on_add");
const addMode = fitOnAddWidget && fitOnAddWidget.value ? 'fit' : 'center';
await canvas.addLayer(img, {}, addMode);
log.info("Dropped layer added and state saved.");
};
img.src = event.target.result;
};
reader.readAsDataURL(file);
};
mainContainer.addEventListener('dragover', (e) => {
e.preventDefault();
e.stopPropagation();
canvasContainer.classList.add('drag-over');
});
mainContainer.addEventListener('dragleave', (e) => {
e.preventDefault();
e.stopPropagation();
canvasContainer.classList.remove('drag-over');
});
mainContainer.addEventListener('drop', async (e) => {
e.preventDefault();
e.stopPropagation();
canvasContainer.classList.remove('drag-over');
if (e.dataTransfer.files) {
for (const file of e.dataTransfer.files) {
await handleFileLoad(file);
}
}
});
// Drag & drop is now handled by CanvasInteractions.js
// Removed duplicate handlers to prevent double loading
const mainWidget = node.addDOMWidget("mainContainer", "widget", mainContainer);

View File

@@ -1,5 +1,6 @@
import {createModuleLogger} from "./LoggerUtils.js";
import {api} from "../../../scripts/api.js";
import {ComfyApp} from "../../../scripts/app.js";
const log = createModuleLogger('ClipboardManager');
@@ -10,68 +11,158 @@ export class ClipboardManager {
}
/**
* Attempts to paste from system clipboard
* Main paste handler that delegates to appropriate methods
* @param {string} addMode - The mode for adding the layer
* @param {string} preference - Clipboard preference ('system' or 'clipspace')
* @returns {Promise<boolean>} - True if successful, false otherwise
*/
async handlePaste(addMode = 'mouse', preference = 'system') {
try {
log.info(`ClipboardManager handling paste with preference: ${preference}`);
// PRIORITY 1: Check internal clipboard first (copied layers)
if (this.canvas.canvasLayers.internalClipboard.length > 0) {
log.info("Found layers in internal clipboard, pasting layers");
this.canvas.canvasLayers.pasteLayers();
return true;
}
// PRIORITY 2: Check external clipboard based on preference
if (preference === 'clipspace') {
log.info("Attempting paste from ComfyUI Clipspace");
const success = await this.tryClipspacePaste(addMode);
if (success) {
return true;
}
log.info("No image found in ComfyUI Clipspace");
}
// PRIORITY 3: Always try system clipboard (either as primary or fallback)
log.info("Attempting paste from system clipboard");
return await this.trySystemClipboardPaste(addMode);
} catch (err) {
log.error("ClipboardManager paste operation failed:", err);
return false;
}
}
/**
* Attempts to paste from ComfyUI Clipspace
* @param {string} addMode - The mode for adding the layer
* @returns {Promise<boolean>} - True if successful, false otherwise
*/
async tryClipspacePaste(addMode) {
try {
log.info("Attempting to paste from ComfyUI Clipspace");
const clipspaceResult = ComfyApp.pasteFromClipspace(this.canvas.node);
if (this.canvas.node.imgs && this.canvas.node.imgs.length > 0) {
const clipspaceImage = this.canvas.node.imgs[0];
if (clipspaceImage && clipspaceImage.src) {
log.info("Successfully got image from ComfyUI Clipspace");
const img = new Image();
img.onload = async () => {
await this.canvas.canvasLayers.addLayerWithImage(img, {}, addMode);
};
img.src = clipspaceImage.src;
return true;
}
}
return false;
} catch (clipspaceError) {
log.warn("ComfyUI Clipspace paste failed:", clipspaceError);
return false;
}
}
/**
* System clipboard paste - handles both image data and text paths
* @param {string} addMode - The mode for adding the layer
* @returns {Promise<boolean>} - True if successful, false otherwise
*/
async trySystemClipboardPaste(addMode) {
if (!navigator.clipboard?.read) {
log.info("Browser does not support clipboard read API");
return false;
}
try {
log.info("Attempting to paste from system clipboard");
const clipboardItems = await navigator.clipboard.read();
for (const item of clipboardItems) {
// First, try to find actual image data
const imageType = item.types.find(type => type.startsWith('image/'));
if (imageType) {
const blob = await item.getType(imageType);
const reader = new FileReader();
reader.onload = (event) => {
const img = new Image();
img.onload = async () => {
await this.canvas.canvasLayers.addLayerWithImage(img, {}, addMode);
};
img.src = event.target.result;
};
reader.readAsDataURL(blob);
log.info("Successfully pasted image from system clipboard");
return true;
}
// If no image data found, check for text that might be a file path
const textType = item.types.find(type => type === 'text/plain');
if (textType) {
const textBlob = await item.getType(textType);
const text = await textBlob.text();
log.info("ClipboardManager: Checking system clipboard for images and paths");
// First try modern clipboard API for both images and text
if (navigator.clipboard?.read) {
try {
const clipboardItems = await navigator.clipboard.read();
for (const item of clipboardItems) {
log.debug("Clipboard item types:", item.types);
if (this.isValidImagePath(text)) {
log.info("Found image file path in clipboard:", text);
// Check for image data first
const imageType = item.types.find(type => type.startsWith('image/'));
if (imageType) {
try {
// Try to load the image using different methods
const success = await this.loadImageFromPath(text, addMode);
if (success) {
return true;
const blob = await item.getType(imageType);
const reader = new FileReader();
reader.onload = (event) => {
const img = new Image();
img.onload = async () => {
log.info("Successfully loaded image from system clipboard");
await this.canvas.canvasLayers.addLayerWithImage(img, {}, addMode);
};
img.src = event.target.result;
};
reader.readAsDataURL(blob);
log.info("Found image data in system clipboard");
return true;
} catch (error) {
log.debug("Error reading image data:", error);
}
}
// Check for text types (file paths, URLs)
const textTypes = ['text/plain', 'text/uri-list'];
for (const textType of textTypes) {
if (item.types.includes(textType)) {
try {
const textBlob = await item.getType(textType);
const text = await textBlob.text();
if (this.isValidImagePath(text)) {
log.info("Found image path in clipboard:", text);
const success = await this.loadImageFromPath(text, addMode);
if (success) {
return true;
}
}
} catch (error) {
log.debug(`Error reading ${textType}:`, error);
}
} catch (pathError) {
log.warn("Error loading image from path:", pathError);
}
}
}
} catch (error) {
log.debug("Modern clipboard API failed:", error);
}
log.info("No image or valid image path found in system clipboard");
return false;
} catch (error) {
log.warn("System clipboard paste failed:", error);
return false;
}
// Fallback to text-only API
if (navigator.clipboard?.readText) {
try {
const text = await navigator.clipboard.readText();
log.debug("Found text in clipboard:", text);
if (text && this.isValidImagePath(text)) {
log.info("Found valid image path in clipboard:", text);
const success = await this.loadImageFromPath(text, addMode);
if (success) {
return true;
}
}
} catch (error) {
log.debug("Could not read text from clipboard:", error);
}
}
log.debug("No images or valid image paths found in system clipboard");
return false;
}
/**
* Validates if a text string is a valid image file path or URL
* @param {string} text - The text to validate
@@ -141,13 +232,13 @@ export class ClipboardManager {
}
/**
* Attempts to load an image from a file path using various methods
* Attempts to load an image from a file path using simplified methods
* @param {string} filePath - The file path to load
* @param {string} addMode - The mode for adding the layer
* @returns {Promise<boolean>} - True if successful, false otherwise
*/
async loadImageFromPath(filePath, addMode) {
// Method 1: Try direct loading for URLs
// Method 1: Direct loading for URLs
if (filePath.startsWith('http://') || filePath.startsWith('https://')) {
try {
const img = new Image();
@@ -170,143 +261,44 @@ export class ClipboardManager {
}
}
// Method 2: Try to load via ComfyUI's view endpoint for local files
// Method 2: Load local files via backend endpoint
try {
log.info("Attempting to load local file via ComfyUI view endpoint");
const success = await this.loadImageViaComfyUIView(filePath, addMode);
log.info("Attempting to load local file via backend");
const success = await this.loadFileViaBackend(filePath, addMode);
if (success) {
return true;
}
} catch (error) {
log.warn("ComfyUI view endpoint method failed:", error);
log.warn("Backend loading failed:", error);
}
// Method 3: Try to prompt user to select the file manually
// Method 3: Fallback to file picker
try {
log.info("Attempting to load local file via file picker");
log.info("Falling back to file picker");
const success = await this.promptUserForFile(filePath, addMode);
if (success) {
return true;
}
} catch (error) {
log.warn("File picker method failed:", error);
log.warn("File picker failed:", error);
}
// Method 4: Show user a helpful message about the limitation
// Method 4: Show user a helpful message
this.showFilePathMessage(filePath);
return false;
}
/**
* Attempts to load an image using ComfyUI's API methods
* Loads a local file via the ComfyUI backend endpoint
* @param {string} filePath - The file path to load
* @param {string} addMode - The mode for adding the layer
* @returns {Promise<boolean>} - True if successful, false otherwise
*/
async loadImageViaComfyUIView(filePath, addMode) {
async loadFileViaBackend(filePath, addMode) {
try {
// First, try to get folder paths to understand ComfyUI structure
const folderPaths = await this.getComfyUIFolderPaths();
log.debug("ComfyUI folder paths:", folderPaths);
log.info("Loading file via ComfyUI backend:", filePath);
// Extract filename from path
const fileName = filePath.split(/[\\\/]/).pop();
// Method 1: Try to upload the file to ComfyUI first, then load it
const uploadSuccess = await this.uploadFileToComfyUI(filePath, addMode);
if (uploadSuccess) {
return true;
}
// Method 2: Try different view endpoints if file might already exist in ComfyUI
const viewConfigs = [
// Direct filename approach
{ filename: fileName },
// Full path approach
{ filename: filePath },
// Input folder approach
{ filename: fileName, type: 'input' },
// Temp folder approach
{ filename: fileName, type: 'temp' },
// Output folder approach
{ filename: fileName, type: 'output' }
];
for (const config of viewConfigs) {
try {
// Build query parameters
const params = new URLSearchParams();
params.append('filename', config.filename);
if (config.type) {
params.append('type', config.type);
}
if (config.subfolder) {
params.append('subfolder', config.subfolder);
}
const viewUrl = api.apiURL(`/view?${params.toString()}`);
log.debug("Trying ComfyUI view URL:", viewUrl);
const img = new Image();
const success = await new Promise((resolve) => {
img.onload = async () => {
log.info("Successfully loaded image via ComfyUI view endpoint:", viewUrl);
await this.canvas.canvasLayers.addLayerWithImage(img, {}, addMode);
resolve(true);
};
img.onerror = () => {
log.debug("Failed to load image via ComfyUI view endpoint:", viewUrl);
resolve(false);
};
// Set a timeout to avoid hanging
setTimeout(() => {
resolve(false);
}, 3000);
img.src = viewUrl;
});
if (success) {
return true;
}
} catch (error) {
log.debug("Error with view config:", config, error);
continue;
}
}
return false;
} catch (error) {
log.warn("Error in loadImageViaComfyUIView:", error);
return false;
}
}
/**
* Gets ComfyUI folder paths using the API
* @returns {Promise<Object>} - Folder paths object
*/
async getComfyUIFolderPaths() {
try {
return await api.getFolderPaths();
} catch (error) {
log.warn("Failed to get ComfyUI folder paths:", error);
return {};
}
}
/**
* Attempts to load a file via ComfyUI backend endpoint
* @param {string} filePath - The file path to load
* @param {string} addMode - The mode for adding the layer
* @returns {Promise<boolean>} - True if successful, false otherwise
*/
async uploadFileToComfyUI(filePath, addMode) {
try {
log.info("Attempting to load file via ComfyUI backend:", filePath);
// Use the new backend endpoint to load image from path
// Use the backend endpoint to load image from path
const response = await api.fetchApi("/ycnode/load_image_from_path", {
method: "POST",
headers: {
@@ -435,6 +427,81 @@ export class ClipboardManager {
log.info("Showed file path limitation message to user");
}
/**
* Shows a helpful message when clipboard appears empty and offers file picker
* @param {string} addMode - The mode for adding the layer
*/
showEmptyClipboardMessage(addMode) {
const message = `Copied a file? Browser can't access file paths for security. Click here to select the file manually.`;
// Create clickable notification
const notification = document.createElement('div');
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: #2d5aa0;
color: white;
padding: 14px 18px;
border-radius: 6px;
box-shadow: 0 4px 12px rgba(0,0,0,0.4);
z-index: 10001;
max-width: 320px;
font-size: 14px;
line-height: 1.4;
cursor: pointer;
border: 2px solid #4a7bc8;
transition: all 0.2s ease;
font-weight: 500;
`;
notification.innerHTML = `
<div style="display: flex; align-items: center; gap: 8px;">
<span style="font-size: 18px;">📁</span>
<span>${message}</span>
</div>
<div style="font-size: 12px; opacity: 0.9; margin-top: 4px;">
💡 Tip: You can also drag & drop files directly onto the canvas
</div>
`;
// Add hover effect
notification.onmouseenter = () => {
notification.style.backgroundColor = '#3d6bb0';
notification.style.borderColor = '#5a8bd8';
notification.style.transform = 'translateY(-1px)';
};
notification.onmouseleave = () => {
notification.style.backgroundColor = '#2d5aa0';
notification.style.borderColor = '#4a7bc8';
notification.style.transform = 'translateY(0)';
};
// Add click handler to open file picker
notification.onclick = async () => {
document.body.removeChild(notification);
try {
const success = await this.promptUserForFile('image_file.jpg', addMode);
if (success) {
log.info("Successfully loaded image via empty clipboard file picker");
}
} catch (error) {
log.warn("Error with empty clipboard file picker:", error);
}
};
// Add to DOM
document.body.appendChild(notification);
// Auto-remove after longer duration
setTimeout(() => {
if (notification.parentNode) {
notification.parentNode.removeChild(notification);
}
}, 12000);
log.info("Showed enhanced empty clipboard message with file picker option");
}
/**
* Shows a temporary notification to the user
* @param {string} message - The message to show