video download: dedicated TV show view — season/episode picker
Shows now get their own download experience instead of the movie layout. Clicking
Download on a show WIDENS the modal (~680→940px, animated) and renders a tree:
- Quality target chips + a show summary (N seasons · M episodes · X in library ·
Y missing) + a master select-all + a primary 'Search N selected'.
- Season cards (collapsible) with a tri-state season checkbox, 'N eps · M missing',
and a per-season search button (auto-opens the season to reveal the scan).
- Episode rows: checkbox, E# · title, In library/Missing/Upcoming badge. Default
selection = everything you're MISSING (owned shown but unticked, upcoming locked).
- Each episode expands inline to its branded per-source search strip (Soulseek/
Torrent/Usenet), so you can grab one episode à la carte; or batch via the season/
bulk buttons. Searches are the same faux-scan stub — no backend yet.
Library shows use shipped episodes; tmdb shows prefetch seasons via the existing
/tmdb/show/<id>/season/<n> endpoint. get-modal stashes the detail + tvId and routes
shows to VideoDownload.render({kind:'show',…}); movie/youtube paths unchanged.
Vibey: staggered entrance, branded scan bars, smooth collapse. Reduced-motion honored.
JS + CSS balance clean.
This commit is contained in:
parent
ecd9c25e3c
commit
8803c1bd0c
3 changed files with 342 additions and 3 deletions
|
|
@ -72,6 +72,7 @@
|
|||
function render(container, opts) {
|
||||
if (!container) return;
|
||||
opts = opts || {};
|
||||
if (opts.kind === 'show') { renderShow(container, opts); return; }
|
||||
container.innerHTML = contentHTML();
|
||||
if (!container._vdlWired) { container._vdlWired = true; container.addEventListener('click', onClick); }
|
||||
|
||||
|
|
@ -197,5 +198,259 @@
|
|||
toast('Automatic search isn’t wired up yet — coming soon', 'info');
|
||||
}
|
||||
|
||||
// ── TV show download view ─────────────────────────────────────────────────
|
||||
// A season→episode picker (everything you're missing pre-ticked), each season
|
||||
// and episode searchable inline across your sources, plus a bulk "Search N
|
||||
// selected". Searches are stubs (faux-scan) — same motion the engine will drive.
|
||||
function isoToday() {
|
||||
var n = new Date();
|
||||
return n.getFullYear() + '-' + ('0' + (n.getMonth() + 1)).slice(-2) + '-' + ('0' + n.getDate()).slice(-2);
|
||||
}
|
||||
function epState(e, today) {
|
||||
if (e && e.owned) return 'owned';
|
||||
if (e && e.air_date && e.air_date > today) return 'upcoming';
|
||||
return 'missing';
|
||||
}
|
||||
|
||||
function renderShow(container, opts) {
|
||||
var d = opts.detail || {};
|
||||
var st = container._dl = {
|
||||
sel: new Set(), today: isoToday(),
|
||||
tvId: opts.tvId || d.tmdb_id || null, source: opts.source || 'library',
|
||||
sources: ['soulseek'], epMeta: {}
|
||||
};
|
||||
container.innerHTML =
|
||||
'<div class="vdl-section"><div class="vdl-sec-label">Quality target</div>' +
|
||||
'<div class="vdl-chips" data-vdl-target><span class="vdl-chip vdl-chip--ghost">Loading…</span></div></div>' +
|
||||
'<div class="vdl-show-bar">' +
|
||||
'<label class="vdl-allchk"><input type="checkbox" data-vdl-all><span class="vdl-allchk-txt">All</span></label>' +
|
||||
'<span class="vdl-show-summary" data-vdl-summary>Loading episodes…</span>' +
|
||||
'<button class="vdl-search-all vdl-search-sel" type="button" data-vdl-search-sel disabled>' +
|
||||
'⌕ Search <span data-vdl-selcount>0</span> selected</button>' +
|
||||
'</div>' +
|
||||
'<div class="vdl-seasons" data-vdl-seasons></div>';
|
||||
|
||||
if (!container._dlShowWired) {
|
||||
container._dlShowWired = true;
|
||||
container.addEventListener('click', onShowClick);
|
||||
container.addEventListener('change', onShowChange);
|
||||
}
|
||||
getJSON('/api/video/downloads/quality').then(function (p) { if (container.isConnected && p) renderTarget(container, p, false); });
|
||||
getJSON('/api/video/downloads/config').then(function (c) { if (container.isConnected) st.sources = sourcesFromConfig(c); });
|
||||
buildSeasons(container, d, st);
|
||||
}
|
||||
|
||||
function buildSeasons(container, d, st) {
|
||||
var host = container.querySelector('[data-vdl-seasons]'); if (!host) return;
|
||||
var seasons = (d.seasons || []).slice();
|
||||
if (!seasons.length) { host.innerHTML = '<div class="vdl-src-empty">No season information available.</div>'; updateShowBar(container); return; }
|
||||
host.innerHTML = seasons.map(function (s) { return seasonShellHTML(s); }).join('');
|
||||
seasons.forEach(function (s) {
|
||||
var card = host.querySelector('.vdl-season[data-vdl-season="' + s.season_number + '"]');
|
||||
var eps = s.episodes || [];
|
||||
if (eps.length) { fillSeason(container, card, s.season_number, eps, st); }
|
||||
else if ((s.episode_total || 0) > 0 && st.tvId) { fetchSeason(container, card, s.season_number, st); }
|
||||
else { var b = card.querySelector('.vdl-season-eps'); if (b) b.innerHTML = '<div class="vdl-season-empty">No episodes.</div>'; }
|
||||
});
|
||||
updateShowBar(container);
|
||||
}
|
||||
|
||||
function seasonShellHTML(s) {
|
||||
var sn = s.season_number;
|
||||
var total = (s.episodes && s.episodes.length) || s.episode_total || 0;
|
||||
return '<div class="vdl-season" data-vdl-season="' + sn + '">' +
|
||||
'<div class="vdl-season-head" data-vdl-season-toggle>' +
|
||||
'<input type="checkbox" class="vdl-season-cb" data-vdl-season-all="' + sn + '">' +
|
||||
'<span class="vdl-season-name">' + esc(s.title || ('Season ' + sn)) + '</span>' +
|
||||
'<span class="vdl-season-meta" data-vdl-season-meta>' + total + ' eps</span>' +
|
||||
'<button class="vdl-season-search" type="button" data-vdl-season-search="' + sn + '" title="Search this season">⌕</button>' +
|
||||
'<span class="vdl-season-chev" aria-hidden="true">⌄</span>' +
|
||||
'</div>' +
|
||||
'<div class="vdl-season-body"><div class="vdl-season-eps"><div class="vdl-season-empty">Loading…</div></div></div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
function fillSeason(container, card, sn, eps, st) {
|
||||
if (!card) return;
|
||||
var body = card.querySelector('.vdl-season-eps'); if (!body) return;
|
||||
var missing = 0;
|
||||
eps.forEach(function (e) {
|
||||
var es = epState(e, st.today);
|
||||
st.epMeta[sn + '_' + e.episode_number] = { state: es };
|
||||
if (es === 'missing') { missing++; st.sel.add(sn + '_' + e.episode_number); }
|
||||
});
|
||||
body.innerHTML = eps.map(function (e) { return epRowHTML(sn, e, st); }).join('');
|
||||
var meta = card.querySelector('[data-vdl-season-meta]');
|
||||
if (meta) meta.textContent = eps.length + ' eps' + (missing ? ' · ' + missing + ' missing' : '');
|
||||
card.setAttribute('data-loaded', '1');
|
||||
syncSeason(container, sn);
|
||||
}
|
||||
|
||||
function fetchSeason(container, card, sn, st) {
|
||||
if (!card || !st.tvId) return;
|
||||
fetch('/api/video/tmdb/show/' + st.tvId + '/season/' + sn, { headers: { Accept: 'application/json' } })
|
||||
.then(function (r) { return r.ok ? r.json() : null; })
|
||||
.then(function (data) {
|
||||
if (!container.isConnected) return;
|
||||
var eps = (data && data.episodes) || [];
|
||||
var b = card.querySelector('.vdl-season-eps');
|
||||
if (!eps.length) { if (b) b.innerHTML = '<div class="vdl-season-empty">No episode info.</div>'; return; }
|
||||
fillSeason(container, card, sn, eps, st);
|
||||
updateShowBar(container);
|
||||
})
|
||||
.catch(function () { var b = card.querySelector('.vdl-season-eps'); if (b) b.innerHTML = '<div class="vdl-season-empty">Couldn’t load.</div>'; });
|
||||
}
|
||||
|
||||
function epRowHTML(sn, e, st) {
|
||||
var key = sn + '_' + e.episode_number;
|
||||
var es = (st.epMeta[key] && st.epMeta[key].state) || epState(e, st.today);
|
||||
var lock = (es === 'upcoming');
|
||||
var ctrl = lock
|
||||
? '<span class="vdl-ep-lock" title="Hasn\'t aired yet">◷</span>'
|
||||
: '<input type="checkbox" class="vdl-ep-cb" data-vdl-ep-cb="' + key + '"' + (st.sel.has(key) ? ' checked' : '') + '>';
|
||||
var badge = es === 'owned' ? '<span class="vdl-ep-badge vdl-ep-badge--owned">In library</span>'
|
||||
: es === 'upcoming' ? '<span class="vdl-ep-badge vdl-ep-badge--soon">Upcoming</span>'
|
||||
: '<span class="vdl-ep-badge vdl-ep-badge--missing">Missing</span>';
|
||||
return '<div class="vdl-ep vdl-ep--' + es + '" data-vdl-ep="' + key + '">' +
|
||||
'<div class="vdl-ep-head"' + (lock ? '' : ' data-vdl-ep-toggle') + '>' +
|
||||
'<span class="vdl-ep-cell">' + ctrl + '</span>' +
|
||||
'<span class="vdl-ep-num">E' + (e.episode_number != null ? e.episode_number : '') + '</span>' +
|
||||
'<span class="vdl-ep-title">' + esc(e.title || ('Episode ' + e.episode_number)) + '</span>' +
|
||||
'<span class="vdl-ep-status" data-vdl-ep-status></span>' +
|
||||
badge +
|
||||
(lock ? '' : '<span class="vdl-ep-chev" aria-hidden="true">⌄</span>') +
|
||||
'</div>' +
|
||||
'<div class="vdl-ep-search" data-vdl-ep-search></div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
function onShowClick(e) {
|
||||
var container = e.currentTarget; if (!container._dl) return;
|
||||
var srch = e.target.closest('[data-vdl-search]');
|
||||
if (srch) { var row = srch.closest('.vdl-src'); if (row) scanRow(row, 0); toast('Automatic search isn’t wired up yet — coming soon', 'info'); return; }
|
||||
var ss = e.target.closest('[data-vdl-season-search]');
|
||||
if (ss) {
|
||||
var sc = container.querySelector('.vdl-season[data-vdl-season="' + ss.getAttribute('data-vdl-season-search') + '"]');
|
||||
if (sc) sc.classList.add('vdl-season--open'); // reveal the rows being scanned
|
||||
searchScope(container, sc); return;
|
||||
}
|
||||
if (e.target.closest('[data-vdl-search-sel]')) { searchScope(container, container.querySelector('[data-vdl-seasons]')); return; }
|
||||
var sh = e.target.closest('[data-vdl-season-toggle]');
|
||||
if (sh && !e.target.closest('.vdl-season-cb') && !e.target.closest('[data-vdl-season-search]')) {
|
||||
sh.closest('.vdl-season').classList.toggle('vdl-season--open'); return;
|
||||
}
|
||||
var eh = e.target.closest('[data-vdl-ep-toggle]');
|
||||
if (eh && !e.target.closest('.vdl-ep-cell')) { toggleEp(container, eh.closest('.vdl-ep')); }
|
||||
}
|
||||
|
||||
function onShowChange(e) {
|
||||
var container = e.currentTarget; var st = container._dl; if (!st) return;
|
||||
var ec = e.target.closest('[data-vdl-ep-cb]');
|
||||
if (ec) {
|
||||
var k = ec.getAttribute('data-vdl-ep-cb');
|
||||
if (ec.checked) st.sel.add(k); else st.sel.delete(k);
|
||||
syncSeason(container, k.split('_')[0]); updateShowBar(container); return;
|
||||
}
|
||||
var sa = e.target.closest('[data-vdl-season-all]');
|
||||
if (sa) { setSeasonSel(container, sa.getAttribute('data-vdl-season-all'), sa.checked); updateShowBar(container); return; }
|
||||
if (e.target.closest('[data-vdl-all]')) { setAllSel(container, e.target.checked); updateShowBar(container); }
|
||||
}
|
||||
|
||||
function toggleEp(container, epEl) {
|
||||
if (!epEl) return;
|
||||
var open = !epEl.classList.contains('vdl-ep--open');
|
||||
epEl.classList.toggle('vdl-ep--open', open);
|
||||
if (open && !epEl.getAttribute('data-srcbuilt')) buildEpSearch(container, epEl);
|
||||
}
|
||||
|
||||
function buildEpSearch(container, epEl) {
|
||||
epEl.setAttribute('data-srcbuilt', '1');
|
||||
var panel = epEl.querySelector('[data-vdl-ep-search]'); if (!panel) return;
|
||||
var srcs = (container._dl.sources || []).filter(function (s) { return SRC_META[s]; });
|
||||
if (!srcs.length) { panel.innerHTML = '<div class="vdl-ep-srcs"><div class="vdl-src-empty">No source configured.</div></div>'; return; }
|
||||
panel.innerHTML = '<div class="vdl-ep-srcs">' + srcs.map(function (s) {
|
||||
var m = SRC_META[s];
|
||||
return '<div class="vdl-src vdl-src--mini" data-vdl-src="' + s + '">' +
|
||||
'<span class="vdl-src-icon"><span class="vdl-src-emoji">' + m.emoji + '</span></span>' +
|
||||
'<span class="vdl-src-main"><span class="vdl-src-name">' + esc(m.name) + '</span>' +
|
||||
'<span class="vdl-src-meta"><span class="vdl-src-dot"></span><span class="vdl-src-status" data-vdl-status>Ready</span></span></span>' +
|
||||
'<button class="vdl-src-search" type="button" data-vdl-search="' + s + '">⌕ Search</button>' +
|
||||
'</div>';
|
||||
}).join('') + '</div>';
|
||||
}
|
||||
|
||||
function setSeasonSel(container, sn, on) {
|
||||
var st = container._dl;
|
||||
var cbs = container.querySelectorAll('.vdl-season[data-vdl-season="' + sn + '"] .vdl-ep-cb');
|
||||
for (var i = 0; i < cbs.length; i++) {
|
||||
cbs[i].checked = on;
|
||||
var k = cbs[i].getAttribute('data-vdl-ep-cb');
|
||||
if (on) st.sel.add(k); else st.sel.delete(k);
|
||||
}
|
||||
syncSeason(container, sn);
|
||||
}
|
||||
|
||||
function setAllSel(container, on) {
|
||||
var cards = container.querySelectorAll('.vdl-season');
|
||||
for (var i = 0; i < cards.length; i++) setSeasonSel(container, cards[i].getAttribute('data-vdl-season'), on);
|
||||
}
|
||||
|
||||
function syncSeason(container, sn) {
|
||||
var card = container.querySelector('.vdl-season[data-vdl-season="' + sn + '"]'); if (!card) return;
|
||||
var all = card.querySelector('[data-vdl-season-all]'); if (!all) return;
|
||||
var cbs = card.querySelectorAll('.vdl-ep-cb'), checked = 0;
|
||||
for (var i = 0; i < cbs.length; i++) if (cbs[i].checked) checked++;
|
||||
all.checked = cbs.length > 0 && checked === cbs.length;
|
||||
all.indeterminate = checked > 0 && checked < cbs.length;
|
||||
all.disabled = cbs.length === 0;
|
||||
}
|
||||
|
||||
function updateShowBar(container) {
|
||||
var st = container._dl; if (!st) return;
|
||||
var n = st.sel.size;
|
||||
var cnt = container.querySelector('[data-vdl-selcount]'); if (cnt) cnt.textContent = n;
|
||||
var btn = container.querySelector('[data-vdl-search-sel]'); if (btn) btn.disabled = n === 0;
|
||||
var sum = container.querySelector('[data-vdl-summary]');
|
||||
if (sum) {
|
||||
var seasons = container.querySelectorAll('.vdl-season').length;
|
||||
var owned = 0, missing = 0, total = 0;
|
||||
for (var k in st.epMeta) { total++; if (st.epMeta[k].state === 'owned') owned++; else if (st.epMeta[k].state === 'missing') missing++; }
|
||||
sum.textContent = seasons + ' season' + (seasons === 1 ? '' : 's') + ' · ' + total + ' episodes · ' +
|
||||
owned + ' in library · ' + missing + ' missing';
|
||||
}
|
||||
var master = container.querySelector('[data-vdl-all]');
|
||||
if (master) {
|
||||
var all = container.querySelectorAll('.vdl-ep-cb'), c = 0;
|
||||
for (var i = 0; i < all.length; i++) if (all[i].checked) c++;
|
||||
master.checked = all.length > 0 && c === all.length;
|
||||
master.indeterminate = c > 0 && c < all.length;
|
||||
}
|
||||
}
|
||||
|
||||
// Scan every SELECTED episode within a scope (a season card or the whole list).
|
||||
function searchScope(container, scopeEl) {
|
||||
if (!scopeEl || !container._dl) return;
|
||||
var st = container._dl, eps = scopeEl.querySelectorAll('.vdl-ep'), picked = [];
|
||||
for (var i = 0; i < eps.length; i++) { var k = eps[i].getAttribute('data-vdl-ep'); if (k && st.sel.has(k)) picked.push(eps[i]); }
|
||||
if (!picked.length) { toast('Select at least one episode', 'info'); return; }
|
||||
picked.forEach(function (epEl, i) { scanEp(epEl, i); });
|
||||
toast('Automatic search isn’t wired up yet — coming soon', 'info');
|
||||
}
|
||||
|
||||
function scanEp(epEl, i) {
|
||||
if (epEl._scanning) return;
|
||||
epEl._scanning = true;
|
||||
epEl.classList.add('vdl-ep--scanning');
|
||||
var s = epEl.querySelector('[data-vdl-ep-status]');
|
||||
if (s) { s.textContent = 'Searching'; s.className = 'vdl-ep-status vdl-ep-status--scanning'; }
|
||||
setTimeout(function () {
|
||||
if (!epEl.isConnected) { epEl._scanning = false; return; }
|
||||
epEl.classList.remove('vdl-ep--scanning');
|
||||
epEl._scanning = false;
|
||||
var x = epEl.querySelector('[data-vdl-ep-status]');
|
||||
if (x) { x.textContent = 'Coming soon'; x.className = 'vdl-ep-status vdl-ep-status--soon'; }
|
||||
}, 1100 + i * 180);
|
||||
}
|
||||
|
||||
window.VideoDownload = { render: render };
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -84,8 +84,15 @@
|
|||
var dl = ov.querySelector('[data-vgm-dl]');
|
||||
var content = ov.querySelector('[data-vgm-dl-content]');
|
||||
if (!dl || !content || !window.VideoDownload) { toast('Download module not loaded', 'error'); return; }
|
||||
var file = (modalState && modalState.kind === 'movie' && modalState.owned) ? (modalState.file || null) : null;
|
||||
VideoDownload.render(content, { kind: o.kind, id: o.id, source: o.source || 'library', isYt: false, file: file });
|
||||
if (o.kind === 'show') {
|
||||
// Shows get a wider modal + a season/episode picker (not the movie layout).
|
||||
VideoDownload.render(content, { kind: 'show', id: o.id, source: o.source || 'library',
|
||||
detail: (modalState && modalState._detail) || null, tvId: (modalState && modalState._tvId) || null });
|
||||
ov.classList.add('vgm-mode-dl-show');
|
||||
} else {
|
||||
var file = (modalState && modalState.kind === 'movie' && modalState.owned) ? (modalState.file || null) : null;
|
||||
VideoDownload.render(content, { kind: o.kind, id: o.id, source: o.source || 'library', isYt: false, file: file });
|
||||
}
|
||||
setDownloadMode(ov, true);
|
||||
dl.hidden = false;
|
||||
var modal = ov.querySelector('.vgm-modal'); if (modal) modal.scrollTop = 0;
|
||||
|
|
@ -94,6 +101,7 @@
|
|||
function exitDownload(ov) {
|
||||
var dl = ov.querySelector('[data-vgm-dl]'); if (dl) dl.hidden = true;
|
||||
setDownloadMode(ov, false);
|
||||
ov.classList.remove('vgm-mode-dl-show');
|
||||
}
|
||||
|
||||
// ── wishlist / watchlist writes ───────────────────────────────────────────
|
||||
|
|
@ -513,6 +521,8 @@
|
|||
if (modalState) { // identity for the wishlist/watchlist writes
|
||||
modalState.tmdbId = d.tmdb_id; modalState.title = d.title;
|
||||
modalState.poster = pUrl || null; modalState.libraryId = libId;
|
||||
modalState._detail = d; // feeds the show download tree (seasons/episodes)
|
||||
modalState._tvId = (o.source === 'tmdb') ? parseInt(o.id, 10) : (d.tmdb_id || null);
|
||||
}
|
||||
} else {
|
||||
modalState = { kind: 'movie', owned: !!d.owned, tmdbId: d.tmdb_id, title: d.title,
|
||||
|
|
|
|||
|
|
@ -2121,9 +2121,12 @@ body[data-side="video"] #soulsync-toggle { display: none; }
|
|||
0 0 90px hsla(var(--vgm-h, 230), 75%, 55%, 0.20),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.06);
|
||||
transform: translateY(22px) scale(0.96);
|
||||
transition: transform 0.42s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.3s ease;
|
||||
transition: transform 0.42s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.3s ease,
|
||||
width 0.4s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
.vgm-overlay.vgm-open .vgm-modal { transform: none; }
|
||||
/* shows expand the modal for the season/episode picker */
|
||||
.vgm-overlay.vgm-mode-dl-show .vgm-modal { width: min(940px, 100%); }
|
||||
|
||||
/* hero — slow Ken Burns drift, hue-tinted scrim, and a drifting light sweep */
|
||||
.vgm-hero { animation: vgmKen 22s ease-in-out infinite alternate; }
|
||||
|
|
@ -3174,3 +3177,74 @@ body[data-side="video"] #soulsync-toggle { display: none; }
|
|||
.vdl-verdict--ok, .vdl-verdict--up, .vdl-verdict--pending, .vdl-search-all { animation: none !important; }
|
||||
.vdl-chip::after, .vdl-search-all::after, .vdl-src--scanning::before { display: none; }
|
||||
}
|
||||
|
||||
/* ── TV show download view: season/episode picker ─────────────────────────── */
|
||||
.vdl-show-bar { display: flex; align-items: center; gap: 13px; flex-wrap: wrap; margin: 2px 0 16px;
|
||||
padding: 11px 14px; border-radius: 13px; background: rgba(255, 255, 255, 0.04);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08); animation: vdlRise 0.45s both; }
|
||||
.vdl-allchk { display: inline-flex; align-items: center; gap: 8px; cursor: pointer; font-size: 12.5px;
|
||||
font-weight: 800; color: rgba(255, 255, 255, 0.85); white-space: nowrap; }
|
||||
.vdl-show-summary { flex: 1; min-width: 120px; font-size: 12px; font-weight: 600; color: rgba(255, 255, 255, 0.5); }
|
||||
.vdl-search-sel:disabled { animation: none; opacity: 0.45; cursor: default; box-shadow: none; }
|
||||
.vdl-seasons input[type="checkbox"], .vdl-show-bar input[type="checkbox"] {
|
||||
accent-color: rgb(var(--accent-rgb)); width: 16px; height: 16px; cursor: pointer; flex-shrink: 0; margin: 0; }
|
||||
|
||||
.vdl-seasons { display: flex; flex-direction: column; gap: 9px; }
|
||||
.vdl-season { border-radius: 13px; border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
background: rgba(255, 255, 255, 0.025); overflow: hidden; animation: vdlSlide 0.42s both; }
|
||||
.vdl-season:hover { border-color: rgba(255, 255, 255, 0.14); }
|
||||
.vdl-season-head { display: flex; align-items: center; gap: 11px; padding: 12px 14px; cursor: pointer;
|
||||
transition: background 0.15s ease; }
|
||||
.vdl-season-head:hover { background: rgba(255, 255, 255, 0.04); }
|
||||
.vdl-season-name { font-size: 14px; font-weight: 800; color: #fff; letter-spacing: -0.01em; }
|
||||
.vdl-season-meta { flex: 1; font-size: 11.5px; font-weight: 600; color: rgba(255, 255, 255, 0.45); }
|
||||
.vdl-season-search { flex-shrink: 0; width: 32px; height: 28px; border-radius: 8px; cursor: pointer; font-size: 13px;
|
||||
background: rgba(var(--accent-rgb), 0.16); border: 1px solid rgba(var(--accent-rgb), 0.35); color: #fff;
|
||||
transition: all 0.15s ease; }
|
||||
.vdl-season-search:hover { background: rgba(var(--accent-rgb), 0.3); border-color: rgba(var(--accent-rgb), 0.6);
|
||||
transform: translateY(-1px); }
|
||||
.vdl-season-chev { flex-shrink: 0; font-size: 13px; color: rgba(255, 255, 255, 0.5); transition: transform 0.25s ease; }
|
||||
.vdl-season--open .vdl-season-chev { transform: rotate(180deg); }
|
||||
.vdl-season-body { max-height: 0; overflow: hidden; transition: max-height 0.32s cubic-bezier(0.2, 0.7, 0.2, 1); }
|
||||
.vdl-season--open .vdl-season-body { max-height: 5000px; }
|
||||
.vdl-season-empty { padding: 12px 16px; font-size: 12.5px; color: rgba(255, 255, 255, 0.4); }
|
||||
|
||||
.vdl-ep { position: relative; border-top: 1px solid rgba(255, 255, 255, 0.05); }
|
||||
.vdl-ep-head { display: flex; align-items: center; gap: 10px; padding: 9px 14px 9px 16px; cursor: pointer;
|
||||
transition: background 0.14s ease; }
|
||||
.vdl-ep-head:hover { background: rgba(255, 255, 255, 0.035); }
|
||||
.vdl-ep--owned { opacity: 0.62; }
|
||||
.vdl-ep--owned:hover { opacity: 0.85; }
|
||||
.vdl-ep-cell { flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center; width: 18px; }
|
||||
.vdl-ep-lock { color: rgba(255, 255, 255, 0.3); font-size: 13px; }
|
||||
.vdl-ep-num { flex-shrink: 0; font: 800 11px/1 'JetBrains Mono', ui-monospace, monospace;
|
||||
color: rgba(255, 255, 255, 0.5); min-width: 26px; }
|
||||
.vdl-ep-title { flex: 1; min-width: 0; font-size: 13px; font-weight: 600; color: rgba(255, 255, 255, 0.92);
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.vdl-ep-status { flex-shrink: 0; font-size: 11px; font-weight: 700; color: rgba(255, 255, 255, 0.4); white-space: nowrap; }
|
||||
.vdl-ep-status:empty { display: none; }
|
||||
.vdl-ep-status--scanning { color: hsl(var(--vgm-h, 220), 80%, 72%); }
|
||||
.vdl-ep-status--scanning::after { content: ''; animation: vdlDots 1.1s steps(1) infinite; }
|
||||
.vdl-ep-status--soon { color: #fbcd7a; }
|
||||
.vdl-ep-badge { flex-shrink: 0; font-size: 9.5px; font-weight: 800; letter-spacing: 0.02em; padding: 2px 8px;
|
||||
border-radius: 999px; text-transform: uppercase; }
|
||||
.vdl-ep-badge--owned { background: rgba(108, 211, 145, 0.16); color: #8fe7af; }
|
||||
.vdl-ep-badge--missing { background: hsla(var(--vgm-h, 220), 50%, 55%, 0.2); color: #fff; }
|
||||
.vdl-ep-badge--soon { background: rgba(255, 255, 255, 0.08); color: rgba(255, 255, 255, 0.5); }
|
||||
.vdl-ep-chev { flex-shrink: 0; font-size: 12px; color: rgba(255, 255, 255, 0.38); transition: transform 0.22s ease; }
|
||||
.vdl-ep--open .vdl-ep-chev { transform: rotate(180deg); }
|
||||
.vdl-ep--scanning::before { content: ''; position: absolute; left: 0; bottom: 0; height: 2px; width: 36%;
|
||||
border-radius: 2px; background: linear-gradient(90deg, transparent, hsl(var(--vgm-h, 220), 85%, 62%), transparent);
|
||||
box-shadow: 0 0 9px 1px hsla(var(--vgm-h, 220), 85%, 62%, 0.7); animation: vdlScan 0.9s ease-in-out infinite; }
|
||||
/* inline per-episode source search */
|
||||
.vdl-ep-search { max-height: 0; overflow: hidden; transition: max-height 0.3s cubic-bezier(0.2, 0.7, 0.2, 1); }
|
||||
.vdl-ep--open .vdl-ep-search { max-height: 360px; }
|
||||
.vdl-ep-srcs { display: flex; flex-direction: column; gap: 7px; padding: 3px 14px 12px 42px; }
|
||||
.vdl-src--mini { padding: 8px 11px; border-radius: 10px; animation: none; }
|
||||
.vdl-src--mini .vdl-src-icon { width: 32px; height: 32px; border-radius: 9px; }
|
||||
.vdl-src--mini .vdl-src-emoji { font-size: 15px; }
|
||||
.vdl-src--mini .vdl-src-name { font-size: 13px; }
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.vdl-show-bar, .vdl-season, .vdl-ep--scanning::before { animation: none !important; }
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue