video: person page — photos gallery + lightbox + 'also known as'

person() now returns profile images (thumb+full) and also_known_as. The person
page shows an 'Also known as' line and a Photos rail that opens the shared
fullscreen lightbox (arrows/Esc/counter).
This commit is contained in:
BoulderBadgeDad 2026-06-15 11:49:43 -07:00
parent 8bbdd712f9
commit df8f6a2acd
4 changed files with 92 additions and 2 deletions

View file

@ -511,7 +511,8 @@ class TMDBClient:
return None
import requests
r = requests.get(self.BASE + "/person/" + str(tmdb_id), params={
"api_key": self.api_key, "append_to_response": "combined_credits,external_ids"}, timeout=15)
"api_key": self.api_key,
"append_to_response": "combined_credits,external_ids,images"}, timeout=15)
r.raise_for_status()
d = r.json() or {}
if not d.get("id"):
@ -542,6 +543,10 @@ class TMDBClient:
for c in (cc.get("crew") or []):
add(c, c.get("department") or "Crew", c.get("job") or None)
credits.sort(key=lambda x: x["popularity"], reverse=True)
profiles = (d.get("images") or {}).get("profiles") or []
photos = [{"thumb": self.PROFILE + p["file_path"], "full": self.IMG + p["file_path"]}
for p in profiles[:16] if p.get("file_path")]
akas = [a for a in (d.get("also_known_as") or []) if a][:6]
return {
"tmdb_id": d.get("id"), "name": d.get("name"),
"biography": d.get("biography") or None,
@ -549,7 +554,7 @@ class TMDBClient:
"birthday": d.get("birthday") or None, "deathday": d.get("deathday") or None,
"place_of_birth": d.get("place_of_birth") or None,
"photo": (self.PROFILE + d["profile_path"]) if d.get("profile_path") else None,
"credits": credits}
"photos": photos, "also_known_as": akas, "credits": credits}
class TVDBClient:

View file

@ -1111,8 +1111,13 @@
</div>
</div>
<div class="vp-body">
<p class="vp-aka" data-vp-aka hidden></p>
<p class="vp-bio" data-vp-bio></p>
<button class="vp-bio-more" type="button" data-vp-bio-more hidden>Read more</button>
<div class="vp-photos-section" data-vp-photos-section hidden>
<h2 class="vp-credits-title vp-known-title">Photos</h2>
<div class="vp-photos" data-vp-photos></div>
</div>
<div class="vp-known-section" data-vp-known-section hidden>
<h2 class="vp-credits-title vp-known-title">Known For</h2>
<div class="vp-known-rail" data-vp-known></div>

View file

