diff --git a/js/CanvasInteractions.js b/js/CanvasInteractions.js index 6379861..bd9b78f 100644 --- a/js/CanvasInteractions.js +++ b/js/CanvasInteractions.js @@ -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); + } } diff --git a/js/CanvasLayers.js b/js/CanvasLayers.js index b42ace2..7125691 100644 --- a/js/CanvasLayers.js +++ b/js/CanvasLayers.js @@ -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) { diff --git a/js/CanvasView.js b/js/CanvasView.js index ff16e62..2906431 100644 --- a/js/CanvasView.js +++ b/js/CanvasView.js @@ -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 = ` +

📋 System Clipboard Mode

+ + + + + + + + +
Ctrl + CCopy selected layers to internal clipboard + system clipboard as flattened image
Ctrl + VPriority:
1️⃣ Internal clipboard (copied layers)
2️⃣ System clipboard (images, screenshots)
3️⃣ System clipboard (file paths, URLs)
Paste ImageSame as Ctrl+V but respects fit_on_add setting
Drag & DropLoad images directly from files
+
+ ⚠️ Security Note: "Paste Image" button for external images may not work due to browser security restrictions. Use Ctrl+V instead or Drag & Drop. +
+
+ 💡 Best for: Working with screenshots, copied images, file paths, and urls. +
+ `; + } else { + tooltipContent = ` +

📋 ComfyUI Clipspace Mode

+ + + + + + + + +
Ctrl + CCopy selected layers to internal clipboard + ComfyUI Clipspace as flattened image
Ctrl + VPriority:
1️⃣ Internal clipboard (copied layers)
2️⃣ ComfyUI Clipspace (workflow images)
3️⃣ System clipboard (fallback)
Paste ImageSame as Ctrl+V but respects fit_on_add setting
Drag & DropLoad images directly from files
+
+ 💡 Best for: ComfyUI workflow integration and node-to-node image transfer +
+ `; + } + + 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); diff --git a/js/utils/ClipboardManager.js b/js/utils/ClipboardManager.js index 6dce54d..5021f80 100644 --- a/js/utils/ClipboardManager.js +++ b/js/utils/ClipboardManager.js @@ -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} - 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} - 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} - 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} - 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} - 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} - 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} - 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 = ` +
+ 📁 + ${message} +
+
+ 💡 Tip: You can also drag & drop files directly onto the canvas +
+ `; + + // 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