Discover: sliding segmented pills + chip polish + no-TMDB state

- Segmented controls (Kind/Sort) now have a highlight 'thumb' that springs
  between options (JS measures the active button → CSS var slide); repositions
  on click, page-show, and resize.
- Chips: brighter active gradient with a glow ring + subtle lift, smoother hover.
- No-TMDB empty state: genres are a static TMDB endpoint, so when they come back
  empty the page shows a 'Discover needs TMDB' card instead of a bare shell.
- Persist 'Hide owned' across sessions (localStorage); async image decoding on
  cards.
This commit is contained in:
BoulderBadgeDad 2026-06-16 14:21:59 -07:00
parent 8043db0ad4
commit 5aa792d399
3 changed files with 71 additions and 18 deletions

View file

@ -904,6 +904,11 @@
</div>
</div>
<div class="vdsc-shelves" data-vdsc-shelves></div>
<div class="vdsc-empty hidden" data-vdsc-empty>
<div class="vdsc-empty-ic">&#127909;</div>
<div class="vdsc-empty-title">Discover needs TMDB</div>
<div class="vdsc-empty-sub">Add a free TMDB API key in <strong>Settings &rarr; Enrichment</strong> to browse movies &amp; shows you don&rsquo;t own yet.</div>
</div>
<div class="vdsc-grid-wrap hidden" data-vdsc-grid-wrap>
<div class="vdsc-grid-head">
<button class="vdsc-btn vdsc-btn--ghost" type="button" data-vdsc-clear>&larr; Discover</button>

View file

