321 lines
10 KiB
JavaScript
321 lines
10 KiB
JavaScript
/**
|
|
* Image Operations Module
|
|
* Handles image loading, thumbnail generation, metadata, and clipboard operations
|
|
*/
|
|
|
|
import { API_ENDPOINTS } from "./config.js";
|
|
import { actions } from "./stateManager.js";
|
|
|
|
/**
|
|
* Load images from a specified folder
|
|
* @param {string} folderType - Type of folder ('notes', 'input', 'output', 'custom')
|
|
* @param {string|null} customPath - Custom path for browsing subfolders
|
|
* @returns {Promise<Object>} Object containing images and folders arrays
|
|
*/
|
|
export async function loadImagesFromFolder(folderType, customPath = null) {
|
|
try {
|
|
let url = API_ENDPOINTS.listImages;
|
|
const params = new URLSearchParams({ folder_type: folderType });
|
|
|
|
if (customPath) {
|
|
params.append('custom_path', customPath);
|
|
}
|
|
|
|
const response = await fetch(`${url}?${params}`);
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
}
|
|
|
|
const result = await response.json();
|
|
|
|
if (!result.success) {
|
|
throw new Error(result.error || 'Unknown error occurred');
|
|
}
|
|
|
|
const images = result.images || [];
|
|
const folders = result.folders || [];
|
|
|
|
// Update state with loaded images and folders
|
|
actions.setImages(images);
|
|
actions.setFolders(folders);
|
|
|
|
// Manage current path based on folder type and custom path
|
|
if (folderType === 'custom' && customPath) {
|
|
// We're in a custom subfolder, set the path
|
|
actions.setCurrentPath(customPath);
|
|
} else {
|
|
// We're in a standard folder (input, output, etc.) or no custom path
|
|
actions.setCurrentPath('');
|
|
}
|
|
|
|
return { images, folders };
|
|
|
|
} catch (error) {
|
|
console.error('Error loading images:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate a thumbnail for an image
|
|
* @param {Object} image - Image object with path and metadata
|
|
* @param {number} size - Thumbnail size (default: 200)
|
|
* @returns {Promise<string|null>} Thumbnail URL or null if failed
|
|
*/
|
|
export async function generateThumbnail(image, size = 200) {
|
|
try {
|
|
const params = new URLSearchParams({
|
|
image_path: image.path,
|
|
size: size.toString()
|
|
});
|
|
|
|
const response = await fetch(`${API_ENDPOINTS.getThumbnail}?${params}`);
|
|
|
|
if (response.ok) {
|
|
const blob = await response.blob();
|
|
const thumbnailUrl = URL.createObjectURL(blob);
|
|
return thumbnailUrl;
|
|
} else {
|
|
console.error('Thumbnail generation failed:', response.statusText);
|
|
return null;
|
|
}
|
|
} catch (error) {
|
|
console.error('Error generating thumbnail:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Copy an image to the clipboard
|
|
* @param {string} imagePath - Path to the image file
|
|
*/
|
|
export async function copyImageToClipboard(imagePath) {
|
|
try {
|
|
const response = await fetch('/sage_utils/copy_image', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ image_path: imagePath })
|
|
});
|
|
|
|
const result = await response.json();
|
|
if (result.success) {
|
|
// Show temporary success message
|
|
showTemporaryMessage('Image copied to clipboard!', 'success');
|
|
} else {
|
|
console.error('Copy failed:', result.error);
|
|
showTemporaryMessage(`Copy failed: ${result.error}`, 'error');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error copying image:', error);
|
|
showTemporaryMessage(`Error copying image: ${error.message}`, 'error');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Load image metadata
|
|
* @param {Object} image - Image object with path
|
|
* @returns {Promise<Object>} Metadata object
|
|
*/
|
|
export async function loadImageMetadata(image) {
|
|
try {
|
|
const response = await fetch('/sage_utils/image_metadata', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ image_path: image.path })
|
|
});
|
|
|
|
const result = await response.json();
|
|
if (result.success) {
|
|
return result.metadata || {};
|
|
} else {
|
|
console.error('Metadata loading failed:', result.error);
|
|
return {};
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading metadata:', error);
|
|
return {};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Load full-size image for viewing
|
|
* @param {string} imagePath - Path to the image
|
|
* @returns {Promise<string|null>} Image URL or null if failed
|
|
*/
|
|
export async function loadFullImage(imagePath) {
|
|
try {
|
|
const params = new URLSearchParams({ image_path: imagePath });
|
|
const response = await fetch(`/sage_utils/get_image?${params}`);
|
|
|
|
if (response.ok) {
|
|
const blob = await response.blob();
|
|
return URL.createObjectURL(blob);
|
|
} else {
|
|
console.error('Full image loading failed:', response.statusText);
|
|
return null;
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading full image:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Browse a custom folder
|
|
* @param {string} folderPath - Path to browse
|
|
* @returns {Promise<Object>} Object containing images and folders
|
|
*/
|
|
export async function browseFolder(folderPath) {
|
|
try {
|
|
const response = await fetch('/sage_utils/browse_folder', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ folder_path: folderPath })
|
|
});
|
|
|
|
const result = await response.json();
|
|
if (result.success) {
|
|
return {
|
|
images: result.images || [],
|
|
folders: result.folders || []
|
|
};
|
|
} else {
|
|
throw new Error(result.error || 'Failed to browse folder');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error browsing folder:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the parent path for navigation
|
|
* @param {string} currentPath - Current folder path
|
|
* @returns {string} Parent path
|
|
*/
|
|
export function getParentPath(currentPath) {
|
|
if (!currentPath || currentPath === '/') return '/';
|
|
const parts = currentPath.split('/').filter(part => part !== '');
|
|
if (parts.length <= 1) return '/';
|
|
return '/' + parts.slice(0, -1).join('/');
|
|
}
|
|
|
|
/**
|
|
* Show a temporary status message
|
|
* @param {string} message - Message to show
|
|
* @param {string} type - Message type ('success', 'error', 'info')
|
|
* @param {number} duration - Duration in milliseconds (default: 3000)
|
|
*/
|
|
export function showTemporaryMessage(message, type = 'info', duration = 3000) {
|
|
// Create or find status element
|
|
let statusElement = document.querySelector('.gallery-temp-status');
|
|
if (!statusElement) {
|
|
statusElement = document.createElement('div');
|
|
statusElement.className = 'gallery-temp-status';
|
|
statusElement.style.cssText = `
|
|
position: fixed;
|
|
top: 20px;
|
|
right: 20px;
|
|
padding: 10px 15px;
|
|
border-radius: 4px;
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
z-index: 9999;
|
|
max-width: 300px;
|
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
|
|
transition: opacity 0.3s ease;
|
|
`;
|
|
document.body.appendChild(statusElement);
|
|
}
|
|
|
|
// Set message and style based on type
|
|
statusElement.textContent = message;
|
|
|
|
const colors = {
|
|
success: { bg: '#4CAF50', text: '#fff' },
|
|
error: { bg: '#F44336', text: '#fff' },
|
|
info: { bg: '#2196F3', text: '#fff' },
|
|
warning: { bg: '#FF9800', text: '#fff' }
|
|
};
|
|
|
|
const color = colors[type] || colors.info;
|
|
statusElement.style.backgroundColor = color.bg;
|
|
statusElement.style.color = color.text;
|
|
statusElement.style.opacity = '1';
|
|
|
|
// Auto-hide after duration
|
|
clearTimeout(statusElement.hideTimeout);
|
|
statusElement.hideTimeout = setTimeout(() => {
|
|
statusElement.style.opacity = '0';
|
|
setTimeout(() => {
|
|
if (statusElement.parentNode) {
|
|
statusElement.parentNode.removeChild(statusElement);
|
|
}
|
|
}, 300);
|
|
}, duration);
|
|
}
|
|
|
|
/**
|
|
* Check if a file is a supported image format
|
|
* @param {string} filename - The filename to check
|
|
* @returns {boolean} True if the file is a supported image
|
|
*/
|
|
export function isSupportedImageFile(filename) {
|
|
if (!filename) return false;
|
|
const imageExtensions = /\.(jpg|jpeg|png|gif|bmp|webp|svg|tiff|tif|ico|heic|heif|avif)$/i;
|
|
return imageExtensions.test(filename);
|
|
}
|
|
|
|
/**
|
|
* Get file size in human readable format
|
|
* @param {number} bytes - File size in bytes
|
|
* @returns {string} Formatted file size
|
|
*/
|
|
export function formatFileSize(bytes) {
|
|
if (bytes === 0) return '0 Bytes';
|
|
|
|
const k = 1024;
|
|
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
}
|
|
|
|
/**
|
|
* Create an image element with loading states
|
|
* @param {string} src - Image source URL
|
|
* @param {string} alt - Alt text
|
|
* @param {Object} options - Additional options (className, style, etc.)
|
|
* @returns {HTMLImageElement} Image element
|
|
*/
|
|
export function createImageElement(src, alt = '', options = {}) {
|
|
const img = document.createElement('img');
|
|
img.src = src;
|
|
img.alt = alt;
|
|
|
|
if (options.className) {
|
|
img.className = options.className;
|
|
}
|
|
|
|
if (options.style) {
|
|
Object.assign(img.style, options.style);
|
|
}
|
|
|
|
// Add loading states
|
|
img.style.opacity = '0';
|
|
img.style.transition = 'opacity 0.3s ease';
|
|
|
|
img.addEventListener('load', () => {
|
|
img.style.opacity = '1';
|
|
});
|
|
|
|
img.addEventListener('error', () => {
|
|
img.style.opacity = '0.5';
|
|
img.style.filter = 'grayscale(100%)';
|
|
img.alt = 'Failed to load image';
|
|
});
|
|
|
|
return img;
|
|
}
|