@ -190,6 +190,53 @@
img.src = '/api/video/img?u=' + encodeURIComponent(photoUrl);
}
// ── photos gallery + lightbox (reuses the shared .vd-lightbox styles) ──────
var photoFulls = [], lbIdx = 0;
function renderPhotos(photos) {
var sec = q('[data-vp-photos-section]'), host = q('[data-vp-photos]');
photos = photos || [];
photoFulls = photos.map(function (p) { return p.full; });
if (!sec || !host) return;
if (!photos.length) { sec.hidden = true; return; }
sec.hidden = false;
host.innerHTML = photos.map(function (p, i) {
return '<button class="vp-photo-thumb" type="button" data-vp-shot="' + i + '">' +
'<img src="' + esc(p.thumb) + '" alt="" loading="lazy"></button>';
}).join('');
}
function openLightbox(idx) {
if (!photoFulls.length) return;
lbIdx = idx;
var ov = document.getElementById('vp-lightbox');
if (!ov) {
ov = document.createElement('div'); ov.id = 'vp-lightbox'; ov.className = 'vd-lightbox';
ov.addEventListener('click', function (e) {
if (e.target.closest('[data-vp-lb-prev]')) lbStep(-1);
else if (e.target.closest('[data-vp-lb-next]')) lbStep(1);
else if (e.target === ov || e.target.closest('[data-vp-lb-close]')) closeLightbox();
});
document.body.appendChild(ov);
}
renderLb(); ov.classList.add('vd-lightbox--open');
}
function renderLb() {
var ov = document.getElementById('vp-lightbox'); if (!ov) return;
ov.innerHTML = '<button class="vd-lb-close" type="button" data-vp-lb-close aria-label="Close">&times;</button>' +
'<button class="vd-lb-nav vd-lb-prev" type="button" data-vp-lb-prev aria-label="Previous">&lsaquo;</button>' +
'<img class="vd-lb-img" src="' + esc(photoFulls[lbIdx]) + '" alt="">' +
'<button class="vd-lb-nav vd-lb-next" type="button" data-vp-lb-next aria-label="Next">&rsaquo;</button>' +
'<div class="vd-lb-count">' + (lbIdx + 1) + ' / ' + photoFulls.length + '</div>';
}
function lbStep(dir) { lbIdx = (lbIdx + dir + photoFulls.length) % photoFulls.length; renderLb(); }
function closeLightbox() {
var ov = document.getElementById('vp-lightbox');
if (ov) { ov.classList.remove('vd-lightbox--open'); ov.innerHTML = ''; }
}
function lbOpen() {
var ov = document.getElementById('vp-lightbox');
return ov && ov.classList.contains('vd-lightbox--open');
}
function computeAge(birthday, deathday) {
if (!birthday) return null;
var b = new Date(birthday), end = deathday ? new Date(deathday) : new Date();
@ -241,9 +288,16 @@
var m = q('[data-vp-meta]');
if (m) m.innerHTML = meta.map(function (x) { return '<span>' + esc(x) + '</span>'; }).join('');
var aka = q('[data-vp-aka]');
if (aka) {
var names = d.also_known_as || [];
aka.textContent = names.length ? 'Also known as: ' + names.join(' · ') : '';
aka.hidden = !names.length;
}
var bio = q('[data-vp-bio]'), more = q('[data-vp-bio-more]');
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'; }
renderPhotos(d.photos);
applyFilters();
var sub = document.querySelector('.video-subpage[data-video-subpage="video-person-detail"]');
@ -264,6 +318,10 @@
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 = '';
var psec = q('[data-vp-photos-section]'); if (psec) psec.hidden = true;
var pho = q('[data-vp-photos]'); if (pho) pho.innerHTML = '';
var ak = q('[data-vp-aka]'); if (ak) ak.hidden = true;
photoFulls = [];
fetch(PERSON_URL + id, { headers: { 'Accept': 'application/json' } })
.then(function (r) { return r.ok ? r.json() : null; })
.then(function (d) {
@ -294,6 +352,8 @@
if (deptBtn && r.contains(deptBtn)) {
dept = deptBtn.getAttribute('data-vp-dept'); applyFilters(); return;
}
var shot = e.target.closest('[data-vp-shot]');
if (shot && r.contains(shot)) { openLightbox(parseInt(shot.getAttribute('data-vp-shot'), 10) || 0); return; }
var moreBtn = e.target.closest('[data-vp-bio-more]');
if (moreBtn && r.contains(moreBtn)) {
var bio = q('[data-vp-bio]');
@ -319,6 +379,12 @@
function init() {
document.addEventListener('soulsync:video-open-detail', onOpen);
document.addEventListener('click', onClick);
document.addEventListener('keydown', function (e) {
if (!lbOpen()) return;
if (e.key === 'Escape') closeLightbox();
else if (e.key === 'ArrowLeft') lbStep(-1);
else if (e.key === 'ArrowRight') lbStep(1);
});
var sortSel = document.querySelector('[data-vp-sort]');
if (sortSel) sortSel.addEventListener('change', function () {
sortBy = sortSel.value; renderCredits();

View file

@ -1361,3 +1361,17 @@ a.vd-prov:hover img, a.vd-prov:hover .vd-prov-ph { border-color: rgba(var(--vd-a
.vd-cm-name { display: block; font-size: 13px; font-weight: 700; color: #fff; line-height: 1.3; }
.vd-cm-char { display: block; font-size: 12px; color: rgba(255, 255, 255, 0.55); line-height: 1.3; }
.vd-cm-eps { display: block; font-size: 11px; color: rgb(var(--vd-accent-rgb)); margin-top: 2px; font-weight: 600; }
/* ── person: photos rail + also-known-as ─────────────────────────────────── */
.vp-aka { font-size: 13.5px; color: rgba(255, 255, 255, 0.6); margin: 0 0 18px; font-style: italic; }
.vp-photos-section { position: relative; z-index: 2; margin-bottom: 38px; }
.vp-photos { display: flex; gap: 14px; overflow-x: auto; padding-bottom: 12px; scroll-snap-type: x proximity; }
.vp-photos::-webkit-scrollbar { height: 8px; }
.vp-photos::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.15); border-radius: 4px; }
.vp-photo-thumb {
flex: 0 0 120px; width: 120px; aspect-ratio: 2 / 3; scroll-snap-align: start; padding: 0; border: 0; cursor: pointer;
border-radius: 10px; overflow: hidden; background: #000; box-shadow: 0 6px 18px rgba(0, 0, 0, 0.4);
transition: transform 0.25s ease, box-shadow 0.25s ease;
}
.vp-photo-thumb img { width: 100%; height: 100%; object-fit: cover; }
.vp-photo-thumb:hover { transform: translateY(-4px); box-shadow: 0 12px 30px rgba(0, 0, 0, 0.55); }