/** * ui_grid.js * Extracted Grid methods. */ import { app } from "../../../scripts/app.js"; import { translate } from './locales.js'; import { focusModel, modelListUrl, renderBaseBar, renderModelTabs, renderTypeBar } from './ui_model_types.js'; import { countBases, inFamily } from './model_bases.js'; import { matchesModelQuery } from './ui_model_search.js'; import { renderSourceBadge } from './model_source.js'; import { renderModelSourcesView } from './ui_model_sources.js'; import { renderModelTidyView } from './ui_model_tidy.js'; import { recordCanvasStep } from './canvas_history.js'; const t = (key, params) => translate(key, params); export function cardPreviewUrl(previewUrl, thumbnailMode) { if (!previewUrl || thumbnailMode === 'original') return previewUrl; return `${previewUrl}${previewUrl.includes('?') ? '&' : '?'}variant=card`; } /** A video cover's still first frame (made once on the server), shown until the video plays. */ function cardPosterUrl(previewUrl) { return `${previewUrl}${previewUrl.includes('?') ? '&' : '?'}variant=poster`; } // The first screen of cards loads first; the rest waits until scrolled near. const EAGER_CARDS = 12; function modelCardDisplayName(model) { const metadata = model?.metadata || {}; return metadata.custom_name || model?.filename || ''; } export async function loadModels() { try { if (this._modelLoadController) this._modelLoadController.abort(); this.modelSources?.controller?.abort(); this.modelTidy?.controller?.abort(); // Cards of an earlier load still being drawn in chunks stop here. this._modelRenderGeneration = (this._modelRenderGeneration || 0) + 1; if (this.modelView === 'tidy') { // the tabs, then what is in the wrong folder or twice stopMediaInContainer(this.grid); this.grid.replaceChildren(renderModelTabs(this)); renderModelTidyView(this, this.grid); return; } if (this.modelView === 'sources') { // the tabs, then where each model is downloaded stopMediaInContainer(this.grid); this.grid.replaceChildren(renderModelTabs(this)); renderModelSourcesView(this, this.grid); return; } const loadController = new AbortController(); this._modelLoadController = loadController; const res = await fetch(modelListUrl(this), { signal: loadController.signal }); const data = await res.json(); if (this._modelLoadController !== loadController) return; if (window.anomalous_update_hash_cache && data.models) { window.anomalous_update_hash_cache(data.models); } this.models = data.models || []; const renderGeneration = this._modelRenderGeneration; if (this._modelMediaObserver) this._modelMediaObserver.disconnect(); this._modelMediaObserver = typeof IntersectionObserver === 'function' ? new IntersectionObserver((entries, observer) => { entries.forEach(entry => { const media = entry.target; const shouldAutoplay = media.tagName === 'VIDEO' && media.dataset.anomalousAutoplay === 'true'; if (!entry.isIntersecting) { if (media.tagName === 'VIDEO') { media.pause(); if (media.getAttribute('src')) { media.removeAttribute('src'); media.load(); } } return; } if (media.tagName === 'VIDEO' && !shouldAutoplay) return; const pendingSrc = media.dataset.anomalousSrc; if (pendingSrc && !media.getAttribute('src')) media.src = pendingSrc; if (shouldAutoplay) media.play().catch(() => {}); }); }, { rootMargin: '300px' }) : null; stopMediaInContainer(this.grid); this.grid.replaceChildren(renderModelTabs(this), renderTypeBar(this, (data.models || []).length)); const query = this.modelQuery || ''; if (data.models?.length && query) data.models = data.models.filter(model => matchesModelQuery(model, query)); const bases = renderBaseBar(this, countBases(data.models)); if (bases) { this.grid.appendChild(bases); data.models = data.models.filter(model => inFamily(model, this.modelBase)); } if (!data.models || data.models.length === 0) { const empty = document.createElement('div'); empty.className = 'anomalous-grid-empty'; empty.textContent = query && this.models.length ? t('modelSearchNone', { query }) : t('noModels'); this.grid.appendChild(empty); return; } let renderIndex = 0; const renderChunk = () => { if (this._modelRenderGeneration !== renderGeneration) return; const fragment = document.createDocumentFragment(); const end = Math.min(renderIndex + 40, data.models.length); for (; renderIndex < end; renderIndex++) { const model = data.models[renderIndex]; const card = document.createElement('div'); card.className = 'anomalous-card'; if (model.preview_url) { const isVideo = model.preview_url.match(/\.mp4(?:&|$)/i) || model.preview_url.match(/\.webm(?:&|$)/i); if (isVideo) { const video = document.createElement('video'); video.dataset.anomalousSrc = model.preview_url; video.muted = true; video.loop = true; video.playsInline = true; // Nothing of the video is fetched until it plays; the poster shows meanwhile. video.preload = 'none'; video.poster = cardPosterUrl(model.preview_url); const ensureVideoSource = () => { if (!video.getAttribute('src')) video.src = video.dataset.anomalousSrc; }; if (this.energySaving) { video.pause(); card.addEventListener('mouseenter', () => { ensureVideoSource(); video.play().catch(() => {}); }); card.addEventListener('mouseleave', () => { video.pause(); video.currentTime = 0; }); } else { video.dataset.anomalousAutoplay = 'true'; } if (this._modelMediaObserver) { this._modelMediaObserver.observe(video); } else { ensureVideoSource(); if (!this.energySaving) video.autoplay = true; } card.appendChild(video); } else { const img = document.createElement('img'); const early = renderIndex < EAGER_CARDS; img.loading = early ? 'eager' : 'lazy'; if (early) img.fetchPriority = 'high'; img.decoding = 'async'; img.className = 'anomalous-skeleton-shimmer'; img.onload = () => { img.classList.remove('anomalous-skeleton-shimmer'); }; img.onerror = () => { img.classList.remove('anomalous-skeleton-shimmer'); }; img.src = cardPreviewUrl(model.preview_url, this.cardThumbnailMode); card.appendChild(img); } } else { const ph = document.createElement('div'); ph.className = 'anomalous-card-placeholder'; ph.style.display = 'flex'; ph.style.flexDirection = 'column'; ph.style.alignItems = 'center'; ph.style.justifyContent = 'center'; ph.style.height = '100%'; ph.style.minHeight = '180px'; ph.style.color = 'var(--amb-lt-muted, #888)'; ph.style.userSelect = 'none'; ph.innerHTML = `