679 lines
21 KiB
JavaScript
679 lines
21 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";
|
|
|
|
/**
|
|
* 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.style.cssText = `
|
|
font-size: 11px;
|
|
margin-top: 5px;
|
|
min-height: 16px;
|
|
color: #4CAF50;
|
|
font-style: italic;
|
|
`;
|
|
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.style.cssText = `
|
|
margin-bottom: 15px;
|
|
padding: 15px;
|
|
background: #2a2a2a;
|
|
border-radius: 6px;
|
|
border: 1px solid #444;
|
|
`;
|
|
|
|
const folderHeader = document.createElement('h4');
|
|
folderHeader.style.cssText = `
|
|
margin: 0 0 10px 0;
|
|
color: #4CAF50;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
`;
|
|
folderHeader.textContent = 'Folder & Controls';
|
|
|
|
const folderDropdown = document.createElement('select');
|
|
folderDropdown.id = 'gallery-folder-selector';
|
|
folderDropdown.style.cssText = `
|
|
width: 100%;
|
|
padding: 8px;
|
|
background: #333;
|
|
color: #fff;
|
|
border: 1px solid #555;
|
|
border-radius: 4px;
|
|
font-size: 12px;
|
|
cursor: pointer;
|
|
`;
|
|
|
|
// 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.style.cssText = `
|
|
margin-top: 15px;
|
|
margin-bottom: 10px;
|
|
display: flex;
|
|
gap: 8px;
|
|
align-items: center;
|
|
`;
|
|
|
|
const sortLabel = document.createElement('label');
|
|
sortLabel.textContent = 'Sort:';
|
|
sortLabel.style.cssText = `
|
|
color: #ccc;
|
|
font-size: 12px;
|
|
min-width: 35px;
|
|
`;
|
|
|
|
const sortSelect = document.createElement('select');
|
|
sortSelect.id = 'gallery-sort';
|
|
sortSelect.style.cssText = `
|
|
flex: 1;
|
|
padding: 4px 8px;
|
|
background: #333;
|
|
color: #fff;
|
|
border: 1px solid #555;
|
|
border-radius: 4px;
|
|
font-size: 11px;
|
|
cursor: pointer;
|
|
`;
|
|
|
|
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.style.cssText = `
|
|
background: #444;
|
|
color: white;
|
|
border: none;
|
|
padding: 4px 8px;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-size: 12px;
|
|
min-width: 30px;
|
|
`;
|
|
|
|
sortContainer.appendChild(sortLabel);
|
|
sortContainer.appendChild(sortSelect);
|
|
sortContainer.appendChild(orderButton);
|
|
|
|
// Thumbnail size and control buttons
|
|
const buttonsContainer = document.createElement('div');
|
|
buttonsContainer.style.cssText = `
|
|
margin-bottom: 10px;
|
|
display: flex;
|
|
gap: 8px;
|
|
flex-wrap: wrap;
|
|
`;
|
|
|
|
const thumbnailSizeSelect = document.createElement('select');
|
|
thumbnailSizeSelect.id = 'gallery-thumbnail-size';
|
|
thumbnailSizeSelect.style.cssText = `
|
|
flex: 1;
|
|
min-width: 80px;
|
|
padding: 4px 8px;
|
|
background: #333;
|
|
color: #fff;
|
|
border: 1px solid #555;
|
|
border-radius: 4px;
|
|
font-size: 11px;
|
|
cursor: pointer;
|
|
`;
|
|
|
|
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.style.cssText = `
|
|
background: #2196F3;
|
|
color: white;
|
|
border: none;
|
|
padding: 4px 8px;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-size: 12px;
|
|
`;
|
|
|
|
const datasetTextButton = document.createElement('button');
|
|
datasetTextButton.textContent = '📝 Dataset';
|
|
datasetTextButton.title = 'Manage dataset text files';
|
|
datasetTextButton.style.cssText = `
|
|
background: #FF9800;
|
|
color: white;
|
|
border: none;
|
|
padding: 4px 8px;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-size: 12px;
|
|
`;
|
|
|
|
buttonsContainer.appendChild(thumbnailSizeSelect);
|
|
buttonsContainer.appendChild(viewModeButton);
|
|
buttonsContainer.appendChild(datasetTextButton);
|
|
|
|
// Search bar - moved to bottom to match Models tab
|
|
const searchContainer = document.createElement('div');
|
|
searchContainer.style.cssText = `
|
|
margin-top: 15px;
|
|
display: flex;
|
|
gap: 8px;
|
|
align-items: center;
|
|
`;
|
|
|
|
// Add search label for better UX (matching Models tab)
|
|
const searchLabel = document.createElement('label');
|
|
searchLabel.textContent = 'Search:';
|
|
searchLabel.style.cssText = `
|
|
color: #ccc;
|
|
font-size: 12px;
|
|
min-width: 50px;
|
|
white-space: nowrap;
|
|
`;
|
|
|
|
const searchInput = document.createElement('input');
|
|
searchInput.type = 'text';
|
|
searchInput.id = 'gallery-search';
|
|
searchInput.placeholder = 'Search images...';
|
|
searchInput.style.cssText = `
|
|
flex: 1;
|
|
padding: 6px 10px;
|
|
background: #333;
|
|
color: #fff;
|
|
border: 1px solid #555;
|
|
border-radius: 4px;
|
|
font-size: 12px;
|
|
`;
|
|
|
|
const clearButton = document.createElement('button');
|
|
clearButton.textContent = '✕';
|
|
clearButton.title = 'Clear search';
|
|
clearButton.style.cssText = `
|
|
background: #666;
|
|
color: white;
|
|
border: none;
|
|
padding: 6px 10px;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-size: 12px;
|
|
`;
|
|
|
|
searchContainer.appendChild(searchLabel);
|
|
searchContainer.appendChild(searchInput);
|
|
searchContainer.appendChild(clearButton);
|
|
|
|
// Metadata filter checkbox
|
|
const metadataFilterContainer = document.createElement('div');
|
|
metadataFilterContainer.style.cssText = `
|
|
margin-top: 10px;
|
|
display: flex;
|
|
gap: 8px;
|
|
align-items: center;
|
|
`;
|
|
|
|
const metadataFilterCheckbox = document.createElement('input');
|
|
metadataFilterCheckbox.type = 'checkbox';
|
|
metadataFilterCheckbox.id = 'gallery-metadata-filter';
|
|
metadataFilterCheckbox.style.cssText = `
|
|
cursor: pointer;
|
|
`;
|
|
|
|
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.style.cssText = `
|
|
color: #ccc;
|
|
font-size: 12px;
|
|
cursor: pointer;
|
|
user-select: none;
|
|
`;
|
|
|
|
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 function createWrappedThumbnailGrid() {
|
|
const gridSection = document.createElement('div');
|
|
gridSection.style.cssText = `
|
|
margin-bottom: 15px;
|
|
padding: 15px;
|
|
background: #2a2a2a;
|
|
border-radius: 6px;
|
|
border: 1px solid #444;
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
`;
|
|
|
|
const gridHeader = document.createElement('h4');
|
|
gridHeader.style.cssText = `
|
|
margin: 0 0 10px 0;
|
|
color: #4CAF50;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
`;
|
|
|
|
// Create header content with count
|
|
const headerContent = document.createElement('span');
|
|
headerContent.innerHTML = 'Images <span id="image-count" style="color: #999; font-weight: normal;"></span>';
|
|
|
|
// Create reload button (small icon)
|
|
const refreshButton = document.createElement('button');
|
|
refreshButton.textContent = '🔄';
|
|
refreshButton.title = 'Reload folder';
|
|
refreshButton.style.cssText = `
|
|
background: #4CAF50;
|
|
color: white;
|
|
border: none;
|
|
padding: 4px 8px;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-size: 14px;
|
|
margin-left: 10px;
|
|
`;
|
|
|
|
refreshButton.addEventListener('mouseenter', () => {
|
|
refreshButton.style.background = '#45a049';
|
|
});
|
|
|
|
refreshButton.addEventListener('mouseleave', () => {
|
|
refreshButton.style.background = '#4CAF50';
|
|
});
|
|
|
|
gridHeader.appendChild(headerContent);
|
|
gridHeader.appendChild(refreshButton);
|
|
|
|
// Use imported thumbnail grid component
|
|
const grid = createThumbnailGrid();
|
|
grid.gridContainer.style.cssText += `
|
|
border: 1px solid #555;
|
|
border-radius: 4px;
|
|
background: #333;
|
|
flex: 1;
|
|
`;
|
|
|
|
// Add placeholder content initially
|
|
grid.gridContainer.innerHTML = `
|
|
<div style="
|
|
grid-column: 1 / -1;
|
|
text-align: center;
|
|
color: #888;
|
|
padding: 40px;
|
|
font-style: italic;
|
|
">
|
|
🖼️ Select a folder to view images
|
|
</div>
|
|
`;
|
|
|
|
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 function createMetadataPanel() {
|
|
const metadataSection = document.createElement('div');
|
|
metadataSection.style.cssText = `
|
|
padding: 15px;
|
|
background: #2a2a2a;
|
|
border-radius: 6px;
|
|
border: 1px solid #444;
|
|
display: block;
|
|
`;
|
|
|
|
const metadataHeader = document.createElement('h4');
|
|
metadataHeader.style.cssText = `
|
|
margin: 0 0 10px 0;
|
|
color: #4CAF50;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
`;
|
|
metadataHeader.innerHTML = `
|
|
<span>Image Details</span>
|
|
<button id="close-metadata" style="
|
|
background: #f44336;
|
|
color: white;
|
|
border: none;
|
|
padding: 4px 8px;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-size: 11px;
|
|
">Clear</button>
|
|
`;
|
|
|
|
const metadataContent = document.createElement('div');
|
|
metadataContent.id = 'metadata-content';
|
|
metadataContent.style.cssText = `
|
|
max-height: 300px;
|
|
overflow-y: auto;
|
|
background: #333;
|
|
border: 1px solid #555;
|
|
border-radius: 4px;
|
|
padding: 15px;
|
|
color: #e0e0e0;
|
|
font-family: 'Courier New', monospace;
|
|
font-size: 11px;
|
|
line-height: 1.4;
|
|
`;
|
|
metadataContent.innerHTML = '<em style="color: #999;">Select an image to view details...</em>';
|
|
|
|
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.style.cssText = `
|
|
padding: 15px;
|
|
`;
|
|
|
|
// Add header with reduced padding
|
|
header.style.padding = '10px 15px'; // Make header more 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);
|
|
}
|