Files
Comfyui-LayerForge/js/utils/ClipboardManager.js
Dariusz L 94ffc64f6e 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.
2025-07-01 07:57:05 +02:00

455 lines
17 KiB
JavaScript

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);
}
}