@ -79,7 +79,7 @@
function card(it) {
var fallback = it.kind === 'movie' ? '🎬' : '📺';
var img = it.poster
? '<img src="' + esc(it.poster) + '" alt="" loading="lazy" ' +
? '<img src="' + esc(it.poster) + '" alt="" loading="lazy" decoding="async" ' +
'onerror="this.outerHTML=\'<div class=&quot;vsr-poster-ph&quot;>' + fallback + '</div>\'">'
: '<div class="vsr-poster-ph">' + fallback + '</div>';
var owned = it.library_id != null;
@ -290,6 +290,18 @@
var all = box.querySelectorAll(selector);
for (var i = 0; i < all.length; i++) all[i].classList.toggle(onClass, all[i] === el);
}
// Slide the segmented-control highlight under the active button.
function moveSeg(box) {
if (!box) return;
var on = box.querySelector('.vdsc-seg-btn--on');
if (!on || !on.offsetWidth) return; // hidden / not laid out yet
box.style.setProperty('--seg-x', on.offsetLeft + 'px');
box.style.setProperty('--seg-w', on.offsetWidth + 'px');
}
function positionSegs() {
var segs = document.querySelectorAll('[data-video-subpage="' + PAGE_ID + '"] .vdsc-seg');
for (var i = 0; i < segs.length; i++) moveSeg(segs[i]);
}
// ── wiring ────────────────────────────────────────────────────────────────
function wire() {
@ -321,6 +333,7 @@
var sbox = seg.closest('[data-vdsc-seg]');
var which = sbox.getAttribute('data-vdsc-seg');
setActive(sbox, seg, '.vdsc-seg-btn', 'vdsc-seg-btn--on');
moveSeg(sbox);
state.sel[which] = seg.getAttribute('data-val');
if (which === 'kind') renderGenreChips(); // genres differ by kind
return;
@ -350,7 +363,15 @@
var more = $('[data-vdsc-more]'); if (more) more.addEventListener('click', function () { state.cat.page++; loadGrid(false); });
var hide = $('[data-vdsc-hideowned]');
if (hide) hide.addEventListener('change', function () { page.classList.toggle('vdsc-hide-owned', hide.checked); });
if (hide) {
try { if (localStorage.getItem('vdsc_hideowned') === '1') hide.checked = true; } catch (e) { /* ignore */ }
page.classList.toggle('vdsc-hide-owned', hide.checked);
hide.addEventListener('change', function () {
page.classList.toggle('vdsc-hide-owned', hide.checked);
try { localStorage.setItem('vdsc_hideowned', hide.checked ? '1' : '0'); } catch (e) { /* ignore */ }
});
}
window.addEventListener('resize', positionSegs);
}
function loadMeta() {
@ -361,10 +382,19 @@
var g = res[0] || {}, t = res[1] || {};
state.genres = { movie: g.movie || [], show: g.show || [] };
state.taste = { movie: t.movie || [], show: t.show || [] };
// Genres are a static TMDB endpoint — empty means TMDB isn't set up.
if (!state.genres.movie.length && !state.genres.show.length) { showEmpty(); return; }
renderGenreChips();
renderShelves();
requestAnimationFrame(positionSegs);
});
}
function showEmpty() {
var e = $('[data-vdsc-empty]'); if (e) e.classList.remove('hidden');
var b = $('[data-video-subpage="' + PAGE_ID + '"] .vdsc-browse'); if (b) b.classList.add('hidden');
var sh = $('[data-vdsc-shelves]'); if (sh) sh.classList.add('hidden');
var h = $('[data-vdsc-hero]'); if (h) h.classList.add('hidden');
}
function load() {
if (state.loaded) return;
state.loaded = true;
@ -374,7 +404,7 @@
function onShown(e) {
if (!e) return;
if (e.detail === PAGE_ID) { wire(); load(); startHeroTimer(); }
if (e.detail === PAGE_ID) { wire(); load(); startHeroTimer(); requestAnimationFrame(positionSegs); }
else stopHeroTimer(); // left the page → stop the slideshow
}
function init() { document.addEventListener('soulsync:video-page-shown', onShown); }

View file

@ -2294,16 +2294,20 @@ body[data-side="video"] #soulsync-toggle { display: none; }
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05), 0 18px 50px -30px rgba(var(--accent-rgb, 88 101 242), 0.5); }
.vdsc-browse-top { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
/* segmented control (pill of buttons with one active) */
.vdsc-seg { display: inline-flex; padding: 3px; gap: 2px; border-radius: 11px;
background: rgba(0, 0, 0, 0.28); border: 1px solid rgba(255, 255, 255, 0.07); }
.vdsc-seg-btn { padding: 8px 16px; border: none; background: none; cursor: pointer; border-radius: 9px;
font-size: 13px; font-weight: 700; color: rgba(255, 255, 255, 0.58); white-space: nowrap;
transition: color 0.15s ease, background 0.15s ease, box-shadow 0.2s ease; }
/* segmented control — a highlight "thumb" slides under the active button */
.vdsc-seg { position: relative; display: inline-flex; padding: 4px; gap: 2px; border-radius: 12px;
background: rgba(0, 0, 0, 0.32); border: 1px solid rgba(255, 255, 255, 0.07);
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.4); }
.vdsc-seg::before { content: ''; position: absolute; top: 4px; bottom: 4px; left: 0; z-index: 0;
width: var(--seg-w, 0); transform: translateX(var(--seg-x, 0)); border-radius: 9px;
background: linear-gradient(135deg, rgba(var(--accent-rgb, 88 101 242), 0.98), rgba(var(--accent-rgb, 88 101 242), 0.72));
box-shadow: 0 7px 20px -7px rgba(var(--accent-rgb, 88 101 242), 0.85), inset 0 1px 0 rgba(255, 255, 255, 0.3);
transition: transform 0.32s cubic-bezier(0.34, 1.35, 0.5, 1), width 0.32s cubic-bezier(0.34, 1.35, 0.5, 1); }
.vdsc-seg-btn { position: relative; z-index: 1; padding: 8px 17px; border: none; background: none; cursor: pointer;
border-radius: 9px; font-size: 13px; font-weight: 700; color: rgba(255, 255, 255, 0.55); white-space: nowrap;
transition: color 0.18s ease; }
.vdsc-seg-btn:hover { color: rgba(255, 255, 255, 0.92); }
.vdsc-seg-btn--on { color: #fff;
background: linear-gradient(135deg, rgba(var(--accent-rgb, 88 101 242), 0.95), rgba(var(--accent-rgb, 88 101 242), 0.72));
box-shadow: 0 6px 18px -6px rgba(var(--accent-rgb, 88 101 242), 0.7); }
.vdsc-seg-btn--on { color: #fff; }
/* chip rows (genres, eras) — horizontally scrollable, edge-faded */
.vdsc-chips { display: flex; gap: 8px; overflow-x: auto; padding: 4px 0 6px; margin-top: 14px;
@ -2313,14 +2317,15 @@ body[data-side="video"] #soulsync-toggle { display: none; }
.vdsc-chips::-webkit-scrollbar { height: 6px; }
.vdsc-chips::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.13); border-radius: 3px; }
.vdsc-chips--era { margin-top: 0; }
.vdsc-chip { flex: 0 0 auto; padding: 7px 14px; border-radius: 999px; cursor: pointer; white-space: nowrap;
.vdsc-chip { flex: 0 0 auto; padding: 7px 15px; border-radius: 999px; cursor: pointer; white-space: nowrap;
font-size: 12.5px; font-weight: 700; color: rgba(255, 255, 255, 0.66);
background: rgba(255, 255, 255, 0.05); border: 1px solid rgba(255, 255, 255, 0.1);
transition: color 0.15s ease, background 0.15s ease, border-color 0.15s ease, box-shadow 0.2s ease; }
.vdsc-chip:hover { color: #fff; background: rgba(255, 255, 255, 0.1); border-color: rgba(255, 255, 255, 0.2); }
.vdsc-chip--on { color: #fff; border-color: transparent;
background: linear-gradient(135deg, rgba(var(--accent-rgb, 88 101 242), 0.95), rgba(var(--accent-rgb, 88 101 242), 0.7));
box-shadow: 0 8px 22px -8px rgba(var(--accent-rgb, 88 101 242), 0.8); }
transition: color 0.15s ease, background 0.15s ease, border-color 0.15s ease, box-shadow 0.2s ease, transform 0.18s cubic-bezier(0.34, 1.4, 0.5, 1); }
.vdsc-chip:hover { color: #fff; background: rgba(255, 255, 255, 0.1); border-color: rgba(255, 255, 255, 0.22); transform: translateY(-1px); }
.vdsc-chip--on { color: #fff; border-color: transparent; transform: translateY(-1px);
background: linear-gradient(135deg, rgba(var(--accent-rgb, 88 101 242), 0.98), rgba(var(--accent-rgb, 88 101 242), 0.68));
box-shadow: 0 9px 22px -8px rgba(var(--accent-rgb, 88 101 242), 0.85),
0 0 0 1px rgba(var(--accent-rgb, 88 101 242), 0.45), inset 0 1px 0 rgba(255, 255, 255, 0.28); }
.vdsc-browse-bottom { display: flex; align-items: center; gap: 16px; margin-top: 12px; }
.vdsc-browse-bottom .vdsc-chips { flex: 1; min-width: 0; margin-top: 0; }
@ -2333,3 +2338,16 @@ body[data-side="video"] #soulsync-toggle { display: none; }
.vdsc-browse-bottom { flex-direction: column; align-items: stretch; }
.vdsc-browse-go { width: 100%; }
}
/* no-TMDB empty state */
.vdsc-empty { text-align: center; padding: 86px 24px; }
.vdsc-empty.hidden { display: none; }
.vdsc-empty-ic { font-size: 46px; margin-bottom: 14px; opacity: 0.85; }
.vdsc-empty-title { font-size: 21px; font-weight: 800; color: #fff; margin-bottom: 9px; }
.vdsc-empty-sub { font-size: 14px; line-height: 1.6; color: rgba(255, 255, 255, 0.58); max-width: 440px; margin: 0 auto; }
.vdsc-empty-sub strong { color: rgba(255, 255, 255, 0.82); font-weight: 700; }
@media (prefers-reduced-motion: reduce) {
.vdsc-seg::before { transition: none; }
.vdsc-chip, .vdsc-chip:hover, .vdsc-chip--on { transition: color 0.15s ease, background 0.15s ease; transform: none; }
}