Add modern UI enhancements over the classic redesign
Layered progressive polish on the warm classic + brass theme: - Card entrance animation on first mount (virtualizer-aware) - Cursor-tracking brass spotlight border on cards - Thumbnail skeleton shimmer until the poster paints - Hover video preview after a short dwell (only when formats resolved) - View Transition + blurred-poster ambient backdrop on player open - Favorite heart pop + expanding ring on add - ⌘K command palette (search, theme, density, reels, source/channel) - Scroll-progress bar + back-to-top FAB - Grid density toggle (comfortable/compact) - Reels HUD: serif title, brass scrubber, muted-state pulse All new motion respects prefers-reduced-motion; no JS/HTML structure changes to the core grid/feed. New glue lives in frontend/js/enhance.js. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -548,6 +548,7 @@ App.videos = App.videos || {};
|
||||
// ---------------------------------------------------------------------
|
||||
App.virtualGrid = (function() {
|
||||
const mounted = new Map(); // loadedVideos index -> card element
|
||||
const revealed = new Set(); // indices that have played their entrance once
|
||||
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
|
||||
@@ -575,7 +576,10 @@ App.videos = App.videos || {};
|
||||
el.style.padding = '0';
|
||||
const inner = el.clientWidth - padX * 2;
|
||||
if (inner <= 0) return false;
|
||||
cols = mobile ? 2 : Math.max(1, Math.floor((inner + gap) / (260 + gap)));
|
||||
// Density toggle (see enhance.js / settings) tunes the minimum card
|
||||
// width, so "compact" packs more columns at the same viewport width.
|
||||
const minCardW = document.body.dataset.density === 'compact' ? 210 : 260;
|
||||
cols = mobile ? 2 : Math.max(1, Math.floor((inner + gap) / (minCardW + gap)));
|
||||
colWidth = (inner - gap * (cols - 1)) / cols;
|
||||
return true;
|
||||
};
|
||||
@@ -682,14 +686,22 @@ App.videos = App.videos || {};
|
||||
if (!v || !l) return;
|
||||
const card = App.videos.buildCard(v);
|
||||
place(card, l);
|
||||
// Entrance animation only the first time an index appears, so cards
|
||||
// don't re-animate every time they scroll back into the window.
|
||||
if (!revealed.has(i)) {
|
||||
revealed.add(i);
|
||||
card.classList.add('is-revealing');
|
||||
card.addEventListener('animationend', () => card.classList.remove('is-revealing'), { once: true });
|
||||
}
|
||||
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.
|
||||
// its true aspect ratio, clear the shimmer, then correct the height.
|
||||
const img = card.querySelector('img');
|
||||
if (img) {
|
||||
const reveal = () => {
|
||||
img.style.aspectRatio = 'auto';
|
||||
img.classList.add('is-loaded');
|
||||
if (mounted.get(i) === card) correct(i);
|
||||
};
|
||||
if (img.complete && img.naturalHeight > 0) requestAnimationFrame(reveal);
|
||||
@@ -807,6 +819,7 @@ App.videos = App.videos || {};
|
||||
const reset = function() {
|
||||
mounted.forEach((card, i) => unmount(i));
|
||||
mounted.clear();
|
||||
revealed.clear(); // new result set should animate in again
|
||||
layout.length = 0;
|
||||
colBottoms = [];
|
||||
colItems = [];
|
||||
@@ -1190,6 +1203,12 @@ App.videos = App.videos || {};
|
||||
return App.videos.buildStreamUrlFromSource(App.videos.resolveStreamSource(videoOrUrl, options));
|
||||
};
|
||||
|
||||
// Lets enhancement layers (e.g. hover preview) recover the video object that
|
||||
// backs a mounted card without reaching into the virtualizer internals.
|
||||
App.videos.getVideoForCard = function(card) {
|
||||
return cardVideo.get(card);
|
||||
};
|
||||
|
||||
App.videos.downloadVideo = function(video) {
|
||||
if (!video) return;
|
||||
const streamUrl = App.videos.buildStreamUrl(video, { applyPreferredQuality: false });
|
||||
|
||||
Reference in New Issue
Block a user