add virtual scrolling — only render visible cards, massive memory reduction
This commit is contained in:
parent
075f585612
commit
f8a5f9aa6d
118
index.html
118
index.html
@ -177,6 +177,12 @@ let currentLibrary = null;
|
||||
let currentFolder = null; // null = all
|
||||
let currentImageId = null;
|
||||
|
||||
// --- Virtual scroller state ---
|
||||
const OVERSCAN = 2; // extra rows to render above/below viewport
|
||||
let vCols = 1;
|
||||
let vRowHeight = 0;
|
||||
let vScrollRAF = null;
|
||||
|
||||
// Init: load library list
|
||||
async function init() {
|
||||
const res = await fetch('/api/libraries');
|
||||
@ -274,6 +280,7 @@ function filterImages() {
|
||||
function renderGrid() {
|
||||
const grid = document.getElementById('grid');
|
||||
const empty = document.getElementById('empty');
|
||||
const container = document.getElementById('grid-container');
|
||||
const stats = document.getElementById('stats');
|
||||
stats.textContent = `${filteredImages.length} items`;
|
||||
|
||||
@ -286,35 +293,95 @@ function renderGrid() {
|
||||
|
||||
empty.style.display = 'none';
|
||||
grid.style.display = 'grid';
|
||||
grid.innerHTML = '';
|
||||
container.scrollTop = 0;
|
||||
|
||||
filteredImages.forEach(img => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'card';
|
||||
card.onclick = () => openLightbox(img);
|
||||
// Measure column count and row height from CSS grid
|
||||
// We do one measurement pass with a dummy card first
|
||||
function measureGrid() {
|
||||
const thumbSize = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--thumb-size')) || 180;
|
||||
const gridWidth = grid.clientWidth || container.clientWidth - 32;
|
||||
vCols = Math.max(1, Math.floor((gridWidth + 12) / (thumbSize + 12)));
|
||||
// Card height: thumb aspect 16/10 + info ~60px
|
||||
vRowHeight = Math.round((thumbSize * 10 / 16)) + 62 + 12; // thumb-h + info + gap
|
||||
}
|
||||
measureGrid();
|
||||
|
||||
const thumbSrc = img._thumbnail
|
||||
? `/api/thumbnail/${currentLibrary}/${img.id}`
|
||||
: null;
|
||||
// Detach scroll listener before wiping grid
|
||||
container.onscroll = null;
|
||||
|
||||
const extIcon = extToIcon(img.ext);
|
||||
function renderVisible() {
|
||||
measureGrid();
|
||||
const totalRows = Math.ceil(filteredImages.length / vCols);
|
||||
const totalHeight = totalRows * vRowHeight;
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="thumb-wrap">
|
||||
${thumbSrc
|
||||
? `<img src="${thumbSrc}" loading="lazy" alt="${escHtml(img.name)}" onerror="this.style.display='none';this.nextElementSibling.style.display='flex'">`
|
||||
: ''}
|
||||
<span class="thumb-placeholder" style="${thumbSrc ? 'display:none' : ''}">${extIcon}</span>
|
||||
<span class="ext-badge">${img.ext || '?'}</span>
|
||||
</div>
|
||||
<div class="card-info">
|
||||
<div class="card-name" title="${escHtml(img.name)}">${escHtml(img.name)}</div>
|
||||
<div class="card-meta">${img.ext?.toUpperCase() || ''} ${img.duration ? '· ' + fmtDuration(img.duration) : ''}</div>
|
||||
${img.tags?.length ? `<div class="card-tags">${img.tags.map(t => `<span class="tag">${escHtml(t)}</span>`).join('')}</div>` : ''}
|
||||
</div>
|
||||
`;
|
||||
grid.appendChild(card);
|
||||
});
|
||||
const scrollTop = container.scrollTop;
|
||||
const viewHeight = container.clientHeight;
|
||||
|
||||
const firstVisRow = Math.max(0, Math.floor(scrollTop / vRowHeight) - OVERSCAN);
|
||||
const lastVisRow = Math.min(totalRows - 1, Math.ceil((scrollTop + viewHeight) / vRowHeight) + OVERSCAN);
|
||||
|
||||
const firstIdx = firstVisRow * vCols;
|
||||
const lastIdx = Math.min(filteredImages.length - 1, (lastVisRow + 1) * vCols - 1);
|
||||
|
||||
// Spacer above
|
||||
const topH = firstVisRow * vRowHeight;
|
||||
// Spacer below
|
||||
const bottomH = Math.max(0, (totalRows - lastVisRow - 1) * vRowHeight);
|
||||
|
||||
grid.innerHTML = '';
|
||||
|
||||
// Top spacer
|
||||
if (topH > 0) {
|
||||
const spacer = document.createElement('div');
|
||||
spacer.style.cssText = `grid-column: 1 / -1; height: ${topH}px; pointer-events: none;`;
|
||||
grid.appendChild(spacer);
|
||||
}
|
||||
|
||||
// Render only visible cards
|
||||
for (let i = firstIdx; i <= lastIdx; i++) {
|
||||
const img = filteredImages[i];
|
||||
const card = document.createElement('div');
|
||||
card.className = 'card';
|
||||
card.onclick = () => openLightbox(img);
|
||||
|
||||
const thumbSrc = img._thumbnail ? `/api/thumbnail/${currentLibrary}/${img.id}` : null;
|
||||
const extIcon = extToIcon(img.ext);
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="thumb-wrap">
|
||||
${thumbSrc
|
||||
? `<img src="${thumbSrc}" alt="${escHtml(img.name)}" onerror="this.style.display='none';this.nextElementSibling.style.display='flex'">`
|
||||
: ''}
|
||||
<span class="thumb-placeholder" style="${thumbSrc ? 'display:none' : ''}">${extIcon}</span>
|
||||
<span class="ext-badge">${img.ext || '?'}</span>
|
||||
</div>
|
||||
<div class="card-info">
|
||||
<div class="card-name" title="${escHtml(img.name)}">${escHtml(img.name)}</div>
|
||||
<div class="card-meta">${img.ext?.toUpperCase() || ''} ${img.duration ? '· ' + fmtDuration(img.duration) : ''}</div>
|
||||
${img.tags?.length ? `<div class="card-tags">${img.tags.map(t => `<span class="tag">${escHtml(t)}</span>`).join('')}</div>` : ''}
|
||||
</div>
|
||||
`;
|
||||
grid.appendChild(card);
|
||||
}
|
||||
|
||||
// Bottom spacer
|
||||
if (bottomH > 0) {
|
||||
const spacer = document.createElement('div');
|
||||
spacer.style.cssText = `grid-column: 1 / -1; height: ${bottomH}px; pointer-events: none;`;
|
||||
grid.appendChild(spacer);
|
||||
}
|
||||
}
|
||||
|
||||
renderVisible();
|
||||
|
||||
// Re-render on scroll (throttled via rAF)
|
||||
container.onscroll = () => {
|
||||
if (vScrollRAF) return;
|
||||
vScrollRAF = requestAnimationFrame(() => {
|
||||
vScrollRAF = null;
|
||||
renderVisible();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function openLightbox(img) {
|
||||
@ -359,6 +426,7 @@ document.addEventListener('keydown', e => { if (e.key === 'Escape') closeLightbo
|
||||
|
||||
function updateThumbSize(val) {
|
||||
document.documentElement.style.setProperty('--thumb-size', val + 'px');
|
||||
if (filteredImages.length > 0) renderGrid();
|
||||
}
|
||||
|
||||
function extToIcon(ext) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user