diff --git a/webui/index.html b/webui/index.html index 4c6ec812..1ee0f652 100644 --- a/webui/index.html +++ b/webui/index.html @@ -1055,8 +1055,13 @@

Filmography

-
+
+
+
+
+
+ diff --git a/webui/static/video/video-person.js b/webui/static/video/video-person.js index b8b06531..2e8f0b22 100644 --- a/webui/static/video/video-person.js +++ b/webui/static/video/video-person.js @@ -16,7 +16,8 @@ var PERSON_URL = '/api/video/person/'; var data = null; var currentId = null; - var tab = 'all'; // all | movie | show + var tab = 'all'; // kind filter: all | movie | show + var own = 'all'; // ownership filter: all | owned | missing var ROLE_NOUNS = { Acting: 'Actor', Directing: 'Director', Writing: 'Writer', Production: 'Producer', Sound: 'Composer', Camera: 'Cinematographer', Editing: 'Editor', Creator: 'Creator', @@ -53,39 +54,77 @@ '' + esc(sub) + ''; } + function tabBtn(attr, key, active, label, count) { + return ''; + } + function renderTabs() { var host = q('[data-vp-tabs]'); if (!host || !data) return; - var credits = data.credits || []; - var movies = credits.filter(function (c) { return c.kind === 'movie'; }).length; - var shows = credits.filter(function (c) { return c.kind === 'show'; }).length; - var defs = [['all', 'All', credits.length], ['movie', 'Movies', movies], ['show', 'TV', shows]]; - host.innerHTML = defs.filter(function (d) { return d[2] > 0; }).map(function (d) { - return ''; + // Counts reflect the CURRENT ownership filter (so the numbers match what + // you'd actually see). + var base = (data.credits || []).filter(function (c) { return matchOwn(c, own); }); + var movies = base.filter(function (c) { return c.kind === 'movie'; }).length; + var shows = base.filter(function (c) { return c.kind === 'show'; }).length; + var defs = [['all', 'All', base.length], ['movie', 'Movies', movies], ['show', 'TV', shows]]; + host.innerHTML = defs.filter(function (d) { return d[2] > 0 || d[0] === 'all'; }).map(function (d) { + return tabBtn('data-vp-tab', d[0], d[0] === tab, d[1], d[2]); }).join(''); } + function renderOwn() { + var host = q('[data-vp-own]'); + if (!host || !data) return; + // Counts reflect the current KIND filter. + var base = (data.credits || []).filter(function (c) { return matchKind(c, tab); }); + var owned = base.filter(isOwned).length; + var defs = [['all', 'All', base.length], ['owned', 'In Library', owned], + ['missing', 'Missing', base.length - owned]]; + host.innerHTML = defs.map(function (d) { + return tabBtn('data-vp-own', d[0], d[0] === own, d[1], d[2]); + }).join(''); + } + + function applyFilters() { + renderTabs(); renderOwn(); renderKnownFor(); renderCredits(); + } + + // ── filters (kind + ownership) ──────────────────────────────────────────── + function matchKind(c, k) { return k === 'all' || c.kind === k; } + function isOwned(c) { return c.library_id != null; } + function matchOwn(c, o) { return o === 'all' || (o === 'owned' ? isOwned(c) : !isOwned(c)); } + function filtered() { + return (data.credits || []).filter(function (c) { return matchKind(c, tab) && matchOwn(c, own); }); + } + function renderKnownFor() { var section = q('[data-vp-known-section]'), host = q('[data-vp-known]'); if (!section || !host || !data) return; - // The credits arrive popularity-sorted → the top few are the "known for". - var top = (data.credits || []).slice(0, 10); + // Credits arrive popularity-sorted → the top few of the FILTERED set are + // the "known for" (so it tracks the owned/missing + kind filters too). + var top = filtered().slice(0, 10); section.hidden = top.length < 3; // only worth a rail if there are a few host.innerHTML = top.map(creditCard).join(''); } function renderCredits() { - var host = q('[data-vp-credits]'); + var host = q('[data-vp-credits]'), empty = q('[data-vp-credits-empty]'); if (!host || !data) return; - var credits = (data.credits || []).filter(function (c) { - return tab === 'all' || c.kind === tab; - }); + var credits = filtered(); // Full filmography reads best chronologically (newest first); Known For // already covers the popular ones. credits.sort(function (a, b) { return (b.date || '').localeCompare(a.date || ''); }); host.innerHTML = credits.map(creditCard).join(''); + if (empty) { + empty.hidden = credits.length > 0; + if (!credits.length) { + empty.textContent = own === 'owned' ? 'Nothing from this person in your library yet.' + : own === 'missing' ? 'You already have everything here. 🎉' + : 'No titles here.'; + } + } } function lifespan(d) { @@ -96,7 +135,7 @@ } function render(d) { - data = d; tab = 'all'; + data = d; tab = 'all'; own = 'all'; var photo = q('[data-vp-photo]'), ph = q('[data-vp-photo-ph]'); if (photo) { if (d.photo) { @@ -130,7 +169,7 @@ if (bio) { bio.textContent = d.biography || ''; bio.hidden = !d.biography; bio.classList.remove('vp-bio--open'); } if (more) { more.hidden = !((d.biography || '').length > 320); more.textContent = 'Read more'; } - renderKnownFor(); renderTabs(); renderCredits(); + applyFilters(); var sub = document.querySelector('.video-subpage[data-video-subpage="video-person-detail"]'); if (sub) sub.scrollTop = 0; } @@ -143,6 +182,8 @@ var m = q('[data-vp-meta]'); if (m) m.innerHTML = ''; var c = q('[data-vp-credits]'); if (c) c.innerHTML = ''; var t = q('[data-vp-tabs]'); if (t) t.innerHTML = ''; + var o = q('[data-vp-own]'); if (o) o.innerHTML = ''; + var ce = q('[data-vp-credits-empty]'); if (ce) ce.hidden = true; var ks = q('[data-vp-known-section]'); if (ks) ks.hidden = true; var k = q('[data-vp-known]'); if (k) k.innerHTML = ''; fetch(PERSON_URL + id, { headers: { 'Accept': 'application/json' } }) @@ -163,9 +204,13 @@ function onClick(e) { var r = root(); if (!r) return; - var tabBtn = e.target.closest('[data-vp-tab]'); - if (tabBtn && r.contains(tabBtn)) { - tab = tabBtn.getAttribute('data-vp-tab'); renderTabs(); renderCredits(); return; + var kindBtn = e.target.closest('[data-vp-tab]'); + if (kindBtn && r.contains(kindBtn)) { + tab = kindBtn.getAttribute('data-vp-tab'); applyFilters(); return; + } + var ownBtn = e.target.closest('[data-vp-own]'); + if (ownBtn && r.contains(ownBtn)) { + own = ownBtn.getAttribute('data-vp-own'); applyFilters(); return; } var moreBtn = e.target.closest('[data-vp-bio-more]'); if (moreBtn && r.contains(moreBtn)) { diff --git a/webui/static/video/video-side.css b/webui/static/video/video-side.css index 75056088..c5d8eed7 100644 --- a/webui/static/video/video-side.css +++ b/webui/static/video/video-side.css @@ -1093,7 +1093,21 @@ body[data-side="video"] .dashboard-header-sweep { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 14px; margin-bottom: 20px; } .vp-credits-title { font-size: 22px; font-weight: 800; color: #fff; margin: 0; } +.vp-credit-filters { display: flex; flex-wrap: wrap; align-items: center; gap: 10px 14px; } .vp-credit-tabs { display: flex; gap: 8px; } +/* The ownership group sits after a faint divider so it reads as a second axis. */ +.vp-own-tabs { position: relative; padding-left: 14px; } +.vp-own-tabs::before { + content: ''; position: absolute; left: 0; top: 14%; height: 72%; width: 1px; + background: rgba(255, 255, 255, 0.16); +} +/* "In Library" filter glows green (matches the owned ribbon) when active. */ +.vp-own-tabs [data-vp-own="owned"].vp-tab--active { + background: rgba(34, 197, 94, 0.92); box-shadow: 0 4px 16px rgba(34, 197, 94, 0.4); +} +.vp-credits-empty { + padding: 44px 20px; text-align: center; font-size: 14px; color: rgba(255, 255, 255, 0.55); +} .vp-tab { background: rgba(255, 255, 255, 0.06); border: 1px solid rgba(255, 255, 255, 0.1); color: rgba(255, 255, 255, 0.7); border-radius: 999px; padding: 7px 16px; cursor: pointer;