Files
arcum42-ComfyUI_SageUtils/js/gallery/galleryLayout.js
T

531 lines
19 KiB
JavaScript

/**
* Gallery Layout Components
* Pure UI layout creation functions for the image gallery tab
* Extracted from imageGalleryTab.js for better organization and maintainability
*/
// Import required dependencies
import { createHeader } from "../components/cacheUI.js";
import { createThumbnailGrid } from "./gallery.js";
import { selectors } from "../shared/stateManager.js";
import {
loadHtmlTemplate,
createElementFromTemplate,
renderHtmlTemplate
} from "../utils/htmlTemplateLoader.js";
let galleryGridHeaderTemplate = null;
let galleryEmptyStateTemplate = null;
let galleryMetadataHeaderTemplate = null;
let galleryMetadataEmptyTemplate = null;
async function getGalleryGridHeaderTemplate() {
if (!galleryGridHeaderTemplate) {
galleryGridHeaderTemplate = await loadHtmlTemplate('extensions/comfyui_sageutils/gallery/partials/galleryGridHeaderContent.html');
}
return galleryGridHeaderTemplate;
}
async function getGalleryEmptyStateTemplate() {
if (!galleryEmptyStateTemplate) {
galleryEmptyStateTemplate = await loadHtmlTemplate('extensions/comfyui_sageutils/gallery/partials/galleryEmptyState.html');
}
return galleryEmptyStateTemplate;
}
async function getGalleryMetadataHeaderTemplate() {
if (!galleryMetadataHeaderTemplate) {
galleryMetadataHeaderTemplate = await loadHtmlTemplate('extensions/comfyui_sageutils/gallery/partials/galleryMetadataHeader.html');
}
return galleryMetadataHeaderTemplate;
}
async function getGalleryMetadataEmptyTemplate() {
if (!galleryMetadataEmptyTemplate) {
galleryMetadataEmptyTemplate = await loadHtmlTemplate('extensions/comfyui_sageutils/gallery/partials/galleryMetadataEmpty.html');
}
return galleryMetadataEmptyTemplate;
}
async function renderGalleryGridHeader() {
const template = await getGalleryGridHeaderTemplate();
return createElementFromTemplate(renderHtmlTemplate(template, {}));
}
async function renderGalleryEmptyState() {
const template = await getGalleryEmptyStateTemplate();
return createElementFromTemplate(renderHtmlTemplate(template, {}));
}
async function renderGalleryMetadataHeader() {
const template = await getGalleryMetadataHeaderTemplate();
return createElementFromTemplate(renderHtmlTemplate(template, {}));
}
async function renderGalleryMetadataEmpty() {
const template = await getGalleryMetadataEmptyTemplate();
return createElementFromTemplate(renderHtmlTemplate(template, {}));
}
/**
* Helper function to get folder favorites from localStorage
* @returns {Array} Array of favorite folder paths
*/
function getFolderFavorites() {
try {
const stored = localStorage.getItem('sageutils_folder_favorites');
if (stored) {
const favorites = JSON.parse(stored);
return Array.isArray(favorites) ? favorites : [];
}
} catch (error) {
console.error('Error loading folder favorites:', error);
}
return [];
}
/**
* Helper function to get a display name for a custom folder path
* @param {string} path - Full folder path
* @returns {string} Display name for the folder
*/
function getCustomFolderDisplayName(path) {
// Handle null, undefined, or non-string values
if (!path || typeof path !== 'string') return 'Custom Folder';
// Get the last part of the path as the folder name
const parts = path.split('/').filter(p => p.length > 0);
const folderName = parts[parts.length - 1] || 'root';
// Truncate path if too long, showing start and end
if (path.length > 50) {
const start = path.substring(0, 20);
const end = path.substring(path.length - 25);
return `📁 ${folderName} (${start}...${end})`;
}
return `📁 ${folderName} (${path})`;
}
/**
* Populates the folder dropdown with default folders, favorites, and browse option
* @param {HTMLSelectElement} dropdown - The dropdown element to populate
* @param {string|null} currentCustomPath - Current custom folder path if any
*/
export function populateFolderDropdown(dropdown, currentCustomPath = null) {
// Clear existing options
dropdown.innerHTML = '';
// Default folder options
const defaultOptions = [
{ value: 'notes', text: '📝 Notes Folder' },
{ value: 'input', text: '📥 Input Folder' },
{ value: 'output', text: '📤 Output Folder' }
];
defaultOptions.forEach(option => {
const opt = document.createElement('option');
opt.value = option.value;
opt.textContent = option.text;
dropdown.appendChild(opt);
});
// Add separator if we have custom folders
const favorites = getFolderFavorites();
// Ensure currentCustomPath is a string or null
const validCustomPath = (currentCustomPath && typeof currentCustomPath === 'string') ? currentCustomPath : null;
const hasCustomFolders = favorites.length > 0 || validCustomPath;
if (hasCustomFolders) {
const separator = document.createElement('option');
separator.disabled = true;
separator.textContent = '──────────';
dropdown.appendChild(separator);
}
// Add current custom path if it exists and isn't in favorites
if (validCustomPath && !favorites.includes(validCustomPath)) {
const opt = document.createElement('option');
opt.value = `custom:${validCustomPath}`;
opt.textContent = getCustomFolderDisplayName(validCustomPath);
opt.dataset.customPath = validCustomPath;
dropdown.appendChild(opt);
}
// Add favorite folders - filter out invalid entries
favorites.filter(favPath => favPath && typeof favPath === 'string').forEach(favPath => {
const opt = document.createElement('option');
opt.value = `custom:${favPath}`;
opt.textContent = getCustomFolderDisplayName(favPath);
opt.dataset.customPath = favPath;
dropdown.appendChild(opt);
});
// Add separator before browse option
const separator2 = document.createElement('option');
separator2.disabled = true;
separator2.textContent = '──────────';
dropdown.appendChild(separator2);
// Add browse option at the end
const browseOpt = document.createElement('option');
browseOpt.value = 'browse';
browseOpt.textContent = '📁 Browse Custom Folder...';
dropdown.appendChild(browseOpt);
}
/**
* Creates the Gallery tab header section with status display
* @returns {HTMLElement} Header element with status display
*/
export function createGalleryHeader() {
const header = createHeader('Gallery', null); // No description for compactness
// Add status display area
const statusDiv = document.createElement('div');
statusDiv.className = 'gallery-status';
statusDiv.textContent = '';
header.appendChild(statusDiv);
header.statusDiv = statusDiv; // Store reference for easy access
return header;
}
/**
* Creates the combined folder selector and controls section for the Gallery tab
* @returns {Object} Folder selector and controls components
*/
export function createFolderSelectorAndControls() {
const folderSection = document.createElement('div');
folderSection.className = 'gallery-folder-section';
const folderHeader = document.createElement('h4');
folderHeader.className = 'gallery-folder-header';
folderHeader.textContent = 'Folder & Controls';
const folderDropdown = document.createElement('select');
folderDropdown.id = 'gallery-folder-selector';
folderDropdown.className = 'gallery-folder-dropdown';
// Populate dropdown with current state
const currentPath = selectors.currentPath();
populateFolderDropdown(folderDropdown, currentPath);
// Set default selection to notes
folderDropdown.value = 'notes';
folderSection.appendChild(folderHeader);
folderSection.appendChild(folderDropdown);
// Add controls to the same section
// Sort controls
const sortContainer = document.createElement('div');
sortContainer.className = 'gallery-sort-container';
const sortLabel = document.createElement('label');
sortLabel.textContent = 'Sort:';
sortLabel.className = 'gallery-sort-label';
const sortSelect = document.createElement('select');
sortSelect.id = 'gallery-sort';
sortSelect.className = 'gallery-sort-select';
const sortOptions = [
{ value: 'name', text: 'Name' },
{ value: 'date', text: 'Date Modified' },
{ value: 'size', text: 'File Size' },
{ value: 'type', text: 'File Type' }
];
sortOptions.forEach(option => {
const optionElement = document.createElement('option');
optionElement.value = option.value;
optionElement.textContent = option.text;
sortSelect.appendChild(optionElement);
});
const orderButton = document.createElement('button');
orderButton.textContent = '↑';
orderButton.title = 'Toggle sort order';
orderButton.className = 'gallery-button gallery-button-secondary gallery-order-button';
sortContainer.appendChild(sortLabel);
sortContainer.appendChild(sortSelect);
sortContainer.appendChild(orderButton);
// Thumbnail size and control buttons
const buttonsContainer = document.createElement('div');
buttonsContainer.className = 'gallery-buttons-container';
const thumbnailSizeSelect = document.createElement('select');
thumbnailSizeSelect.id = 'gallery-thumbnail-size';
thumbnailSizeSelect.className = 'gallery-thumbnail-size-select';
const sizeOptions = [
{ value: 'small', text: 'Small' },
{ value: 'medium', text: 'Medium' },
{ value: 'large', text: 'Large' }
];
sizeOptions.forEach(option => {
const optionElement = document.createElement('option');
optionElement.value = option.value;
optionElement.textContent = option.text;
thumbnailSizeSelect.appendChild(optionElement);
});
const viewModeButton = document.createElement('button');
viewModeButton.textContent = '⊞ Grid View';
viewModeButton.title = 'Toggle view mode';
viewModeButton.className = 'gallery-button gallery-button-primary';
const datasetTextButton = document.createElement('button');
datasetTextButton.textContent = '📝 Dataset';
datasetTextButton.title = 'Manage dataset text files';
datasetTextButton.className = 'gallery-button gallery-button-warning';
buttonsContainer.appendChild(thumbnailSizeSelect);
buttonsContainer.appendChild(viewModeButton);
buttonsContainer.appendChild(datasetTextButton);
// Search bar - moved to bottom to match Models tab
const searchContainer = document.createElement('div');
searchContainer.className = 'gallery-search-container';
// Add search label for better UX (matching Models tab)
const searchLabel = document.createElement('label');
searchLabel.textContent = 'Search:';
searchLabel.className = 'gallery-search-label';
const searchInput = document.createElement('input');
searchInput.type = 'text';
searchInput.id = 'gallery-search';
searchInput.placeholder = 'Search images...';
searchInput.className = 'gallery-search-input';
const clearButton = document.createElement('button');
clearButton.textContent = '✕';
clearButton.title = 'Clear search';
clearButton.className = 'gallery-button gallery-button-secondary';
searchContainer.appendChild(searchLabel);
searchContainer.appendChild(searchInput);
searchContainer.appendChild(clearButton);
// Metadata filter checkbox
const metadataFilterContainer = document.createElement('div');
metadataFilterContainer.className = 'gallery-metadata-filter-container';
const metadataFilterCheckbox = document.createElement('input');
metadataFilterCheckbox.type = 'checkbox';
metadataFilterCheckbox.id = 'gallery-metadata-filter';
metadataFilterCheckbox.className = 'gallery-metadata-filter-checkbox';
const metadataFilterLabel = document.createElement('label');
metadataFilterLabel.htmlFor = 'gallery-metadata-filter';
metadataFilterLabel.textContent = 'Show only images with generation parameters';
metadataFilterLabel.title = 'Filter for images containing AI generation metadata (parameters, prompt, extra_pnginfo, etc.)';
metadataFilterLabel.className = 'gallery-metadata-filter-label';
metadataFilterContainer.appendChild(metadataFilterCheckbox);
metadataFilterContainer.appendChild(metadataFilterLabel);
folderSection.appendChild(sortContainer);
folderSection.appendChild(buttonsContainer);
folderSection.appendChild(searchContainer);
folderSection.appendChild(metadataFilterContainer);
// Restore persisted gallery values from state
const savedFolder = selectors.selectedFolder();
const savedSort = selectors.gallerySortBy();
const savedSearch = selectors.gallerySearchQuery();
const savedViewMode = selectors.galleryViewMode();
const savedThumbnailSize = selectors.thumbnailSize();
const savedCurrentPath = selectors.currentPath();
// Handle custom folder restoration
if (savedFolder) {
// Check if it's a custom folder path
if (savedFolder.startsWith('custom:')) {
// Extract the path from the value
const customPath = savedFolder.substring(7);
// Update dropdown with the current custom path
populateFolderDropdown(folderDropdown, customPath);
folderDropdown.value = savedFolder;
} else if (savedCurrentPath && savedFolder === 'custom') {
// Legacy format: folder is 'custom' and path is in currentPath
populateFolderDropdown(folderDropdown, savedCurrentPath);
folderDropdown.value = `custom:${savedCurrentPath}`;
} else {
// Standard folder (notes, input, output)
folderDropdown.value = savedFolder;
}
}
if (savedSort) {
const sortValue = savedSort.replace(/-desc$/, '');
const isDescending = savedSort.endsWith('-desc');
sortSelect.value = sortValue;
orderButton.textContent = isDescending ? '↓' : '↑';
orderButton.dataset.descending = isDescending ? 'true' : 'false';
}
if (savedSearch) {
searchInput.value = savedSearch;
}
if (savedViewMode) {
viewModeButton.textContent = savedViewMode === 'grid' ? '⊞ Grid View' : '≡ List View';
viewModeButton.dataset.mode = savedViewMode;
}
if (savedThumbnailSize) {
thumbnailSizeSelect.value = savedThumbnailSize;
}
const savedMetadataOnly = selectors.showMetadataOnly();
if (savedMetadataOnly) {
metadataFilterCheckbox.checked = savedMetadataOnly;
}
console.debug('[GalleryTab] Restored gallery values from state:', {
folder: savedFolder,
currentPath: savedCurrentPath,
sort: savedSort,
search: savedSearch,
viewMode: savedViewMode,
thumbnailSize: savedThumbnailSize,
metadataOnly: savedMetadataOnly
});
return {
folderSection,
folderHeader,
folderDropdown,
// Controls components
searchContainer,
searchInput,
clearButton,
sortContainer,
sortSelect,
orderButton,
buttonsContainer,
thumbnailSizeSelect,
viewModeButton,
datasetTextButton,
metadataFilterCheckbox
};
}
/**
* Creates a wrapped thumbnail grid with header for the Gallery tab
* @returns {Object} Thumbnail grid components
*/
export async function createWrappedThumbnailGrid() {
const gridSection = document.createElement('div');
gridSection.className = 'gallery-grid-section';
const gridHeader = document.createElement('h4');
gridHeader.className = 'gallery-grid-header';
// Create header content with count
const headerContent = await renderGalleryGridHeader();
// Create reload button (small icon)
const refreshButton = document.createElement('button');
refreshButton.textContent = '🔄';
refreshButton.title = 'Reload folder';
refreshButton.className = 'gallery-button gallery-button-success gallery-refresh-button';
gridHeader.appendChild(headerContent);
gridHeader.appendChild(refreshButton);
// Use imported thumbnail grid component
const grid = createThumbnailGrid();
grid.gridContainer.classList.add('gallery-grid-container');
// Add placeholder content initially
const emptyState = await renderGalleryEmptyState();
grid.gridContainer.appendChild(emptyState);
gridSection.appendChild(gridHeader);
gridSection.appendChild(grid.gridContainer);
// Get references from the header
const imageCountSpan = gridHeader.querySelector('#image-count');
return {
gridSection,
gridHeader,
gridContainer: grid.gridContainer,
imageCountSpan,
refreshButton,
updateGridLayout: grid.updateGridLayout,
clear: grid.clear
};
}
/**
* Creates the metadata panel section for the Gallery tab
* @returns {Object} Metadata panel components
*/
export async function createMetadataPanel() {
const metadataSection = document.createElement('div');
metadataSection.className = 'gallery-metadata-section';
const metadataHeader = document.createElement('h4');
metadataHeader.className = 'gallery-metadata-header';
const metadataHeaderContent = await renderGalleryMetadataHeader();
metadataHeader.appendChild(metadataHeaderContent);
const metadataContent = document.createElement('div');
metadataContent.id = 'metadata-content';
metadataContent.className = 'gallery-metadata-content';
const metadataEmpty = await renderGalleryMetadataEmpty();
metadataContent.appendChild(metadataEmpty);
metadataSection.appendChild(metadataHeader);
metadataSection.appendChild(metadataContent);
return {
metadataSection,
metadataHeader,
metadataContent,
closeButton: metadataHeader.querySelector('#close-metadata')
};
}
/**
* Assembles the complete gallery tab layout from individual components
* @param {HTMLElement} container - Container element to populate
* @param {Object} components - Layout components to assemble
*/
export function assembleGalleryTabLayout(container, components) {
const {
header,
folderAndControls, // Combined section
grid,
metadata
} = components;
// Clear container
container.innerHTML = '';
// Create main gallery container
const galleryContainer = document.createElement('div');
galleryContainer.className = 'gallery-main-container';
// Add header with reduced padding
header.classList.add('gallery-header-compact');
container.appendChild(header);
// Add all sections to gallery container
galleryContainer.appendChild(folderAndControls.folderSection); // Combined folder and controls
galleryContainer.appendChild(grid.gridSection);
galleryContainer.appendChild(metadata.metadataSection);
// Add gallery container to main container
container.appendChild(galleryContainer);
}