Files
arcum42-ComfyUI_SageUtils/js/shared/galleryGrid.js
T
2026-07-18 08:39:56 -07:00

373 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Gallery Grid Components
* UI components for creating thumbnail items, folder items, and grid navigation
* Extracted from imageGalleryTab.js for better organization and reusability
*/
import { getThumbnailSize } from "./config.js";
import { formatFileSize } from "../reports/reportGenerator.js";
import { actions, selectors } from "./stateManager.js";
import { copyImageToClipboard } from "./imageUtils.js";
import { getLazyLoader } from "./lazyImageLoader.js";
import { showFullImage } from "./imageViewer.js";
/**
* Creates a thumbnail item for an image with hover effects and click handlers
* @param {Object} image - Image object with path, filename, size, dimensions
* @param {Function} showImageContextMenu - Callback to show context menu
* @param {Function} showMetadata - Callback to show image metadata in the current UI
* @returns {HTMLElement} Thumbnail item element
*/
export function createImageItem(image, showImageContextMenu, showMetadata) {
const item = document.createElement('div');
item.className = 'gallery-image-item';
item.style.cssText = `
position: relative;
background: #444;
border-radius: 6px;
overflow: hidden;
cursor: pointer;
transition: transform 0.2s ease, box-shadow 0.2s ease;
border: 2px solid transparent;
`;
// Add hover effects
item.addEventListener('mouseenter', () => {
item.style.transform = 'scale(1.05)';
item.style.boxShadow = '0 4px 12px rgba(76, 175, 80, 0.3)';
item.style.borderColor = '#4CAF50';
});
item.addEventListener('mouseleave', () => {
item.style.transform = 'scale(1)';
item.style.boxShadow = 'none';
item.style.borderColor = 'transparent';
});
// Create thumbnail image
const thumbnail = document.createElement('img');
thumbnail.style.cssText = `
width: 100%;
height: 150px;
object-fit: cover;
background: #333;
`;
// Set thumbnail source with lazy loader and error handling
const thumbnailSizeConfig = getThumbnailSize(selectors.thumbnailSize());
const sizeName = thumbnailSizeConfig.width <= 120 ? 'small' :
thumbnailSizeConfig.width <= 200 ? 'medium' : 'large';
const errorDiv = document.createElement('div');
errorDiv.style.cssText = `
width: 100%;
height: 150px;
display: flex;
align-items: center;
justify-content: center;
background: #333;
color: #999;
font-size: 12px;
position: absolute;
inset: 0;
`;
errorDiv.textContent = 'Thumbnail failed';
errorDiv.style.display = 'none';
item.appendChild(errorDiv);
thumbnail.addEventListener('error', () => {
thumbnail.style.display = 'none';
errorDiv.style.display = 'flex';
});
const lazyLoader = getLazyLoader();
lazyLoader.observe(thumbnail, image, sizeName);
// Create overlay with filename and info
const overlay = document.createElement('div');
overlay.style.cssText = `
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: linear-gradient(transparent, rgba(0,0,0,0.8));
color: white;
padding: 8px;
font-size: 11px;
opacity: 0;
transition: opacity 0.2s ease;
`;
const filename = document.createElement('div');
filename.style.cssText = `
font-weight: bold;
margin-bottom: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
`;
filename.textContent = image.filename;
const info = document.createElement('div');
info.style.cssText = `
font-size: 10px;
color: #ccc;
display: flex;
justify-content: space-between;
gap: 8px;
align-items: center;
`;
let infoText = '';
if (image.dimensions) {
infoText += `${image.dimensions.width}×${image.dimensions.height}`;
}
if (image.size) {
if (infoText) infoText += ' • ';
infoText += formatFileSize(image.size);
}
info.textContent = infoText;
const actionsRow = document.createElement('div');
actionsRow.className = 'gallery-item-actions';
const createActionButton = (text, title, onClick, disabled = false) => {
const btn = document.createElement('button');
btn.type = 'button';
btn.textContent = text;
btn.title = title;
btn.className = 'gallery-item-action-button';
if (disabled) {
btn.disabled = true;
btn.classList.add('gallery-item-action-button-disabled');
}
btn.addEventListener('click', (e) => {
e.stopPropagation();
onClick(e);
});
return btn;
};
const fullSizeButton = createActionButton('👁️', 'View full size', () => showFullImage(image, { showMetadata }));
const metadataButton = createActionButton('ℹ️', 'Show metadata', () => {
if (typeof showMetadata === 'function') {
showMetadata(image);
}
}, typeof showMetadata !== 'function');
const copyButton = createActionButton('📋', 'Copy image path', () => copyImageToClipboard(image.path || image.relative_path || image.name));
actionsRow.appendChild(fullSizeButton);
actionsRow.appendChild(metadataButton);
actionsRow.appendChild(copyButton);
const hintRow = document.createElement('div');
hintRow.className = 'gallery-item-hint';
hintRow.textContent = 'Shift+click copies path';
overlay.appendChild(filename);
overlay.appendChild(info);
overlay.appendChild(actionsRow);
overlay.appendChild(hintRow);
// Show overlay on hover
item.addEventListener('mouseenter', () => {
overlay.style.opacity = '1';
});
item.addEventListener('mouseleave', () => {
overlay.style.opacity = '0';
});
// Click handlers
item.addEventListener('click', (e) => {
if (e.shiftKey) {
// Copy to clipboard on Shift+click
copyImageToClipboard(image.path || image.relative_path || image.name);
} else {
// Show full image on regular click
showFullImage(image, { showMetadata });
}
});
// Right-click context menu
item.addEventListener('contextmenu', (e) => {
e.preventDefault();
showImageContextMenu(e, image);
});
item.appendChild(thumbnail);
item.appendChild(overlay);
return item;
}
/**
* Creates a folder navigation item with click handler for folder browsing
* @param {Object} folder - Folder object with name and path
* @param {Function} loadImagesFromFolder - Callback to load images from folder
* @returns {HTMLElement} Folder item element
*/
export function createFolderItem(folder, loadImagesFromFolder = null) {
const item = document.createElement('div');
item.className = 'gallery-folder-item';
item.style.cssText = `
position: relative;
background: #333;
border-radius: 6px;
overflow: hidden;
cursor: pointer;
transition: transform 0.2s ease, box-shadow 0.2s ease;
border: 2px solid transparent;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 150px;
`;
// Add hover effects
item.addEventListener('mouseenter', () => {
item.style.transform = 'scale(1.05)';
item.style.boxShadow = '0 4px 12px rgba(76, 175, 80, 0.3)';
item.style.borderColor = '#4CAF50';
});
item.addEventListener('mouseleave', () => {
item.style.transform = 'scale(1)';
item.style.boxShadow = 'none';
item.style.borderColor = 'transparent';
});
// Folder icon
const folderIcon = document.createElement('div');
folderIcon.style.cssText = `
font-size: 48px;
color: #4CAF50;
margin-bottom: 10px;
`;
folderIcon.textContent = '📁';
// Folder name
const folderName = document.createElement('div');
folderName.style.cssText = `
color: #fff;
font-size: 12px;
text-align: center;
padding: 0 8px;
word-break: break-word;
line-height: 1.2;
`;
folderName.textContent = folder.name;
// Click handler for folder navigation
item.addEventListener('click', () => {
// Load images from the clicked folder
actions.setCurrentPath(folder.path);
if (loadImagesFromFolder) {
loadImagesFromFolder('custom', folder.path);
} else {
console.error('loadImagesFromFolder callback not provided for folder navigation');
}
});
item.appendChild(folderIcon);
item.appendChild(folderName);
return item;
}
/**
* Creates a back navigation item for returning to parent directory
* @param {Function} loadImagesFromFolder - Callback to load images from folder
* @returns {HTMLElement} Back navigation item element
*/
export function createBackNavigationItem(loadImagesFromFolder = null) {
const item = document.createElement('div');
item.className = 'gallery-back-item';
item.style.cssText = `
position: relative;
background: #2a2a2a;
border-radius: 6px;
overflow: hidden;
cursor: pointer;
transition: transform 0.2s ease, box-shadow 0.2s ease;
border: 2px solid #555;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 150px;
`;
// Add hover effects
item.addEventListener('mouseenter', () => {
item.style.transform = 'scale(1.05)';
item.style.boxShadow = '0 4px 12px rgba(255, 193, 7, 0.3)';
item.style.borderColor = '#FFC107';
});
item.addEventListener('mouseleave', () => {
item.style.transform = 'scale(1)';
item.style.boxShadow = 'none';
item.style.borderColor = '#555';
});
// Back icon
const backIcon = document.createElement('div');
backIcon.style.cssText = `
font-size: 48px;
color: #FFC107;
margin-bottom: 10px;
`;
backIcon.textContent = '⬆️';
// Back label
const backLabel = document.createElement('div');
backLabel.style.cssText = `
color: #FFC107;
font-size: 12px;
text-align: center;
font-weight: bold;
`;
backLabel.textContent = '.. (Back)';
// Click handler for back navigation
item.addEventListener('click', () => {
const currentPath = selectors.currentPath();
if (currentPath && loadImagesFromFolder) {
// Navigate to parent directory
const pathParts = currentPath.split('/').filter(part => part.length > 0);
// Get the selected folder type to determine the base path structure
const selectedFolderType = selectors.selectedFolder();
// Check if we're going back to the root folder
// For standard folders like 'input', 'output', the structure is typically:
// /path/to/comfyui/input/subfolder, so if we remove one level and we're at the base input path,
// we should return to the root folder type
const parentPathParts = pathParts.slice(0, -1);
const parentPath = '/' + parentPathParts.join('/');
// If the parent path ends with the selected folder type (like '/input'),
// then we're going back to the root of that folder type
const isBackToRoot = parentPathParts.length > 0 &&
parentPathParts[parentPathParts.length - 1] === selectedFolderType;
if (isBackToRoot) {
// Back to root folder - load the selected folder type (input, output, etc.)
loadImagesFromFolder(selectedFolderType);
} else {
// Navigate to parent subfolder
loadImagesFromFolder('custom', parentPath);
}
} else {
console.error('loadImagesFromFolder callback not provided for back navigation');
}
});
item.appendChild(backIcon);
item.appendChild(backLabel);
return item;
}