mirror of
https://github.com/Azornes/Comfyui-LayerForge.git
synced 2026-03-25 06:22:14 -03:00
Refactor clipboard paste logic into ClipboardManager
Moved all system clipboard paste and image path handling logic from CanvasLayers.js into a new ClipboardManager utility class. This improves code organization and separation of concerns, making clipboard-related functionality easier to maintain and extend.
This commit is contained in:
@@ -3,13 +3,14 @@ import {createModuleLogger} from "./utils/LoggerUtils.js";
|
|||||||
import {generateUUID, generateUniqueFileName} from "./utils/CommonUtils.js";
|
import {generateUUID, generateUniqueFileName} from "./utils/CommonUtils.js";
|
||||||
import {withErrorHandling, createValidationError} from "./ErrorHandler.js";
|
import {withErrorHandling, createValidationError} from "./ErrorHandler.js";
|
||||||
import {app, ComfyApp} from "../../scripts/app.js";
|
import {app, ComfyApp} from "../../scripts/app.js";
|
||||||
import {api} from "../../scripts/api.js";
|
import {ClipboardManager} from "./utils/ClipboardManager.js";
|
||||||
|
|
||||||
const log = createModuleLogger('CanvasLayers');
|
const log = createModuleLogger('CanvasLayers');
|
||||||
|
|
||||||
export class CanvasLayers {
|
export class CanvasLayers {
|
||||||
constructor(canvas) {
|
constructor(canvas) {
|
||||||
this.canvas = canvas;
|
this.canvas = canvas;
|
||||||
|
this.clipboardManager = new ClipboardManager(canvas);
|
||||||
this.blendModes = [
|
this.blendModes = [
|
||||||
{name: 'normal', label: 'Normal'},
|
{name: 'normal', label: 'Normal'},
|
||||||
{name: 'multiply', label: 'Multiply'},
|
{name: 'multiply', label: 'Multiply'},
|
||||||
@@ -134,442 +135,9 @@ export class CanvasLayers {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async trySystemClipboardPaste(addMode) {
|
async trySystemClipboardPaste(addMode) {
|
||||||
if (!navigator.clipboard?.read) {
|
return await this.clipboardManager.trySystemClipboardPaste(addMode);
|
||||||
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.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();
|
|
||||||
|
|
||||||
if (this.isValidImagePath(text)) {
|
|
||||||
log.info("Found image file path in clipboard:", text);
|
|
||||||
try {
|
|
||||||
// Try to load the image using different methods
|
|
||||||
const success = await this.loadImageFromPath(text, addMode);
|
|
||||||
if (success) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} catch (pathError) {
|
|
||||||
log.warn("Error loading image from path:", pathError);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates if a text string is a valid image file path
|
|
||||||
* @param {string} text - The text to validate
|
|
||||||
* @returns {boolean} - True if the text appears to be a valid image file path
|
|
||||||
*/
|
|
||||||
isValidImagePath(text) {
|
|
||||||
if (!text || typeof text !== 'string') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trim whitespace
|
|
||||||
text = text.trim();
|
|
||||||
|
|
||||||
// Check if it's empty after trimming
|
|
||||||
if (!text) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Common image file extensions
|
|
||||||
const imageExtensions = [
|
|
||||||
'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp',
|
|
||||||
'.svg', '.tiff', '.tif', '.ico', '.avif'
|
|
||||||
];
|
|
||||||
|
|
||||||
// Check if the text ends with a valid image extension (case insensitive)
|
|
||||||
const hasImageExtension = imageExtensions.some(ext =>
|
|
||||||
text.toLowerCase().endsWith(ext)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!hasImageExtension) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Basic path validation - should look like a file path
|
|
||||||
// Accept both Windows and Unix style paths, and URLs
|
|
||||||
const pathPatterns = [
|
|
||||||
/^[a-zA-Z]:[\\\/]/, // Windows absolute path (C:\... or C:/...)
|
|
||||||
/^[\\\/]/, // Unix absolute path (/...)
|
|
||||||
/^\.{1,2}[\\\/]/, // Relative path (./... or ../...)
|
|
||||||
/^https?:\/\//, // HTTP/HTTPS URL
|
|
||||||
/^file:\/\//, // File URL
|
|
||||||
/^[^\\\/]*[\\\/]/ // Contains path separators
|
|
||||||
];
|
|
||||||
|
|
||||||
const isValidPath = pathPatterns.some(pattern => pattern.test(text)) ||
|
|
||||||
(!text.includes('/') && !text.includes('\\') && text.includes('.')); // Simple filename
|
|
||||||
|
|
||||||
return isValidPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attempts to load an image from a file path using various 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
|
|
||||||
if (filePath.startsWith('http://') || filePath.startsWith('https://')) {
|
|
||||||
try {
|
|
||||||
const img = new Image();
|
|
||||||
img.crossOrigin = 'anonymous';
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
img.onload = async () => {
|
|
||||||
log.info("Successfully loaded image from URL");
|
|
||||||
await this.addLayerWithImage(img, {}, addMode);
|
|
||||||
resolve(true);
|
|
||||||
};
|
|
||||||
img.onerror = () => {
|
|
||||||
log.warn("Failed to load image from URL:", filePath);
|
|
||||||
resolve(false);
|
|
||||||
};
|
|
||||||
img.src = filePath;
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
log.warn("Error loading image from URL:", error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Method 2: Try to load via ComfyUI's view endpoint for local files
|
|
||||||
try {
|
|
||||||
log.info("Attempting to load local file via ComfyUI view endpoint");
|
|
||||||
const success = await this.loadImageViaComfyUIView(filePath, addMode);
|
|
||||||
if (success) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
log.warn("ComfyUI view endpoint method failed:", error);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Method 3: Try to prompt user to select the file manually
|
|
||||||
try {
|
|
||||||
log.info("Attempting to load local file via file picker");
|
|
||||||
const success = await this.promptUserForFile(filePath, addMode);
|
|
||||||
if (success) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
log.warn("File picker method failed:", error);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Method 4: Show user a helpful message about the limitation
|
|
||||||
this.showFilePathMessage(filePath);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attempts to load an image using ComfyUI's API 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 loadImageViaComfyUIView(filePath, addMode) {
|
|
||||||
try {
|
|
||||||
// First, try to get folder paths to understand ComfyUI structure
|
|
||||||
const folderPaths = await this.getComfyUIFolderPaths();
|
|
||||||
log.debug("ComfyUI folder paths:", folderPaths);
|
|
||||||
|
|
||||||
// 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.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
|
|
||||||
const response = await api.fetchApi("/ycnode/load_image_from_path", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
file_path: filePath
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorData = await response.json();
|
|
||||||
log.debug("Backend failed to load image:", errorData.error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (!data.success) {
|
|
||||||
log.debug("Backend returned error:", data.error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info("Successfully loaded image via ComfyUI backend:", filePath);
|
|
||||||
|
|
||||||
// Create image from the returned base64 data
|
|
||||||
const img = new Image();
|
|
||||||
const success = await new Promise((resolve) => {
|
|
||||||
img.onload = async () => {
|
|
||||||
log.info("Successfully loaded image from backend response");
|
|
||||||
await this.addLayerWithImage(img, {}, addMode);
|
|
||||||
resolve(true);
|
|
||||||
};
|
|
||||||
img.onerror = () => {
|
|
||||||
log.warn("Failed to load image from backend response");
|
|
||||||
resolve(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
img.src = data.image_data;
|
|
||||||
});
|
|
||||||
|
|
||||||
return success;
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
log.debug("Error loading file via ComfyUI backend:", error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prompts the user to select a file when a local path is detected
|
|
||||||
* @param {string} originalPath - The original file path from clipboard
|
|
||||||
* @param {string} addMode - The mode for adding the layer
|
|
||||||
* @returns {Promise<boolean>} - True if successful, false otherwise
|
|
||||||
*/
|
|
||||||
async promptUserForFile(originalPath, addMode) {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
// Create a temporary file input
|
|
||||||
const fileInput = document.createElement('input');
|
|
||||||
fileInput.type = 'file';
|
|
||||||
fileInput.accept = 'image/*';
|
|
||||||
fileInput.style.display = 'none';
|
|
||||||
|
|
||||||
// Extract filename from path for user reference
|
|
||||||
const fileName = originalPath.split(/[\\\/]/).pop();
|
|
||||||
|
|
||||||
fileInput.onchange = async (event) => {
|
|
||||||
const file = event.target.files[0];
|
|
||||||
if (file && file.type.startsWith('image/')) {
|
|
||||||
try {
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = (e) => {
|
|
||||||
const img = new Image();
|
|
||||||
img.onload = async () => {
|
|
||||||
log.info("Successfully loaded image from file picker");
|
|
||||||
await this.addLayerWithImage(img, {}, addMode);
|
|
||||||
resolve(true);
|
|
||||||
};
|
|
||||||
img.onerror = () => {
|
|
||||||
log.warn("Failed to load selected image");
|
|
||||||
resolve(false);
|
|
||||||
};
|
|
||||||
img.src = e.target.result;
|
|
||||||
};
|
|
||||||
reader.onerror = () => {
|
|
||||||
log.warn("Failed to read selected file");
|
|
||||||
resolve(false);
|
|
||||||
};
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
} catch (error) {
|
|
||||||
log.warn("Error processing selected file:", error);
|
|
||||||
resolve(false);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log.warn("Selected file is not an image");
|
|
||||||
resolve(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clean up
|
|
||||||
document.body.removeChild(fileInput);
|
|
||||||
};
|
|
||||||
|
|
||||||
fileInput.oncancel = () => {
|
|
||||||
log.info("File selection cancelled by user");
|
|
||||||
document.body.removeChild(fileInput);
|
|
||||||
resolve(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Show a brief notification to the user
|
|
||||||
this.showNotification(`Detected image path: ${fileName}. Please select the file to load it.`, 3000);
|
|
||||||
|
|
||||||
// Add to DOM and trigger click
|
|
||||||
document.body.appendChild(fileInput);
|
|
||||||
fileInput.click();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shows a message to the user about file path limitations
|
|
||||||
* @param {string} filePath - The file path that couldn't be loaded
|
|
||||||
*/
|
|
||||||
showFilePathMessage(filePath) {
|
|
||||||
const fileName = filePath.split(/[\\\/]/).pop();
|
|
||||||
const message = `Cannot load local file directly due to browser security restrictions. File detected: ${fileName}`;
|
|
||||||
this.showNotification(message, 5000);
|
|
||||||
log.info("Showed file path limitation message to user");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shows a temporary notification to the user
|
|
||||||
* @param {string} message - The message to show
|
|
||||||
* @param {number} duration - Duration in milliseconds
|
|
||||||
*/
|
|
||||||
showNotification(message, duration = 3000) {
|
|
||||||
// Create notification element
|
|
||||||
const notification = document.createElement('div');
|
|
||||||
notification.style.cssText = `
|
|
||||||
position: fixed;
|
|
||||||
top: 20px;
|
|
||||||
right: 20px;
|
|
||||||
background: #333;
|
|
||||||
color: white;
|
|
||||||
padding: 12px 16px;
|
|
||||||
border-radius: 4px;
|
|
||||||
box-shadow: 0 2px 10px rgba(0,0,0,0.3);
|
|
||||||
z-index: 10001;
|
|
||||||
max-width: 300px;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.4;
|
|
||||||
`;
|
|
||||||
notification.textContent = message;
|
|
||||||
|
|
||||||
// Add to DOM
|
|
||||||
document.body.appendChild(notification);
|
|
||||||
|
|
||||||
// Remove after duration
|
|
||||||
setTimeout(() => {
|
|
||||||
if (notification.parentNode) {
|
|
||||||
notification.parentNode.removeChild(notification);
|
|
||||||
}
|
|
||||||
}, duration);
|
|
||||||
}
|
|
||||||
|
|
||||||
addLayerWithImage = withErrorHandling(async (image, layerProps = {}, addMode = 'default') => {
|
addLayerWithImage = withErrorHandling(async (image, layerProps = {}, addMode = 'default') => {
|
||||||
if (!image) {
|
if (!image) {
|
||||||
|
|||||||
454
js/utils/ClipboardManager.js
Normal file
454
js/utils/ClipboardManager.js
Normal file
@@ -0,0 +1,454 @@
|
|||||||
|
import {createModuleLogger} from "./LoggerUtils.js";
|
||||||
|
import {api} from "../../../scripts/api.js";
|
||||||
|
|
||||||
|
const log = createModuleLogger('ClipboardManager');
|
||||||
|
|
||||||
|
export class ClipboardManager {
|
||||||
|
constructor(canvas) {
|
||||||
|
this.canvas = canvas;
|
||||||
|
this.clipboardPreference = 'system'; // 'system', 'clipspace'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to paste from system clipboard
|
||||||
|
* @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();
|
||||||
|
|
||||||
|
if (this.isValidImagePath(text)) {
|
||||||
|
log.info("Found image file path in clipboard:", text);
|
||||||
|
try {
|
||||||
|
// Try to load the image using different methods
|
||||||
|
const success = await this.loadImageFromPath(text, addMode);
|
||||||
|
if (success) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (pathError) {
|
||||||
|
log.warn("Error loading image from path:", pathError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates if a text string is a valid image file path
|
||||||
|
* @param {string} text - The text to validate
|
||||||
|
* @returns {boolean} - True if the text appears to be a valid image file path
|
||||||
|
*/
|
||||||
|
isValidImagePath(text) {
|
||||||
|
if (!text || typeof text !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trim whitespace
|
||||||
|
text = text.trim();
|
||||||
|
|
||||||
|
// Check if it's empty after trimming
|
||||||
|
if (!text) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Common image file extensions
|
||||||
|
const imageExtensions = [
|
||||||
|
'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp',
|
||||||
|
'.svg', '.tiff', '.tif', '.ico', '.avif'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Check if the text ends with a valid image extension (case insensitive)
|
||||||
|
const hasImageExtension = imageExtensions.some(ext =>
|
||||||
|
text.toLowerCase().endsWith(ext)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!hasImageExtension) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Basic path validation - should look like a file path
|
||||||
|
// Accept both Windows and Unix style paths, and URLs
|
||||||
|
const pathPatterns = [
|
||||||
|
/^[a-zA-Z]:[\\\/]/, // Windows absolute path (C:\... or C:/...)
|
||||||
|
/^[\\\/]/, // Unix absolute path (/...)
|
||||||
|
/^\.{1,2}[\\\/]/, // Relative path (./... or ../...)
|
||||||
|
/^https?:\/\//, // HTTP/HTTPS URL
|
||||||
|
/^file:\/\//, // File URL
|
||||||
|
/^[^\\\/]*[\\\/]/ // Contains path separators
|
||||||
|
];
|
||||||
|
|
||||||
|
const isValidPath = pathPatterns.some(pattern => pattern.test(text)) ||
|
||||||
|
(!text.includes('/') && !text.includes('\\') && text.includes('.')); // Simple filename
|
||||||
|
|
||||||
|
return isValidPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to load an image from a file path using various 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
|
||||||
|
if (filePath.startsWith('http://') || filePath.startsWith('https://')) {
|
||||||
|
try {
|
||||||
|
const img = new Image();
|
||||||
|
img.crossOrigin = 'anonymous';
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
img.onload = async () => {
|
||||||
|
log.info("Successfully loaded image from URL");
|
||||||
|
await this.canvas.canvasLayers.addLayerWithImage(img, {}, addMode);
|
||||||
|
resolve(true);
|
||||||
|
};
|
||||||
|
img.onerror = () => {
|
||||||
|
log.warn("Failed to load image from URL:", filePath);
|
||||||
|
resolve(false);
|
||||||
|
};
|
||||||
|
img.src = filePath;
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
log.warn("Error loading image from URL:", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method 2: Try to load via ComfyUI's view endpoint for local files
|
||||||
|
try {
|
||||||
|
log.info("Attempting to load local file via ComfyUI view endpoint");
|
||||||
|
const success = await this.loadImageViaComfyUIView(filePath, addMode);
|
||||||
|
if (success) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
log.warn("ComfyUI view endpoint method failed:", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method 3: Try to prompt user to select the file manually
|
||||||
|
try {
|
||||||
|
log.info("Attempting to load local file via file picker");
|
||||||
|
const success = await this.promptUserForFile(filePath, addMode);
|
||||||
|
if (success) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
log.warn("File picker method failed:", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method 4: Show user a helpful message about the limitation
|
||||||
|
this.showFilePathMessage(filePath);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to load an image using ComfyUI's API 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 loadImageViaComfyUIView(filePath, addMode) {
|
||||||
|
try {
|
||||||
|
// First, try to get folder paths to understand ComfyUI structure
|
||||||
|
const folderPaths = await this.getComfyUIFolderPaths();
|
||||||
|
log.debug("ComfyUI folder paths:", folderPaths);
|
||||||
|
|
||||||
|
// 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
|
||||||
|
const response = await api.fetchApi("/ycnode/load_image_from_path", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
file_path: filePath
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json();
|
||||||
|
log.debug("Backend failed to load image:", errorData.error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!data.success) {
|
||||||
|
log.debug("Backend returned error:", data.error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("Successfully loaded image via ComfyUI backend:", filePath);
|
||||||
|
|
||||||
|
// Create image from the returned base64 data
|
||||||
|
const img = new Image();
|
||||||
|
const success = await new Promise((resolve) => {
|
||||||
|
img.onload = async () => {
|
||||||
|
log.info("Successfully loaded image from backend response");
|
||||||
|
await this.canvas.canvasLayers.addLayerWithImage(img, {}, addMode);
|
||||||
|
resolve(true);
|
||||||
|
};
|
||||||
|
img.onerror = () => {
|
||||||
|
log.warn("Failed to load image from backend response");
|
||||||
|
resolve(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
img.src = data.image_data;
|
||||||
|
});
|
||||||
|
|
||||||
|
return success;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
log.debug("Error loading file via ComfyUI backend:", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prompts the user to select a file when a local path is detected
|
||||||
|
* @param {string} originalPath - The original file path from clipboard
|
||||||
|
* @param {string} addMode - The mode for adding the layer
|
||||||
|
* @returns {Promise<boolean>} - True if successful, false otherwise
|
||||||
|
*/
|
||||||
|
async promptUserForFile(originalPath, addMode) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
// Create a temporary file input
|
||||||
|
const fileInput = document.createElement('input');
|
||||||
|
fileInput.type = 'file';
|
||||||
|
fileInput.accept = 'image/*';
|
||||||
|
fileInput.style.display = 'none';
|
||||||
|
|
||||||
|
// Extract filename from path for user reference
|
||||||
|
const fileName = originalPath.split(/[\\\/]/).pop();
|
||||||
|
|
||||||
|
fileInput.onchange = async (event) => {
|
||||||
|
const file = event.target.files[0];
|
||||||
|
if (file && file.type.startsWith('image/')) {
|
||||||
|
try {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = async () => {
|
||||||
|
log.info("Successfully loaded image from file picker");
|
||||||
|
await this.canvas.canvasLayers.addLayerWithImage(img, {}, addMode);
|
||||||
|
resolve(true);
|
||||||
|
};
|
||||||
|
img.onerror = () => {
|
||||||
|
log.warn("Failed to load selected image");
|
||||||
|
resolve(false);
|
||||||
|
};
|
||||||
|
img.src = e.target.result;
|
||||||
|
};
|
||||||
|
reader.onerror = () => {
|
||||||
|
log.warn("Failed to read selected file");
|
||||||
|
resolve(false);
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
} catch (error) {
|
||||||
|
log.warn("Error processing selected file:", error);
|
||||||
|
resolve(false);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.warn("Selected file is not an image");
|
||||||
|
resolve(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
document.body.removeChild(fileInput);
|
||||||
|
};
|
||||||
|
|
||||||
|
fileInput.oncancel = () => {
|
||||||
|
log.info("File selection cancelled by user");
|
||||||
|
document.body.removeChild(fileInput);
|
||||||
|
resolve(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Show a brief notification to the user
|
||||||
|
this.showNotification(`Detected image path: ${fileName}. Please select the file to load it.`, 3000);
|
||||||
|
|
||||||
|
// Add to DOM and trigger click
|
||||||
|
document.body.appendChild(fileInput);
|
||||||
|
fileInput.click();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows a message to the user about file path limitations
|
||||||
|
* @param {string} filePath - The file path that couldn't be loaded
|
||||||
|
*/
|
||||||
|
showFilePathMessage(filePath) {
|
||||||
|
const fileName = filePath.split(/[\\\/]/).pop();
|
||||||
|
const message = `Cannot load local file directly due to browser security restrictions. File detected: ${fileName}`;
|
||||||
|
this.showNotification(message, 5000);
|
||||||
|
log.info("Showed file path limitation message to user");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows a temporary notification to the user
|
||||||
|
* @param {string} message - The message to show
|
||||||
|
* @param {number} duration - Duration in milliseconds
|
||||||
|
*/
|
||||||
|
showNotification(message, duration = 3000) {
|
||||||
|
// Create notification element
|
||||||
|
const notification = document.createElement('div');
|
||||||
|
notification.style.cssText = `
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
background: #333;
|
||||||
|
color: white;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.3);
|
||||||
|
z-index: 10001;
|
||||||
|
max-width: 300px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.4;
|
||||||
|
`;
|
||||||
|
notification.textContent = message;
|
||||||
|
|
||||||
|
// Add to DOM
|
||||||
|
document.body.appendChild(notification);
|
||||||
|
|
||||||
|
// Remove after duration
|
||||||
|
setTimeout(() => {
|
||||||
|
if (notification.parentNode) {
|
||||||
|
notification.parentNode.removeChild(notification);
|
||||||
|
}
|
||||||
|
}, duration);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user