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
|
sel: { kind: 'movie', genre: '', decade: '', providers: '', sort: 'popularity.desc' }, // Browse panel
|
||||||
};
|
};
|
||||||
var AUTO = (typeof IntersectionObserver !== 'undefined'); // infinite-scroll capable
|
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 $(s, r) { return (r || document).querySelector(s); }
|
||||||
function esc(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 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; }
|
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) ──────────────
|
// ── the rail stack (personalized + curated + genre + decade) ──────────────
|
||||||
var CURATED = [
|
var CURATED = [
|
||||||
|
|
@ -301,8 +312,7 @@
|
||||||
shelf.setAttribute('data-vdsc-loaded', '1');
|
shelf.setAttribute('data-vdsc-loaded', '1');
|
||||||
var rail = $('[data-vdsc-rail]', shelf);
|
var rail = $('[data-vdsc-rail]', shelf);
|
||||||
// 2 pages (~40 items) so a rail still looks full after 'Hide owned'.
|
// 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' } })
|
cachedFetch(LIST_URL + '?' + shelf.getAttribute('data-vdsc-q') + '&pages=2')
|
||||||
.then(function (r) { return r.ok ? r.json() : null; })
|
|
||||||
.then(function (d) {
|
.then(function (d) {
|
||||||
var items = (d && d.items) || [];
|
var items = (d && d.items) || [];
|
||||||
if (!items.length) { shelf.remove(); return; } // drop empty shelves
|
if (!items.length) { shelf.remove(); return; } // drop empty shelves
|
||||||
|
|
@ -334,11 +344,11 @@
|
||||||
var c = state.cat;
|
var c = state.cat;
|
||||||
if (c.busy) return;
|
if (c.busy) return;
|
||||||
c.busy = true;
|
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 more = $('[data-vdsc-more]'); if (more && !reset) { more.disabled = true; more.textContent = 'Loading…'; }
|
||||||
var ld = reset ? $('[data-vdsc-grid-loading]') : $('[data-vdsc-more-loading]');
|
var ld = reset ? $('[data-vdsc-grid-loading]') : $('[data-vdsc-more-loading]');
|
||||||
if (ld) ld.classList.remove('hidden');
|
if (ld) ld.classList.remove('hidden');
|
||||||
fetch(LIST_URL + '?' + c.q + '&page=' + c.page, { headers: { Accept: 'application/json' } })
|
cachedFetch(LIST_URL + '?' + c.q + '&page=' + c.page)
|
||||||
.then(function (r) { return r.ok ? r.json() : null; })
|
|
||||||
.then(function (d) {
|
.then(function (d) {
|
||||||
c.busy = false;
|
c.busy = false;
|
||||||
if (ld) ld.classList.add('hidden');
|
if (ld) ld.classList.add('hidden');
|
||||||
|
|
@ -348,9 +358,9 @@
|
||||||
var empty = $('[data-vdsc-grid-empty]');
|
var empty = $('[data-vdsc-grid-empty]');
|
||||||
if (empty) empty.classList.toggle('hidden', !(reset && !items.length));
|
if (empty) empty.classList.toggle('hidden', !(reset && !items.length));
|
||||||
c.hasMore = c.paginates && items.length >= 18;
|
c.hasMore = c.paginates && items.length >= 18;
|
||||||
// With infinite scroll the sentinel pulls more — keep the button only
|
// Button is always the reliable control; the sentinel auto-loads on top.
|
||||||
// as a no-IntersectionObserver fallback.
|
if (more) { more.textContent = 'Load more'; more.disabled = false; more.classList.toggle('hidden', !c.hasMore); }
|
||||||
if (more) { more.textContent = 'Load more'; more.disabled = false; more.classList.toggle('hidden', AUTO || !c.hasMore); }
|
maybeAutoLoad(); // keep filling while the sentinel stays in view
|
||||||
})
|
})
|
||||||
.catch(function () {
|
.catch(function () {
|
||||||
c.busy = false;
|
c.busy = false;
|
||||||
|
|
@ -358,6 +368,15 @@
|
||||||
if (more) { more.textContent = 'Load more'; more.disabled = false; }
|
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) {
|
function activeChipText(chipset) {
|
||||||
var c = $('[data-vdsc-chipset="' + chipset + '"] .vdsc-chip--on');
|
var c = $('[data-vdsc-chipset="' + chipset + '"] .vdsc-chip--on');
|
||||||
return (c && c.getAttribute('data-val')) ? c.textContent : '';
|
return (c && c.getAttribute('data-val')) ? c.textContent : '';
|
||||||
|
|
@ -452,7 +471,7 @@
|
||||||
|
|
||||||
var apply = $('[data-vdsc-apply]'); if (apply) apply.addEventListener('click', applyFilter);
|
var apply = $('[data-vdsc-apply]'); if (apply) apply.addEventListener('click', applyFilter);
|
||||||
var clear = $('[data-vdsc-clear]'); if (clear) clear.addEventListener('click', closeCategory);
|
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]');
|
var hide = $('[data-vdsc-hideowned]');
|
||||||
if (hide) {
|
if (hide) {
|
||||||
|
|
@ -467,9 +486,8 @@
|
||||||
var sentinel = $('[data-vdsc-sentinel]');
|
var sentinel = $('[data-vdsc-sentinel]');
|
||||||
if (sentinel && AUTO) {
|
if (sentinel && AUTO) {
|
||||||
new IntersectionObserver(function (entries) {
|
new IntersectionObserver(function (entries) {
|
||||||
if (entries[0].isIntersecting && state.mode === 'grid' && state.cat.hasMore && !state.cat.busy) {
|
sentinelVisible = entries[0].isIntersecting;
|
||||||
state.cat.page++; loadGrid(false);
|
maybeAutoLoad();
|
||||||
}
|
|
||||||
}, { rootMargin: '600px 0px' }).observe(sentinel);
|
}, { 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 ─── */
|
/* ── 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; }
|
.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 */
|
/* 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;
|
.vdsc-amb { position: absolute; top: -40px; left: 0; z-index: 0;
|
||||||
width: 130%; height: 580px; pointer-events: none; opacity: 0.5; transition: opacity 0.7s ease;
|
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%); }
|
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 */
|
/* "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-btn:focus-visible, .vdsc-hero-trailer:focus-visible, .vdsc-dot:focus-visible, .vdsc-toggle:focus-within,
|
||||||
.vdsc-page .vsr-card:focus-visible {
|
.vdsc-page .vsr-card:focus-visible {
|
||||||
outline: 2px solid rgba(var(--accent-rgb, 88 101 242), 0.9); outline-offset: 2px; }
|
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