395 lines
16 KiB
JavaScript
395 lines
16 KiB
JavaScript
/**
|
|
* HTML Templates and Client-Side Code for SageUtils Reports
|
|
* Contains HTML document templates and client-side JavaScript
|
|
*/
|
|
|
|
import {
|
|
loadHtmlTemplate,
|
|
renderHtmlTemplate
|
|
} from '../utils/htmlTemplateLoader.js';
|
|
|
|
let reportDocumentStartTemplate = null;
|
|
let reportDocumentEndTemplate = null;
|
|
let reportSummarySectionTemplate = null;
|
|
let reportTableSectionTemplate = null;
|
|
let reportSectionHeaderTemplate = null;
|
|
let reportTableWrapperTemplate = null;
|
|
let reportTableHeaderCellTemplate = null;
|
|
|
|
async function getReportDocumentStartTemplate() {
|
|
if (!reportDocumentStartTemplate) {
|
|
reportDocumentStartTemplate = await loadHtmlTemplate('extensions/comfyui_sageutils/reports/partials/reportDocumentStart.html');
|
|
}
|
|
return reportDocumentStartTemplate;
|
|
}
|
|
|
|
async function getReportDocumentEndTemplate() {
|
|
if (!reportDocumentEndTemplate) {
|
|
reportDocumentEndTemplate = await loadHtmlTemplate('extensions/comfyui_sageutils/reports/partials/reportDocumentEnd.html');
|
|
}
|
|
return reportDocumentEndTemplate;
|
|
}
|
|
|
|
async function getReportSummarySectionTemplate() {
|
|
if (!reportSummarySectionTemplate) {
|
|
reportSummarySectionTemplate = await loadHtmlTemplate('extensions/comfyui_sageutils/reports/partials/reportSummarySection.html');
|
|
}
|
|
return reportSummarySectionTemplate;
|
|
}
|
|
|
|
async function getReportTableSectionTemplate() {
|
|
if (!reportTableSectionTemplate) {
|
|
reportTableSectionTemplate = await loadHtmlTemplate('extensions/comfyui_sageutils/reports/partials/reportTableSection.html');
|
|
}
|
|
return reportTableSectionTemplate;
|
|
}
|
|
|
|
async function getReportSectionHeaderTemplate() {
|
|
if (!reportSectionHeaderTemplate) {
|
|
reportSectionHeaderTemplate = await loadHtmlTemplate('extensions/comfyui_sageutils/reports/partials/reportSectionHeader.html');
|
|
}
|
|
return reportSectionHeaderTemplate;
|
|
}
|
|
|
|
async function getReportTableWrapperTemplate() {
|
|
if (!reportTableWrapperTemplate) {
|
|
reportTableWrapperTemplate = await loadHtmlTemplate('extensions/comfyui_sageutils/reports/partials/reportTableWrapper.html');
|
|
}
|
|
return reportTableWrapperTemplate;
|
|
}
|
|
|
|
async function getReportTableHeaderCellTemplate() {
|
|
if (!reportTableHeaderCellTemplate) {
|
|
reportTableHeaderCellTemplate = await loadHtmlTemplate('extensions/comfyui_sageutils/reports/partials/reportTableHeaderCell.html');
|
|
}
|
|
return reportTableHeaderCellTemplate;
|
|
}
|
|
|
|
/**
|
|
* 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.classList.add('sortable-header');
|
|
header.addEventListener('click', () => sortTable(table, index));
|
|
|
|
const indicator = document.createElement('span');
|
|
indicator.className = 'sort-indicator';
|
|
indicator.innerHTML = ' ⇅';
|
|
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
|
|
|
|
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.classList.remove('active');
|
|
}
|
|
});
|
|
|
|
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.classList.add('active');
|
|
}
|
|
}
|
|
|
|
rows.sort((a, b) => {
|
|
const cellA = a.cells[columnIndex];
|
|
const cellB = b.cells[columnIndex];
|
|
if (!cellA || !cellB) return 0;
|
|
|
|
let valueA = cellA.getAttribute('sorttable_customkey');
|
|
let valueB = cellB.getAttribute('sorttable_customkey');
|
|
|
|
if (valueA && valueB) {
|
|
valueA = parseFloat(valueA);
|
|
valueB = parseFloat(valueB);
|
|
} else {
|
|
valueA = cellA.textContent.trim();
|
|
valueB = cellB.textContent.trim();
|
|
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;
|
|
});
|
|
|
|
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) {
|
|
img.classList.add('thumbnail-image-expanded');
|
|
img.isExpanded = true;
|
|
img.setAttribute('data-expanded', 'true');
|
|
|
|
const backdrop = document.createElement('div');
|
|
backdrop.className = 'report-image-backdrop';
|
|
backdrop.addEventListener('click', () => toggleImageExpand(img));
|
|
document.body.appendChild(backdrop);
|
|
img.backdrop = backdrop;
|
|
} else {
|
|
img.classList.remove('thumbnail-image-expanded');
|
|
img.isExpanded = false;
|
|
img.removeAttribute('data-expanded');
|
|
|
|
if (img.backdrop) {
|
|
document.body.removeChild(img.backdrop);
|
|
delete img.backdrop;
|
|
}
|
|
}
|
|
}
|
|
|
|
function makeContainerCompact(container) {
|
|
container.classList.add('compact-thumbnail-container');
|
|
let parentCell = container.parentElement;
|
|
while (parentCell && parentCell.tagName !== 'TD') {
|
|
parentCell = parentCell.parentElement;
|
|
}
|
|
if (parentCell && parentCell.classList.contains('image-cell')) {
|
|
parentCell.classList.add('compact-image-cell');
|
|
}
|
|
}
|
|
|
|
async function loadCivitaiImage(element) {
|
|
let hash = element.getAttribute('data-civitai-hash');
|
|
let apiUrl = element.getAttribute('data-civitai-url');
|
|
let targetElement = element;
|
|
if (!hash || !apiUrl) {
|
|
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 || [];
|
|
const appropriateImage = images.find(img => (img.nsfwLevel || 0) <= 1);
|
|
if (appropriateImage && appropriateImage.url) {
|
|
const imgElement = document.createElement('img');
|
|
imgElement.src = appropriateImage.url;
|
|
imgElement.className = 'thumbnail-image';
|
|
imgElement.alt = 'Model example image';
|
|
imgElement.loading = 'lazy';
|
|
imgElement.title = 'Click to expand/collapse';
|
|
imgElement.onerror = function() {
|
|
makeContainerCompact(this.parentElement);
|
|
this.parentElement.innerHTML = '<span class="thumbnail-placeholder-text">No image</span>';
|
|
};
|
|
imgElement.addEventListener('click', function(e) {
|
|
e.stopPropagation();
|
|
toggleImageExpand(this);
|
|
});
|
|
targetElement.innerHTML = '';
|
|
targetElement.appendChild(imgElement);
|
|
} else {
|
|
makeContainerCompact(targetElement);
|
|
targetElement.innerHTML = '<span class="thumbnail-placeholder-text">No image</span>';
|
|
}
|
|
} else {
|
|
makeContainerCompact(targetElement);
|
|
targetElement.innerHTML = '<span class="thumbnail-placeholder-text">Not available</span>';
|
|
}
|
|
} catch (error) {
|
|
console.debug('Failed to load Civitai image:', error);
|
|
makeContainerCompact(targetElement);
|
|
targetElement.innerHTML = '<span class="thumbnail-placeholder-text">Load failed</span>';
|
|
}
|
|
}
|
|
|
|
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) => {
|
|
setTimeout(() => {
|
|
loadCivitaiImage(container);
|
|
}, index * 200);
|
|
});
|
|
}
|
|
|
|
function initializePage() {
|
|
makeSortable();
|
|
autoLoadImages();
|
|
applyInitialSort();
|
|
}
|
|
|
|
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')) {
|
|
const tableId = table.id || 'table_' + Math.random().toString(36).substr(2, 9);
|
|
if (!table.id) table.id = tableId;
|
|
const sortKey = tableId + '_' + columnIndex;
|
|
sortDirection[sortKey] = isDescending ? 'asc' : 'desc';
|
|
sortTable(table, columnIndex);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', initializePage);
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', initializePage);
|
|
} else {
|
|
initializePage();
|
|
}
|
|
|
|
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 HTML document start with external CSS and JavaScript
|
|
* @param {string} currentDateTime - Current date/time string
|
|
* @param {string} clientJs - Client-side JavaScript content
|
|
* @param {string} cssHref - CSS href for the report stylesheet
|
|
* @returns {string} - HTML document start
|
|
*/
|
|
export async function generateHtmlDocumentStart(currentDateTime, clientJs, cssHref) {
|
|
const template = await getReportDocumentStartTemplate();
|
|
return renderHtmlTemplate(template, { currentDateTime, clientJs, cssHref });
|
|
}
|
|
|
|
export async function generateHtmlDocumentEnd() {
|
|
const template = await getReportDocumentEndTemplate();
|
|
return renderHtmlTemplate(template, {});
|
|
}
|
|
|
|
export async function generateTableHeaders(visibleColumns) {
|
|
const template = await getReportTableHeaderCellTemplate();
|
|
const headers = await Promise.all(visibleColumns.map(async col => {
|
|
const sortClass = col.sortable ? '' : ' sorttable_nosort';
|
|
return renderHtmlTemplate(template, {
|
|
key: col.key,
|
|
sortClass,
|
|
name: col.name
|
|
});
|
|
}));
|
|
return headers.join('\n');
|
|
}
|
|
|
|
export async function generateSectionHeader(sectionTitle, modelCount) {
|
|
const template = await getReportSectionHeaderTemplate();
|
|
return renderHtmlTemplate(template, { sectionTitle, modelCount });
|
|
}
|
|
|
|
export async function generateTableWrapper(visibleColumns, tableRows) {
|
|
const headers = await generateTableHeaders(visibleColumns);
|
|
const template = await getReportTableWrapperTemplate();
|
|
return renderHtmlTemplate(template, { tableHeaders: headers, tableRows });
|
|
}
|
|
|
|
export async function generateModelStatsSection(currentDateTime, filterDescription, searchDescription, lastUsedDescription, sortDescription, allModels, checkpointModels, loraModels, folderFilter) {
|
|
const template = await getReportSummarySectionTemplate();
|
|
const folderBreakdown = folderFilter === 'all' ? ` (${checkpointModels.length} Checkpoints, ${loraModels.length} LoRAs)` : '';
|
|
return renderHtmlTemplate(template, {
|
|
currentDateTime,
|
|
filterDescription,
|
|
searchDescription,
|
|
lastUsedDescription,
|
|
sortDescription,
|
|
allCount: allModels.length,
|
|
folderBreakdown
|
|
});
|
|
}
|
|
|
|
export async function generateTableSection(sectionTitle, modelCount, tableRows, visibleColumns) {
|
|
const headers = await generateTableHeaders(visibleColumns);
|
|
const template = await getReportTableSectionTemplate();
|
|
return renderHtmlTemplate(template, {
|
|
sectionTitle,
|
|
modelCount,
|
|
tableHeaders: headers,
|
|
tableRows
|
|
});
|
|
}
|