Discover: fix grid pagination + session cache + responsive pass
Pagination (regression): the IntersectionObserver sentinel only fires on intersection *changes*, so a short first page kept it on-screen and it never re-fired — stuck at 20 — and I'd hidden the Load more button whenever IO exists, leaving no fallback. Now: - Load more button is always shown while there's more (the reliable control). - Auto-load is self-correcting: track sentinel visibility and, after each load, pull again via rAF if it's still on-screen (rAF lets the observer update first so a cached category doesn't load every page at once). - page increment moved inside loadGrid so the button + sentinel can't double-bump. Caching: cachedFetch() memoizes /discover/list responses per URL for the session (rails + grid pages) — revisits, paging and reopening a category are instant and don't re-hit TMDB. Responsive: small-screen pass for the Browse panel, hero (height/title/actions full-width), grid header wrap, and the trailer close button (kept on-screen). Also fixed the ambient layer's 130% width that could cause horizontal scroll.
This commit is contained in:
parent
80d504f94d
commit
246c650db4
2 changed files with 51 additions and 14 deletions
|
|
@ -25,6 +25,8 @@
|
|||
sel: { kind: 'movie', genre: '', decade: '', providers: '', sort: 'popularity.desc' }, // Browse panel
|
||||
};
|
||||
var AUTO = (typeof IntersectionObserver !== 'undefined'); // infinite-scroll capable
|
||||
var sentinelVisible = false;
|
||||
var listCache = {}; // session cache of /discover/list responses, keyed by URL
|
||||
|
||||
function $(s, r) { return (r || document).querySelector(s); }
|
||||
function esc(s) {
|
||||
|
|
@ -34,6 +36,15 @@
|
|||
}
|
||||
function hueOf(s) { var h = 0, t = String(s || ''); for (var i = 0; i < t.length; i++) h = (h * 31 + t.charCodeAt(i)) >>> 0; return h % 360; }
|
||||
function idMap(list) { var m = {}; (list || []).forEach(function (g) { m[(g.name || '').toLowerCase()] = g.id; }); return m; }
|
||||
// Session-memoized GET → JSON for the list endpoint, so revisiting Discover /
|
||||
// paging / reopening a category is instant and doesn't re-hit TMDB. (Ownership
|
||||
// is re-stamped server-side; caching for a session is fine.)
|
||||
function cachedFetch(url) {
|
||||
if (listCache[url]) return Promise.resolve(listCache[url]);
|
||||
return fetch(url, { headers: { Accept: 'application/json' } })
|
||||
.then(function (r) { return r.ok ? r.json() : null; })
|
||||
.then(function (d) { if (d) listCache[url] = d; return d; });
|
||||
}
|
||||
|
||||
// ── the rail stack (personalized + curated + genre + decade) ──────────────
|
||||
var CURATED = [
|
||||
|
|
@ -301,8 +312,7 @@
|
|||
shelf.setAttribute('data-vdsc-loaded', '1');
|
||||
var rail = $('[data-vdsc-rail]', shelf);
|
||||
// 2 pages (~40 items) so a rail still looks full after 'Hide owned'.
|
||||
fetch(LIST_URL + '?' + shelf.getAttribute('data-vdsc-q') + '&pages=2', { headers: { Accept: 'application/json' } })
|
||||
.then(function (r) { return r.ok ? r.json() : null; })
|
||||
cachedFetch(LIST_URL + '?' + shelf.getAttribute('data-vdsc-q') + '&pages=2')
|
||||
.then(function (d) {
|
||||
var items = (d && d.items) || [];
|
||||
if (!items.length) { shelf.remove(); return; } // drop empty shelves
|
||||
|
|
@ -334,11 +344,11 @@
|
|||
var c = state.cat;
|
||||
if (c.busy) return;
|
||||
c.busy = true;
|
||||
if (!reset) c.page++; // advance to the next page here
|
||||
var more = $('[data-vdsc-more]'); if (more && !reset) { more.disabled = true; more.textContent = 'Loading…'; }
|
||||
var ld = reset ? $('[data-vdsc-grid-loading]') : $('[data-vdsc-more-loading]');
|
||||
if (ld) ld.classList.remove('hidden');
|
||||
fetch(LIST_URL + '?' + c.q + '&page=' + c.page, { headers: { Accept: 'application/json' } })
|
||||
.then(function (r) { return r.ok ? r.json() : null; })
|
||||
cachedFetch(LIST_URL + '?' + c.q + '&page=' + c.page)
|
||||
.then(function (d) {
|
||||
c.busy = false;
|
||||
if (ld) ld.classList.add('hidden');
|
||||
|
|
@ -348,9 +358,9 @@
|
|||
var empty = $('[data-vdsc-grid-empty]');
|
||||
if (empty) empty.classList.toggle('hidden', !(reset && !items.length));
|
||||
c.hasMore = c.paginates && items.length >= 18;
|
||||
// With infinite scroll the sentinel pulls more — keep the button only
|
||||
// as a no-IntersectionObserver fallback.
|
||||
if (more) { more.textContent = 'Load more'; more.disabled = false; more.classList.toggle('hidden', AUTO || !c.hasMore); }
|
||||
// Button is always the reliable control; the sentinel auto-loads on top.
|
||||
if (more) { more.textContent = 'Load more'; more.disabled = false; more.classList.toggle('hidden', !c.hasMore); }
|
||||
maybeAutoLoad(); // keep filling while the sentinel stays in view
|
||||
})
|
||||
.catch(function () {
|
||||
c.busy = false;
|
||||
|
|
@ -358,6 +368,15 @@
|
|||
if (more) { more.textContent = 'Load more'; more.disabled = false; }
|
||||
});
|
||||
}
|
||||
// Self-correcting infinite scroll: if the sentinel is still on-screen after a
|
||||
// load (short page), pull the next one. rAF lets the observer update first so
|
||||
// a fully-cached category doesn't load every page at once.
|
||||
function maybeAutoLoad() {
|
||||
if (!(state.mode === 'grid' && sentinelVisible && state.cat.hasMore && !state.cat.busy)) return;
|
||||
requestAnimationFrame(function () {
|
||||
if (state.mode === 'grid' && sentinelVisible && state.cat.hasMore && !state.cat.busy) loadGrid(false);
|
||||
});
|
||||
}
|
||||
function activeChipText(chipset) {
|
||||
var c = $('[data-vdsc-chipset="' + chipset + '"] .vdsc-chip--on');
|
||||
return (c && c.getAttribute('data-val')) ? c.textContent : '';
|
||||
|
|
@ -452,7 +471,7 @@
|
|||
|
||||
var apply = $('[data-vdsc-apply]'); if (apply) apply.addEventListener('click', applyFilter);
|
||||
var clear = $('[data-vdsc-clear]'); if (clear) clear.addEventListener('click', closeCategory);
|
||||
var more = $('[data-vdsc-more]'); if (more) more.addEventListener('click', function () { state.cat.page++; loadGrid(false); });
|
||||
var more = $('[data-vdsc-more]'); if (more) more.addEventListener('click', function () { loadGrid(false); });
|
||||
|
||||
var hide = $('[data-vdsc-hideowned]');
|
||||
if (hide) {
|
||||
|
|
@ -467,9 +486,8 @@
|
|||
var sentinel = $('[data-vdsc-sentinel]');
|
||||
if (sentinel && AUTO) {
|
||||
new IntersectionObserver(function (entries) {
|
||||
if (entries[0].isIntersecting && state.mode === 'grid' && state.cat.hasMore && !state.cat.busy) {
|
||||
state.cat.page++; loadGrid(false);
|
||||
}
|
||||
sentinelVisible = entries[0].isIntersecting;
|
||||
maybeAutoLoad();
|
||||
}, { rootMargin: '600px 0px' }).observe(sentinel);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2250,11 +2250,11 @@ body[data-side="video"] #soulsync-toggle { display: none; }
|
|||
}
|
||||
|
||||
/* ── Discover v2: ambient glow + see-all + grid head + hide-owned + polish ─── */
|
||||
.vdsc-page { position: relative; }
|
||||
.vdsc-page { position: relative; overflow-x: clip; }
|
||||
.vdsc-hero, .vdsc-browse, .vdsc-shelves, .vdsc-grid-wrap { position: relative; z-index: 1; }
|
||||
/* a faint page-top color bleed that follows the current hero slide's hue */
|
||||
.vdsc-amb { position: absolute; top: -40px; left: 50%; transform: translateX(-50%); z-index: 0;
|
||||
width: 130%; height: 580px; pointer-events: none; opacity: 0.5; transition: opacity 0.7s ease;
|
||||
.vdsc-amb { position: absolute; top: -40px; left: 0; z-index: 0;
|
||||
width: 100%; height: 580px; pointer-events: none; opacity: 0.5; transition: opacity 0.7s ease;
|
||||
background: radial-gradient(58% 80% at 50% 0%, hsla(var(--vdsc-amb, 230), 82%, 55%, 0.22), transparent 70%); }
|
||||
|
||||
/* "See all" → opens the rail as a paged grid */
|
||||
|
|
@ -2396,3 +2396,22 @@ body[data-side="video"] #soulsync-toggle { display: none; }
|
|||
.vdsc-btn:focus-visible, .vdsc-hero-trailer:focus-visible, .vdsc-dot:focus-visible, .vdsc-toggle:focus-within,
|
||||
.vdsc-page .vsr-card:focus-visible {
|
||||
outline: 2px solid rgba(var(--accent-rgb, 88 101 242), 0.9); outline-offset: 2px; }
|
||||
|
||||
/* ── small screens ────────────────────────────────────────────────────────── */
|
||||
@media (max-width: 640px) {
|
||||
.vdsc-browse { padding: 13px 13px 15px; margin-bottom: 22px; }
|
||||
.vdsc-browse-top { gap: 10px; }
|
||||
.vdsc-shelf { margin-bottom: 24px; }
|
||||
.vdsc-shelf-title { font-size: 16px; }
|
||||
.vdsc-grid-head { flex-wrap: wrap; gap: 10px; }
|
||||
.vdsc-grid-title { font-size: 18px; }
|
||||
.vdsc-hero { height: 300px; margin-bottom: 20px; }
|
||||
.vdsc-hero-body { left: 18px; right: 18px; bottom: 18px; }
|
||||
.vdsc-hero-title { font-size: 23px; }
|
||||
.vdsc-hero-ov { -webkit-line-clamp: 3; }
|
||||
.vdsc-hero-actions { gap: 9px; }
|
||||
.vdsc-hero-actions .discog-submit-btn, .vdsc-hero-trailer { flex: 1 1 auto; justify-content: center; }
|
||||
.vdsc-dots { right: 16px; bottom: 16px; }
|
||||
/* keep the trailer close button on-screen (it sits above the frame on desktop) */
|
||||
.vdsc-tr-close { top: 6px; right: 6px; background: rgba(0, 0, 0, 0.65); }
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue