531 lines
19 KiB
JavaScript
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);
|
|
}
|