Wishlist TV: episodes grouped under clickable season headers; show/season -> show page

Per feedback:
- Removed the 'View show' button. The info bar is now strictly synopsis (left) +
  cast (right).
- Episodes are shown grouped under a season header that links to the show page;
  clicking the show title or a season header navigates to the show detail. Clicking
  an episode SELECTS it (drives the info bar: episode synopsis + guest cast),
  click again to go back to show-level.
- Dropped the season-tile reveal step (episodes always visible when expanded).

Scoped under .vwsh-nebula; music wishlist untouched.
This commit is contained in:
BoulderBadgeDad 2026-06-16 19:07:46 -07:00
parent 11cc9aa789
commit 7ef794fe74
2 changed files with 37 additions and 63 deletions

View file

@ -2507,12 +2507,6 @@ body[data-side="video"] #soulsync-toggle { display: none; }
background: linear-gradient(180deg, hsla(var(--orb-hue, 230), 55%, 12%, 0.55), rgba(12, 12, 16, 0.92)); }
.vwsh-nebula .wl-orb-group.expanded > * { position: relative; z-index: 1; }
/* #2 season number stamped on the tile art (so seasons read as distinct) */
.vwsh-nebula .wl-album-tile-art { position: relative; }
.vwsh-nebula .vwsh-season-tag { position: absolute; top: 5px; left: 5px; z-index: 2; font-size: 12px; font-weight: 900;
color: #fff; padding: 2px 7px; border-radius: 6px; letter-spacing: 0.02em; backdrop-filter: blur(4px);
background: rgba(0, 0, 0, 0.58); border: 1px solid rgba(255, 255, 255, 0.18); text-shadow: 0 1px 3px rgba(0, 0, 0, 0.7); }
/* episode status dot (used in the episode-card meta line) */
.vwsh-nebula .vwsh-ep-dot { display: inline-block; vertical-align: middle; margin-right: 6px;
width: 7px; height: 7px; border-radius: 50%;
@ -2545,14 +2539,6 @@ body[data-side="video"] #soulsync-toggle { display: none; }
.vwsh-nebula .wl-orb.orb-md { width: 104px; height: 104px; }
.vwsh-nebula .wl-orb.orb-lg { width: 124px; height: 124px; }
/* softer count badge (the big number read like a rating) */
.vwsh-nebula .wl-album-tile-badge { font-size: 9px; font-weight: 800; letter-spacing: 0.02em; padding: 2px 6px;
background: rgba(0, 0, 0, 0.55); color: rgba(255, 255, 255, 0.85); border: 1px solid rgba(255, 255, 255, 0.14);
backdrop-filter: blur(4px); }
/* selected season tile glows in the show hue */
.vwsh-nebula .wl-album-tile.vwsh-tile--sel { border: 1px solid hsla(var(--orb-hue, 230), 70%, 60%, 0.6);
box-shadow: 0 0 0 1px hsla(var(--orb-hue, 230), 70%, 55%, 0.4), 0 8px 22px -10px hsla(var(--orb-hue, 230), 70%, 45%, 0.6); }
/* show info bar — synopsis (left) + clickable cast bubbles (right), lazy-loaded */
.vwsh-nebula .vwsh-info:empty { display: none; }
.vwsh-nebula .vwsh-info { display: flex; gap: 20px; align-items: flex-start; text-align: left; margin: 4px 0 18px; }
@ -2579,19 +2565,30 @@ body[data-side="video"] #soulsync-toggle { display: none; }
}
.vwsh-nebula .vwsh-info-eyebrow { display: block; font-size: 10px; font-weight: 800; text-transform: uppercase;
letter-spacing: 0.05em; color: hsl(var(--orb-hue, 230) 80% 76%); margin-bottom: 6px; }
.vwsh-nebula .vwsh-info-viewshow { display: inline-block; margin-top: 12px; padding: 7px 14px; border-radius: 9px;
font-size: 11.5px; font-weight: 800; cursor: pointer; color: #fff;
border: 1px solid hsla(var(--orb-hue, 230), 70%, 55%, 0.5); background: hsla(var(--orb-hue, 230), 60%, 32%, 0.4);
transition: background 0.15s ease, border-color 0.15s ease, transform 0.15s ease; }
.vwsh-nebula .vwsh-info-viewshow:hover { background: hsla(var(--orb-hue, 230), 65%, 42%, 0.6);
border-color: hsla(var(--orb-hue, 230), 70%, 64%, 0.8); transform: translateY(-1px); }
/* selected episode card */
.vwsh-nebula .vwsh-epc.vwsh-epc--sel { background: hsla(var(--orb-hue, 230), 48%, 22%, 0.5);
border-color: hsla(var(--orb-hue, 230), 70%, 58%, 0.65); }
/* #1/#2 — the picked season drops its episodes here, full-width, as 2-line cards */
.vwsh-nebula .vwsh-ep-area:empty { display: none; }
.vwsh-nebula .vwsh-ep-area { margin-top: 14px; }
/* episodes grouped under a clickable season header (header → show page) */
.vwsh-nebula .vwsh-seasons { display: flex; flex-direction: column; gap: 16px; }
.vwsh-nebula .vwsh-szn-hd { display: flex; align-items: center; gap: 11px; padding: 7px 9px; margin-bottom: 9px; cursor: pointer;
border-radius: 10px; background: rgba(255, 255, 255, 0.03); border: 1px solid rgba(255, 255, 255, 0.06);
transition: background 0.15s ease, border-color 0.15s ease; }
.vwsh-nebula .vwsh-szn-hd:hover { background: hsla(var(--orb-hue, 230), 45%, 24%, 0.45); border-color: hsla(var(--orb-hue, 230), 70%, 58%, 0.45); }
.vwsh-nebula .vwsh-szn-thumb { flex-shrink: 0; width: 34px; height: 48px; border-radius: 5px; overflow: hidden; background: #16161d;
display: flex; align-items: center; justify-content: center; }
.vwsh-nebula .vwsh-szn-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.vwsh-nebula .vwsh-szn-ph { font-size: 16px; opacity: 0.4; }
.vwsh-nebula .vwsh-szn-name { font-size: 14px; font-weight: 800; color: #fff; }
.vwsh-nebula .vwsh-szn-count { font-size: 11.5px; font-weight: 600; color: rgba(255, 255, 255, 0.4); }
.vwsh-nebula .vwsh-szn-go { margin-left: auto; font-size: 14px; font-weight: 800; color: rgba(255, 255, 255, 0.3); transition: color 0.15s ease, transform 0.15s ease; }
.vwsh-nebula .vwsh-szn-hd:hover .vwsh-szn-go { color: hsl(var(--orb-hue, 230) 80% 75%); transform: translateX(3px); }
.vwsh-nebula .vwsh-szn-rm { flex-shrink: 0; width: 22px; height: 22px; border-radius: 50%; border: none; background: rgba(0, 0, 0, 0.35);
color: rgba(255, 255, 255, 0.4); font-size: 12px; cursor: pointer; display: flex; align-items: center; justify-content: center;
opacity: 0; transition: all 0.15s ease; }
.vwsh-nebula .vwsh-szn-hd:hover .vwsh-szn-rm { opacity: 1; }
.vwsh-nebula .vwsh-szn-rm:hover { background: rgba(239, 68, 68, 0.85); color: #fff; }
.vwsh-nebula .vwsh-ep-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 10px;
max-height: 420px; overflow-y: auto; padding: 2px; scrollbar-width: thin; }
.vwsh-nebula .vwsh-epc { position: relative; display: flex; align-items: flex-start; gap: 11px; padding: 9px; cursor: pointer;

View file

@ -77,21 +77,23 @@
? '<img class="wl-orb-img" src="' + esc(sh.poster_url) + '" alt="" ' +
'onerror="this.outerHTML=\'<div class=&quot;wl-orb-initials&quot;>' + esc(initials(sh.title)) + '</div>\'">'
: '<div class="wl-orb-initials">' + esc(initials(sh.title)) + '</div>';
// Season tiles are SELECTORS now — picking one renders its episodes full-
// width below (so episode cards get room), instead of cramming inside.
var tiles = (sh.seasons || []).map(function (se) {
// Episodes are shown grouped under a clickable season header (header →
// show page); each episode card SELECTS the episode (drives the info bar).
var seasons = (sh.seasons || []).map(function (se) {
var n = se.episodes.length;
var posterUrl = se.poster_url || sh.poster_url || null;
var inner = posterUrl ? '<img src="' + esc(posterUrl) + '" alt="">' : '<div class="wl-album-tile-fallback">📺</div>';
return '<div class="wl-album-tile" data-vwsh-tile data-tmdb="' + esc(sh.tmdb_id) + '" data-vwsh-season="' + se.season_number + '">' +
'<div class="wl-album-tile-art">' + inner + '<span class="vwsh-season-tag">S' + se.season_number + '</span></div>' +
'<div class="wl-album-tile-info">' +
'<div class="wl-album-tile-name">Season ' + se.season_number + '</div>' +
'<div class="wl-album-tile-count">' + n + ' episode' + (n === 1 ? '' : 's') + '</div>' +
var thumb = posterUrl ? '<img src="' + esc(posterUrl) + '" alt="">' : '<span class="vwsh-szn-ph">📺</span>';
var cards = (se.episodes || []).map(function (e) { return epCard(sh, se, e); }).join('');
return '<div class="vwsh-szn">' +
'<div class="vwsh-szn-hd" data-vwsh-open-show data-vwsh-src="' + src + '" data-vwsh-id="' + esc(openId) + '" title="Open show page">' +
'<span class="vwsh-szn-thumb">' + thumb + '</span>' +
'<span class="vwsh-szn-name">Season ' + se.season_number + '</span>' +
'<span class="vwsh-szn-count">' + n + ' ep</span>' +
'<span class="vwsh-szn-go" aria-hidden="true">&rarr;</span>' +
'<button class="vwsh-szn-rm" type="button" data-vwsh-rm="season" ' +
'data-tmdb="' + esc(sh.tmdb_id) + '" data-s="' + se.season_number + '" title="Remove season">&#10005;</button>' +
'</div>' +
'<span class="wl-album-tile-badge">' + n + ' ep</span>' +
'<button class="wl-album-tile-remove" type="button" data-vwsh-rm="season" ' +
'data-tmdb="' + esc(sh.tmdb_id) + '" data-s="' + se.season_number + '" title="Remove season">&#10005;</button>' +
'<div class="vwsh-ep-grid">' + cards + '</div>' +
'</div>';
}).join('');
var eps = total + ' episode' + (total === 1 ? '' : 's');
@ -111,8 +113,7 @@
'<div class="wl-orb-meta">' + eps + (sh.done ? ' · ' + sh.done + ' done' : '') + '</div>' +
'<div class="wl-orb-expanded">' +
'<div class="vwsh-info" data-vwsh-info></div>' +
'<div class="wl-album-fan">' + tiles + '</div>' +
'<div class="vwsh-ep-area" data-vwsh-ep-area></div></div>' +
'<div class="vwsh-seasons">' + seasons + '</div></div>' +
'</div>';
}
@ -139,9 +140,7 @@
// sel = a selected episode object (episode synopsis + guest cast), or null (show synopsis + show cast).
function renderInfoBar(group, tmdb, sel) {
var info = group && group.querySelector('[data-vwsh-info]'); if (!info) return;
var sh = state.showData[tmdb] || {}, si = state.showInfo[tmdb] || {};
var src = sh.library_id != null ? 'library' : 'tmdb';
var openId = sh.library_id != null ? sh.library_id : tmdb;
var si = state.showInfo[tmdb] || {};
var eyebrow, overview, castArr;
if (sel) {
eyebrow = 'S' + sel.season_number + ' · E' + sel.episode_number;
@ -152,9 +151,7 @@
}
var left = '<div class="vwsh-info-syn">' +
(eyebrow ? '<span class="vwsh-info-eyebrow">' + esc(eyebrow) + '</span>' : '') +
esc(overview) +
'<button class="vwsh-info-viewshow" type="button" data-vwsh-open-show data-vwsh-src="' + src + '" data-vwsh-id="' + esc(openId) + '">View show &rarr;</button>' +
'</div>';
esc(overview) + '</div>';
info.innerHTML = left + (castArr.length ? '<div class="vwsh-info-cast">' + castBubbles(castArr) + '</div>' : '');
// lazily fetch the episode's guest stars, then re-render if still selected
if (sel && sel._guests === undefined) {
@ -209,14 +206,6 @@
'data-tmdb="' + esc(sh.tmdb_id) + '" data-s="' + se.season_number + '" data-e="' + e.episode_number + '" title="Remove">&#10005;</button>' +
'</div>';
}
function renderEpisodeArea(group, tmdb, seasonNum) {
var area = group && group.querySelector('[data-vwsh-ep-area]'); if (!area) return;
var sh = state.showData[tmdb]; var se = null;
if (sh) (sh.seasons || []).forEach(function (x) { if (x.season_number === seasonNum) se = x; });
if (!se) { area.innerHTML = ''; return; }
area.innerHTML = '<div class="vwsh-ep-grid">' +
(se.episodes || []).map(function (e) { return epCard(sh, se, e); }).join('') + '</div>';
}
function render(items) {
var grid = $('[data-vwsh-grid]'); if (!grid) return;
@ -371,18 +360,6 @@
renderInfoBar(eg, etmdb, findEpisode(etmdb, parseInt(epc.getAttribute('data-s'), 10), parseInt(epc.getAttribute('data-e'), 10)));
return;
}
var tile = e.target.closest('[data-vwsh-tile]');
if (tile) { // season → render its episodes full-width below (single-select)
var group = tile.closest('.wl-orb-group');
var tmdb = parseInt(tile.getAttribute('data-tmdb'), 10);
var wasSel = tile.classList.contains('vwsh-tile--sel');
if (group) { var all = group.querySelectorAll('.wl-album-tile'); for (var i = 0; i < all.length; i++) all[i].classList.remove('vwsh-tile--sel'); }
renderInfoBar(group, tmdb, null); // changing/closing a season resets the info bar to show-level
if (wasSel) { var a = group && group.querySelector('[data-vwsh-ep-area]'); if (a) a.innerHTML = ''; return; }
tile.classList.add('vwsh-tile--sel');
renderEpisodeArea(group, tmdb, parseInt(tile.getAttribute('data-vwsh-season'), 10));
return;
}
var orb = e.target.closest('[data-vwsh-orb]');
if (orb) { // show → reveal seasons + lazily load the synopsis/cast info bar
var g = orb.closest('.wl-orb-group');