Files
arcum42-ComfyUI_SageUtils/js/reports/htmlTemplates.js
T

596 lines
23 KiB
JavaScript

/**
* HTML Templates and Client-Side Code for SageUtils Reports
* Contains HTML document templates, CSS styles, and client-side JavaScript
*/
import {
DEFAULT_THUMBNAIL_WIDTH,
DEFAULT_THUMBNAIL_HEIGHT
} from '../shared/constants.js';
/**
* Generate CSS styles for the HTML report
* @returns {string} - CSS style definitions
*/
export function generateCssStyles() {
return `
body {
font-family: Arial, sans-serif;
margin: 20/**
* Generate HTML document start with CSS and JavaScript
* @param {string} currentDateTime - Current date/time string
* @param {string} cssStyles - CSS styles content
* @param {string} clientJs - Client-side JavaScript content
* @returns {string} - HTML document start
*/
export function generateHtmlDocumentStart(currentDateTime, cssStyles, clientJs) { background-color: #f5f5f5;
}
h1 {
text-align: center;
color: white;
background-color: green;
padding: 15px;
border-radius: 8px;
margin-bottom: 10px;
}
h2 {
text-align: center;
color: darkblue;
background-color: yellow;
padding: 10px;
border-radius: 8px;
margin-bottom: 20px;
}
.info {
text-align: center;
color: #666;
margin-bottom: 20px;
font-style: italic;
}
table {
width: 100%;
border-collapse: collapse;
margin-bottom: 30px;
background-color: white;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
th {
border: 2px solid blue;
color: yellow;
background-color: blue;
font-size: 14px;
padding: 10px;
position: sticky;
top: 0;
z-index: 10;
}
td {
border: 2px solid maroon;
font-size: 12px;
padding: 8px;
vertical-align: top;
}
td.image-cell {
width: ${DEFAULT_THUMBNAIL_WIDTH + 6}px;
height: ${DEFAULT_THUMBNAIL_HEIGHT + 6}px;
padding: 3px;
text-align: center;
vertical-align: middle;
}
td.image-cell.compact {
width: auto;
height: auto;
min-height: 30px;
padding: 3px;
text-align: center;
vertical-align: middle;
}
.section-header {
background-color: #333;
color: white;
padding: 15px;
text-align: center;
font-size: 18px;
font-weight: bold;
margin-top: 30px;
border-radius: 8px;
}
button {
cursor: pointer;
border: none;
border-radius: 4px;
padding: 4px 8px;
}
a {
color: blue;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
/* Sortable table styles */
table.sortable th:not(.sorttable_nosort) {
cursor: pointer;
user-select: none;
position: sticky;
}
table.sortable th:not(.sorttable_nosort):hover {
background-color: #5555ff;
}
table.sortable th.sorttable_sorted {
background-color: #00aa00;
}
table.sortable th.sorttable_sorted_reverse {
background-color: #aa0000;
}
table.sortable th.sorttable_nosort {
background-color: #666;
cursor: default;
position: sticky;
top: 0;
z-index: 10;
}
.sort-indicator {
font-size: 14px;
margin-left: 5px;
}
/* Model grouping styles */
.model-group-first {
border-top: 3px solid #4CAF50 !important;
}
.model-group {
background-color: rgba(76, 175, 80, 0.05);
}
.model-group-last {
border-bottom: 3px solid #4CAF50 !important;
}
.civitai-group-header {
background-color: #e8f5e8;
font-weight: bold;
color: #2e7d2e;
border: 2px solid #4CAF50 !important;
}`;
}
/**
* Generate client-side JavaScript for the HTML report
* @param {string} sortBy - Current sort selection from model tab
* @returns {string} - JavaScript code for client-side functionality
*/
export function generateClientSideJs(sortBy = 'name') {
return `
// Configuration from report generation
const INITIAL_SORT_BY = '${sortBy}';
// Map sortBy values from Models tab to table column indices (accounting for hidden columns)
function getSortColumnIndex(sortBy) {
const sortMap = {
'name': 0, // Model Name
'name-desc': 0, // Model Name (reverse)
'type': 2, // Type
'size': 7, // File Size (shifted due to added Version ID column)
'size-desc': 7, // File Size (reverse)
'lastused': 8, // Last Used (shifted due to added Version ID column)
'lastused-desc': 8 // Last Used (reverse)
};
return sortMap[sortBy] !== undefined ? sortMap[sortBy] : -1;
}
// Determine if initial sort should be descending
function isDescendingSort(sortBy) {
return sortBy.endsWith('-desc');
}
// Custom sortable table implementation
let sortDirection = {};
function makeSortable() {
const tables = document.querySelectorAll('table.sortable');
tables.forEach(table => {
const headers = table.querySelectorAll('th');
headers.forEach((header, index) => {
if (!header.classList.contains('sorttable_nosort')) {
header.style.cursor = 'pointer';
header.addEventListener('click', () => sortTable(table, index));
// Add sort indicator
const indicator = document.createElement('span');
indicator.className = 'sort-indicator';
indicator.innerHTML = ' ⇅';
indicator.style.opacity = '0.5';
header.appendChild(indicator);
}
});
});
}
function sortTable(table, columnIndex) {
const tableId = table.id || 'table_' + Math.random().toString(36).substr(2, 9);
if (!table.id) table.id = tableId;
const sortKey = tableId + '_' + columnIndex;
const currentDirection = sortDirection[sortKey] || 'asc';
const newDirection = currentDirection === 'asc' ? 'desc' : 'asc';
sortDirection[sortKey] = newDirection;
const tbody = table.querySelector('tbody') || table;
const rows = Array.from(tbody.querySelectorAll('tr')).slice(1); // Skip header row
// Clear all header styles and indicators
const headers = table.querySelectorAll('th');
headers.forEach(header => {
header.classList.remove('sorttable_sorted', 'sorttable_sorted_reverse');
const indicator = header.querySelector('.sort-indicator');
if (indicator) {
indicator.innerHTML = ' ⇅';
indicator.style.opacity = '0.5';
}
});
// Update current header style and indicator
const currentHeader = headers[columnIndex];
if (currentHeader) {
currentHeader.classList.add(newDirection === 'asc' ? 'sorttable_sorted' : 'sorttable_sorted_reverse');
const indicator = currentHeader.querySelector('.sort-indicator');
if (indicator) {
indicator.innerHTML = newDirection === 'asc' ? ' ↑' : ' ↓';
indicator.style.opacity = '1';
}
}
rows.sort((a, b) => {
const cellA = a.cells[columnIndex];
const cellB = b.cells[columnIndex];
if (!cellA || !cellB) return 0;
// Check for custom sort key
let valueA = cellA.getAttribute('sorttable_customkey');
let valueB = cellB.getAttribute('sorttable_customkey');
if (valueA && valueB) {
// Numeric comparison for custom keys
valueA = parseFloat(valueA);
valueB = parseFloat(valueB);
} else {
// Text comparison
valueA = cellA.textContent.trim();
valueB = cellB.textContent.trim();
// Try to parse as numbers for better sorting
const numA = parseFloat(valueA.replace(/[^\\\\d.-]/g, ''));
const numB = parseFloat(valueB.replace(/[^\\\\d.-]/g, ''));
if (!isNaN(numA) && !isNaN(numB)) {
valueA = numA;
valueB = numB;
}
}
let comparison = 0;
if (valueA < valueB) {
comparison = -1;
} else if (valueA > valueB) {
comparison = 1;
}
return newDirection === 'desc' ? -comparison : comparison;
});
// Re-append sorted rows
rows.forEach(row => tbody.appendChild(row));
}
async function copyToClipboard(text) {
try {
await navigator.clipboard.writeText(text);
console.log('Text copied to clipboard');
} catch (err) {
console.error('Failed to copy: ', err);
}
}
function toggleImageExpand(img) {
if (!img.isExpanded) {
// Expand
img.originalStyle = img.style.cssText;
img.style.cssText = 'width:auto;height:auto;max-width:90vw;max-height:80vh;object-fit:contain;border-radius:4px;cursor:pointer;transition:all 0.3s ease;position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);z-index:9999;box-shadow:0 4px 20px rgba(0,0,0,0.5);background:white;';
img.isExpanded = true;
img.setAttribute('data-expanded', 'true');
// Add backdrop
const backdrop = document.createElement('div');
backdrop.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.8);z-index:9998;';
backdrop.addEventListener('click', () => {
toggleImageExpand(img);
});
document.body.appendChild(backdrop);
img.backdrop = backdrop;
} else {
// Collapse
img.style.cssText = img.originalStyle || 'width:150px;height:100px;object-fit:cover;border-radius:4px;cursor:pointer;transition:all 0.3s ease;';
img.isExpanded = false;
img.removeAttribute('data-expanded');
// Remove backdrop
if (img.backdrop) {
document.body.removeChild(img.backdrop);
delete img.backdrop;
}
}
}
function getThumbnailStyle(additionalStyle) {
if (typeof additionalStyle !== 'string') additionalStyle = '';
var DEFAULT_THUMBNAIL_WIDTH = ${DEFAULT_THUMBNAIL_WIDTH};
var DEFAULT_THUMBNAIL_HEIGHT = ${DEFAULT_THUMBNAIL_HEIGHT};
var baseStyle = 'max-width:' + DEFAULT_THUMBNAIL_WIDTH + 'px;max-height:' + DEFAULT_THUMBNAIL_HEIGHT + 'px;width:auto;height:auto;object-fit:cover;border-radius:4px;cursor:pointer;transition:all 0.3s ease;';
return additionalStyle ? baseStyle + additionalStyle : baseStyle;
}
function makeContainerCompact(container) {
// Resize container to be compact for text-only content
container.style.width = 'auto';
container.style.height = 'auto';
container.style.minWidth = '60px';
container.style.minHeight = '20px';
container.style.padding = '8px';
container.style.display = 'flex';
container.style.alignItems = 'center';
container.style.justifyContent = 'center';
// Also resize the parent table cell to be compact
var parentCell = container.parentElement;
while (parentCell && parentCell.tagName !== 'TD') {
parentCell = parentCell.parentElement;
}
if (parentCell && parentCell.classList.contains('image-cell')) {
parentCell.style.height = 'auto';
parentCell.style.minHeight = '30px';
parentCell.style.verticalAlign = 'middle';
}
}
async function loadCivitaiImage(element) {
// Check if the current element has the attributes, if not check parent
let hash = element.getAttribute('data-civitai-hash');
let apiUrl = element.getAttribute('data-civitai-url');
let targetElement = element;
if (!hash || !apiUrl) {
// Try parent element
targetElement = element.parentElement;
hash = targetElement.getAttribute('data-civitai-hash');
apiUrl = targetElement.getAttribute('data-civitai-url');
}
if (!hash || !apiUrl) return;
try {
const response = await fetch(apiUrl);
if (response.ok) {
const data = await response.json();
const images = data.images || [];
// Find first appropriate image (NSFW level <= 1)
const appropriateImage = images.find(img => (img.nsfwLevel || 0) <= 1);
if (appropriateImage && appropriateImage.url) {
const imgElement = document.createElement('img');
imgElement.src = appropriateImage.url;
imgElement.style.cssText = getThumbnailStyle();
imgElement.alt = 'Model example image';
imgElement.loading = 'lazy';
imgElement.title = 'Click to expand/collapse';
imgElement.onerror = function() {
makeContainerCompact(this.parentElement);
this.parentElement.innerHTML = '<span style="color:#999;font-size:11px;">No image</span>';
};
// Add click handler for expand/collapse
imgElement.addEventListener('click', function(e) {
e.stopPropagation();
toggleImageExpand(this);
});
targetElement.innerHTML = '';
targetElement.appendChild(imgElement);
} else {
makeContainerCompact(targetElement);
targetElement.innerHTML = '<span style="color:#999;font-size:11px;">No image</span>';
}
} else {
makeContainerCompact(targetElement);
targetElement.innerHTML = '<span style="color:#999;font-size:11px;">Not available</span>';
}
} catch (error) {
console.debug('Failed to load Civitai image:', error);
makeContainerCompact(targetElement);
targetElement.innerHTML = '<span style="color:#999;font-size:11px;">Load failed</span>';
}
}
// Auto-load all images when page loads
function autoLoadImages() {
const imageContainers = document.querySelectorAll('.auto-load-image');
const compactContainers = document.querySelectorAll('.image-cell.compact').length;
console.log('Loading ' + imageContainers.length + ' images from Civitai. ' + compactContainers + ' entries skipped due to previous failures.');
imageContainers.forEach((container, index) => {
// Stagger the loading to avoid overwhelming the API
setTimeout(() => {
loadCivitaiImage(container);
}, index * 200); // 200ms delay between each image
});
}
// Initialize everything when page loads
function initializePage() {
makeSortable();
autoLoadImages();
applyInitialSort();
}
// Apply initial sort based on Models tab selection
function applyInitialSort() {
const columnIndex = getSortColumnIndex(INITIAL_SORT_BY);
if (columnIndex >= 0) {
const isDescending = isDescendingSort(INITIAL_SORT_BY);
const tables = document.querySelectorAll('table.sortable');
tables.forEach(table => {
const headers = table.querySelectorAll('th');
if (headers[columnIndex] && !headers[columnIndex].classList.contains('sorttable_nosort')) {
// Set up the sort direction before calling sortTable
const tableId = table.id || 'table_' + Math.random().toString(36).substr(2, 9);
if (!table.id) table.id = tableId;
const sortKey = tableId + '_' + columnIndex;
// Set opposite direction so sortTable will switch to desired direction
sortDirection[sortKey] = isDescending ? 'asc' : 'desc';
// Trigger the sort
sortTable(table, columnIndex);
}
});
}
}
// Load functionality after page content is ready
document.addEventListener('DOMContentLoaded', initializePage);
// Also run immediately in case DOMContentLoaded already fired
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializePage);
} else {
initializePage();
}
// Add ESC key support to close expanded images
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
const expandedImages = document.querySelectorAll('img[data-expanded="true"]');
expandedImages.forEach(img => {
if (img.isExpanded) {
toggleImageExpand(img);
}
});
}
});`;
}
/**
* Generate the complete HTML document template
* @param {Object} options - Template options
* @param {string} options.currentDateTime - Current date and time
* @param {string} options.sortBy - Current sort selection
* @param {string} options.filterDescription - Description of applied filters
* @param {string} options.searchDescription - Description of search filter
* @param {string} options.lastUsedDescription - Description of last used filter
* @param {string} options.sortDescription - Description of sort criteria
* @param {Array} options.allModels - All models array
* @param {Array} options.checkpointModels - Checkpoint models array
* @param {Array} options.loraModels - LoRA models array
* @param {string} options.folderFilter - Current folder filter
* @returns {string} - Complete HTML document template
*/
export function generateHtmlDocumentStart(currentDateTime, cssStyles, clientJs) {
return `<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'>
<title>SageUtils Model Report - ${currentDateTime}</title>
<style>
${cssStyles}
</style>
<script>
${clientJs}
</script>
</head>
<body>
`;
}
/**
* Generate the HTML document end
* @returns {string} - HTML document closing tags
*/
export function generateHtmlDocumentEnd() {
return `
</body>
</html>
`;
}
/**
* Generate table headers based on visible columns
* @param {Array} visibleColumns - Array of visible column configurations
* @returns {string} - HTML table headers
*/
export function generateTableHeaders(visibleColumns) {
return visibleColumns.map(col => {
const sortClass = col.sortable ? '' : ' class="sorttable_nosort"';
return ` <th style="width:${col.width};"${sortClass}><b>${col.name}</b></th>`;
}).join('\n');
}
/**
* Generate section header for model types
* @param {string} sectionTitle - Title of the section
* @param {number} modelCount - Number of models in section
* @returns {string} - HTML section header
*/
export function generateSectionHeader(sectionTitle, modelCount) {
return `
<div class="section-header">${sectionTitle} (${modelCount})</div>`;
}
/**
* Generate table wrapper with headers
* @param {Array} visibleColumns - Array of visible column configurations
* @param {string} tableRows - HTML table rows content
* @returns {string} - Complete HTML table
*/
export function generateTableWrapper(visibleColumns, tableRows) {
return `
<table class="sortable">
<tr>
${generateTableHeaders(visibleColumns)}
</tr>
${tableRows}
</table>
`;
}
/**
* Generate model statistics section
*/
export function generateModelStatsSection(currentDateTime, filterDescription, searchDescription, lastUsedDescription, sortDescription, allModels, checkpointModels, loraModels, folderFilter) {
return `
<h1>SageUtils Model Report</h1>
<h2>Generated: ${currentDateTime}</h2>
<div class="info">
Filters Applied: ${filterDescription}${searchDescription}${lastUsedDescription}${sortDescription}<br>
Total Models: ${allModels.length}${folderFilter === 'all' ? ` (${checkpointModels.length} Checkpoints, ${loraModels.length} LoRAs)` : ''}<br>
<small style="color: #999; font-style: italic;">Click column headers to sort • Images load automatically from Civitai</small>
</div>
`;
}
/**
* Generate complete table section with headers and rows
*/
export function generateTableSection(sectionTitle, modelCount, tableRows, visibleColumns) {
return `
<div class="section-header">${sectionTitle} (${modelCount})</div>
<table class="sortable">
<tr>
${generateTableHeaders(visibleColumns)}
</tr>
${tableRows}
</table>
`;
}