improve tiktok mode

This commit is contained in:
Simon
2026-06-23 21:19:19 +00:00
parent b9ff61244c
commit f5bb33521e
3 changed files with 239 additions and 13 deletions

View File

@@ -539,14 +539,18 @@ App.videos = App.videos || {};
// * mounting/unmounting a card never moves any other card -- positions are
// assigned once and never change -- so there is no scroll jank.
//
// Card heights are deterministic (single-line title, fixed 16:9 thumbnail,
// optional tag/uploader/duration rows), so we measure one card per distinct
// "shape" and reuse that height. Packing is shortest-column-first.
// Thumbnails keep their natural aspect ratio (not cropped), so a card's real
// height isn't known until its image loads. We place each card with a 16:9
// estimate first, then once the image loads we correct that card's height
// and shift only the cards below it *in the same column* (each column is an
// independent vertical stack), so a correction never disturbs other columns
// or anything above it. Columns are assigned once and never change.
// ---------------------------------------------------------------------
App.virtualGrid = (function() {
const mounted = new Map(); // loadedVideos index -> card element
const layout = []; // index -> { top, left, width, height }
const heightCache = new Map(); // shape signature -> measured px height
const layout = []; // index -> { top, left, width, height, col, posInCol }
const heightCache = new Map(); // shape signature -> estimated px height
let colItems = []; // col -> ordered list of item indices in that column
let colBottoms = []; // running bottom y of each column
let cols = 0, colWidth = 0, gap = 16, padX = 24, padY = 24;
let initialized = false;
@@ -582,8 +586,10 @@ App.videos = App.videos || {};
v.uploader ? 1 : 0, (v.duration > 0) ? 1 : 0].join('|');
};
// Height of a card of `v`'s shape at the current column width. Measured
// once per shape via a hidden probe, then cached.
// Estimated height of a card of `v`'s shape at the current column width,
// using the CSS 16:9 thumbnail placeholder (the image isn't loaded in the
// probe). Measured once per shape, then cached. The real height replaces
// this per-card once the thumbnail loads (see correct()).
const heightOf = function(v) {
const sig = signatureOf(v);
const cached = heightCache.get(sig);
@@ -611,10 +617,12 @@ App.videos = App.videos || {};
};
// Assigns positions to items [start, end). Earlier items keep their
// positions because colBottoms carries forward unchanged.
// positions because colBottoms carries forward unchanged. Each item is
// assigned to the currently-shortest column and stays there for good.
const packFrom = function(start) {
if (!cols) { if (!measureMetrics()) return; }
if (!colBottoms.length) colBottoms = new Array(cols).fill(padY);
if (!colItems.length) colItems = Array.from({ length: cols }, () => []);
for (let i = start; i < state.loadedVideos.length; i++) {
const v = state.loadedVideos[i];
const h = heightOf(v);
@@ -627,13 +635,39 @@ App.videos = App.videos || {};
top,
left: padX + col * (colWidth + gap),
width: colWidth,
height: h
height: h,
col,
posInCol: colItems[col].length
};
colItems[col].push(i);
colBottoms[col] = top + h + gap;
}
setContainerHeight();
};
// Replaces item i's estimated height with its real (post-image-load)
// height and slides every card below it in the same column by the delta.
// Other columns and everything above are untouched -> no global reflow.
const correct = function(i) {
const card = mounted.get(i);
const l = layout[i];
if (!card || !l) return;
const real = card.getBoundingClientRect().height;
if (!real || Math.abs(real - l.height) < 1) return;
const delta = real - l.height;
l.height = real;
const list = colItems[l.col];
for (let k = l.posInCol + 1; k < list.length; k++) {
const j = list[k];
layout[j].top += delta;
const mc = mounted.get(j);
if (mc) mc.style.top = layout[j].top + 'px';
}
colBottoms[l.col] += delta;
setContainerHeight();
scheduleUpdate();
};
const place = function(card, l) {
card.style.position = 'absolute';
card.style.top = l.top + 'px';
@@ -650,6 +684,17 @@ App.videos = App.videos || {};
place(card, l);
grid().appendChild(card);
mounted.set(i, card);
// Once the thumbnail loads, drop the 16:9 placeholder so it shows at
// its true aspect ratio, then correct this card's height.
const img = card.querySelector('img');
if (img) {
const reveal = () => {
img.style.aspectRatio = 'auto';
if (mounted.get(i) === card) correct(i);
};
if (img.complete && img.naturalHeight > 0) requestAnimationFrame(reveal);
else img.addEventListener('load', reveal);
}
// Marquee + direct-playability probe only matter for on-screen cards.
requestAnimationFrame(() => { if (mounted.get(i) === card) measureTitle(card); });
probeObserver.observe(card);
@@ -699,6 +744,7 @@ App.videos = App.videos || {};
mounted.forEach((card, i) => unmount(i));
layout.length = 0;
colBottoms = new Array(cols).fill(padY);
colItems = Array.from({ length: cols }, () => []);
packFrom(0);
update();
};
@@ -720,6 +766,7 @@ App.videos = App.videos || {};
mounted.clear();
layout.length = 0;
colBottoms = [];
colItems = [];
const el = grid();
if (el) { el.innerHTML = ''; el.style.height = '0px'; }
};