From 2a61ac6a13f73cc4a1e749e76978b303c9bf08ed Mon Sep 17 00:00:00 2001 From: BoulderBadgeDad Date: Fri, 19 Jun 2026 11:27:59 -0700 Subject: [PATCH] video download: render the download view IN-PLACE in the get-modal (no 2nd modal) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per feedback — one modal with a view transition beats close+open. Clicking 'Download' now swaps the get-modal's detail body for the download view (quality target + owned verdict + per-source search) with a '← Back to details' button; the selection-only sections (episodes/next/follow) and the Full page/Download/Add buttons collapse, the all-related movie bits stay. - video-download-modal.js → video-download-view.js: now a reusable RENDERER, VideoDownload.render(container, opts), not its own overlay. A future YouTube trigger can render the same view into its own container (still universal). - get-modal: enterDownload/exitDownload toggle the in-place view; stashes the owned file on modalState to feed the verdict. Removed the separate .vdl overlay/hero/close CSS; added .vgm-dl/.vgm-back + [hidden] guards; chips now pick up the modal's --vgm-h vibe hue. Pure front-end; balance + div + css brace checks clean. --- webui/index.html | 5 +- webui/static/video/video-download-modal.js | 253 --------------------- webui/static/video/video-download-view.js | 183 +++++++++++++++ webui/static/video/video-get-modal.js | 54 +++-- webui/static/video/video-side.css | 46 +--- 5 files changed, 236 insertions(+), 305 deletions(-) delete mode 100644 webui/static/video/video-download-modal.js create mode 100644 webui/static/video/video-download-view.js diff --git a/webui/index.html b/webui/index.html index e28e5e7a..39fcd772 100644 --- a/webui/index.html +++ b/webui/index.html @@ -10210,8 +10210,9 @@ - - + + diff --git a/webui/static/video/video-download-modal.js b/webui/static/video/video-download-modal.js deleted file mode 100644 index 301d1ced..00000000 --- a/webui/static/video/video-download-modal.js +++ /dev/null @@ -1,253 +0,0 @@ -/* - * SoulSync — universal Download modal (movie / TV show / YouTube). - * - * Opened from the get-modal's "Download" button (and, later, the YouTube card). - * v1 is VISUAL scaffolding for the direct-download flow: it shows the quality - * TARGET (read from the Settings → Downloads profile you configured), judges any - * copy you ALREADY own against that target (real — via /downloads/evaluate), and - * lists each attached download source with a per-source "Search" affordance. The - * searches themselves are stubs — no backend wiring yet (that's the engine phase). - * - * VideoDownload.open({ kind, id, source, title, thumb }). Self-contained. - */ -(function () { - 'use strict'; - - function esc(s) { - return String(s == null ? '' : s) - .replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); - } - function toast(msg, type) { if (typeof showToast === 'function') showToast(msg, type); } - function hueOf(s) { var h = 0, t = String(s || ''); for (var i = 0; i < t.length; i++) h = (h * 31 + t.charCodeAt(i)) >>> 0; return h % 360; } - function resLabel(res) { - if (!res) return ''; - res = String(res).toLowerCase(); - if (res.indexOf('2160') > -1 || res === '4k') return '4K'; - if (res.indexOf('1080') > -1) return '1080p'; - if (res.indexOf('720') > -1) return '720p'; - if (res.indexOf('480') > -1 || res.indexOf('576') > -1) return 'SD'; - return res.toUpperCase(); - } - var CUT_LABEL = { '2160p': '4K', '1080p': '1080p', '720p': '720p', '480p': 'SD' }; - var SRC_META = { - soulseek: { name: 'Soulseek', emoji: '🎵' }, - torrent: { name: 'Torrent', emoji: '🧲' }, - usenet: { name: 'Usenet', emoji: '📰' }, - youtube: { name: 'YouTube', emoji: '▶' } - }; - - function getJSON(url) { - return fetch(url, { headers: { Accept: 'application/json' } }) - .then(function (r) { return r.ok ? r.json() : null; }).catch(function () { return null; }); - } - function postJSON(url, body) { - return fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, - body: JSON.stringify(body) }).then(function (r) { return r.ok ? r.json() : null; }).catch(function () { return null; }); - } - - var modalEl = null, keyHandler = null; - - function closeModal() { - if (!modalEl) return; - modalEl.classList.remove('vdl-open'); - document.body.style.removeProperty('overflow'); - if (keyHandler) { document.removeEventListener('keydown', keyHandler); keyHandler = null; } - var el = modalEl; modalEl = null; - setTimeout(function () { if (el && el.parentNode) el.parentNode.removeChild(el); }, 220); - } - - function shell(o) { - return ''; - } - - function open(o) { - if (!o) return; - closeModal(); - var ov = document.createElement('div'); - ov.className = 'vdl-overlay'; - ov.style.setProperty('--vdl-h', hueOf(o.title || '')); - ov.innerHTML = shell(o); - document.body.appendChild(ov); - document.body.style.overflow = 'hidden'; - modalEl = ov; - requestAnimationFrame(function () { ov.classList.add('vdl-open'); }); - - ov.addEventListener('click', function (e) { - if (e.target === ov || e.target.closest('[data-vdl-close]')) { closeModal(); return; } - var sb = e.target.closest('[data-vdl-search]'); - if (sb) { stubSearch(sb.getAttribute('data-vdl-search')); return; } - if (e.target.closest('[data-vdl-search-all]')) { stubSearch('*'); return; } - }); - keyHandler = function (e) { if (e.key === 'Escape') closeModal(); }; - document.addEventListener('keydown', keyHandler); - - load(o); - } - - function load(o) { - var isYt = o.kind === 'youtube'; - if (isYt) { - fillYt(o); - renderSources(['youtube']); - } else { - var url = (o.source === 'tmdb') - ? '/api/video/tmdb/' + o.kind + '/' + o.id - : '/api/video/detail/' + o.kind + '/' + o.id; - getJSON(url).then(function (d) { if (modalEl && d) fillDetail(d, o); }); - getJSON('/api/video/downloads/config').then(function (c) { if (modalEl) renderSources(sourcesFromConfig(c)); }); - } - var qurl = isYt ? '/api/video/downloads/youtube-quality' : '/api/video/downloads/quality'; - getJSON(qurl).then(function (p) { if (modalEl && p) renderTarget(p, isYt); }); - } - - function sourcesFromConfig(c) { - c = c || {}; - if (c.download_mode === 'hybrid' && Array.isArray(c.hybrid_order) && c.hybrid_order.length) return c.hybrid_order; - if (c.download_mode) return [c.download_mode]; - return ['soulseek']; - } - - function fillDetail(d, o) { - var q = function (s) { return modalEl.querySelector(s); }; - if (d.title) { var t = q('[data-vdl-title]'); if (t) t.textContent = d.title; modalEl.style.setProperty('--vdl-h', hueOf(d.title)); } - var hero = q('[data-vdl-hero]'); - var bg = (o.source !== 'tmdb' && d.has_backdrop) - ? '/api/video/backdrop/' + o.kind + '/' + o.id + '?w=1280' - : (d.backdrop_url || d.backdrop || ''); - if (hero && bg) hero.style.backgroundImage = "url('" + bg + "')"; - var poster = q('[data-vdl-poster]'); - var pUrl = (o.source !== 'tmdb' && d.has_poster) - ? '/api/video/poster/' + o.kind + '/' + o.id - : (d.poster_url || d.poster || ''); - if (poster && pUrl) { - poster.onload = function () { if (hero) hero.classList.add('vdl-has-poster'); poster.hidden = false; }; - poster.onerror = function () { poster.hidden = true; }; - poster.src = pUrl; - } - var meta = []; - if (d.year) meta.push(d.year); - if (d.runtime_minutes) meta.push(d.runtime_minutes + ' min'); - meta.push(o.kind === 'show' ? 'TV series' : 'Movie'); - var mt = q('[data-vdl-meta]'); if (mt) mt.textContent = meta.filter(Boolean).join(' · '); - if (o.kind === 'movie' && d.owned && d.file) renderOwned(d.file); - } - - function fillYt(o) { - var t = modalEl.querySelector('[data-vdl-title]'); if (t && o.title) t.textContent = o.title; - var hero = modalEl.querySelector('[data-vdl-hero]'); - if (hero && o.thumb) hero.style.backgroundImage = "url('" + o.thumb + "')"; - var mt = modalEl.querySelector('[data-vdl-meta]'); if (mt) mt.textContent = 'YouTube'; - } - - function chip(text, mod) { return '' + esc(text) + ''; } - - function renderTarget(p, isYt) { - var box = modalEl.querySelector('[data-vdl-target]'); if (!box) return; - var chips = []; - if (isYt) { - chips.push(chip('Up to ' + (p.max_resolution === 'best' ? 'best' : (p.max_resolution || '1080p')))); - if (p.video_codec && p.video_codec !== 'any') chips.push(chip('Prefer ' + p.video_codec.toUpperCase())); - if (p.container) chips.push(chip(p.container.toUpperCase())); - if (p.prefer_60fps) chips.push(chip('60fps')); - chips.push(chip(p.allow_hdr ? 'HDR ok' : 'SDR')); - } else { - chips.push(chip(p.cutoff_resolution ? 'Stop at ' + (CUT_LABEL[p.cutoff_resolution] || p.cutoff_resolution) : 'Always upgrade')); - if (p.prefer_codec && p.prefer_codec !== 'any') chips.push(chip('Prefer ' + (p.prefer_codec === 'hevc' ? 'HEVC' : p.prefer_codec.toUpperCase()))); - if (p.prefer_hdr === 'prefer') chips.push(chip('Prefer HDR')); - else if (p.prefer_hdr === 'require') chips.push(chip('HDR required', 'req')); - if (Array.isArray(p.rejects) && p.rejects.length) chips.push(chip('Reject ' + p.rejects.join(', '), 'rej')); - if (p.max_movie_gb) chips.push(chip('Movie ≤ ' + p.max_movie_gb + ' GB')); - if (p.max_episode_gb) chips.push(chip('Episode ≤ ' + p.max_episode_gb + ' GB')); - } - box.innerHTML = chips.join(''); - } - - // Owned copy → "In your library · 720p · BluRay · X265" + a verdict against the - // quality target (real: /downloads/evaluate). meets → reassuring; else upgrade. - function renderOwned(file) { - var box = modalEl.querySelector('[data-vdl-owned]'); if (!box) return; - var bits = [resLabel(file.resolution), file.release_source, (file.video_codec || '').toUpperCase()].filter(Boolean); - box.innerHTML = - '
' + - '' + - 'In your library' + (bits.length ? ' · ' + esc(bits.join(' · ')) : '') + '' + - 'checking…' + - '
' + - '
'; - box.hidden = false; - postJSON('/api/video/downloads/evaluate', { file: file }).then(function (v) { - if (!modalEl || !v) return; - var badge = box.querySelector('[data-vdl-verdict]'); - if (badge) { - badge.classList.remove('vdl-verdict--pending'); - badge.classList.add(v.meets ? 'vdl-verdict--ok' : 'vdl-verdict--up'); - badge.textContent = v.meets ? 'Meets your target' : 'Eligible for upgrade'; - } - var rs = box.querySelector('[data-vdl-reasons]'); - if (rs && v.reasons && v.reasons.length) { - rs.innerHTML = v.reasons.map(function (r) { - return '
' + - (r.ok ? '✓' : '↑') + ' ' + esc(r.text) + '
'; - }).join(''); - } - }); - } - - function renderSources(list) { - var box = modalEl.querySelector('[data-vdl-sources]'); if (!box) return; - list = (list || []).filter(function (s) { return SRC_META[s]; }); - if (!list.length) { - box.innerHTML = '
No download source configured — pick one on Settings → Downloads.
'; - return; - } - box.innerHTML = list.map(function (s) { - var m = SRC_META[s]; - return '
' + - '' + m.emoji + '' + - '' + esc(m.name) + '' + - 'Ready' + - '' + - '
'; - }).join(''); - } - - // Scaffold: flip the targeted row(s) to a "coming soon" state. No backend yet. - function stubSearch(which) { - if (!modalEl) return; - var sel = which === '*' ? '[data-vdl-src]' : '[data-vdl-src="' + which + '"]'; - var rows = modalEl.querySelectorAll(sel); - for (var i = 0; i < rows.length; i++) { - var st = rows[i].querySelector('[data-vdl-status]'); - if (st) { st.textContent = 'Search engine coming soon'; st.className = 'vdl-src-status vdl-src-status--soon'; } - } - toast('Automatic search isn’t wired up yet — coming soon', 'info'); - } - - window.VideoDownload = { open: open }; -})(); diff --git a/webui/static/video/video-download-view.js b/webui/static/video/video-download-view.js new file mode 100644 index 00000000..49529a84 --- /dev/null +++ b/webui/static/video/video-download-view.js @@ -0,0 +1,183 @@ +/* + * SoulSync — Download VIEW renderer (movie / TV show / YouTube). + * + * NOT its own modal: it renders the direct-download content INTO a container the + * caller owns — the get-modal swaps its detail body for this view (with a Back + * button) when you click "Download", and a future YouTube trigger can reuse it. + * + * v1 is VISUAL scaffolding: it shows the quality TARGET (read from the Settings → + * Downloads profile), judges any copy you ALREADY own against that target (real — + * via /downloads/evaluate), and lists each attached source with a per-source + * "Search" affordance. The searches are stubs — no backend yet (engine phase). + * + * VideoDownload.render(containerEl, { kind, id, source, isYt, file }). Self-contained. + */ +(function () { + 'use strict'; + + function esc(s) { + return String(s == null ? '' : s) + .replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); + } + function toast(msg, type) { if (typeof showToast === 'function') showToast(msg, type); } + function resLabel(res) { + if (!res) return ''; + res = String(res).toLowerCase(); + if (res.indexOf('2160') > -1 || res === '4k') return '4K'; + if (res.indexOf('1080') > -1) return '1080p'; + if (res.indexOf('720') > -1) return '720p'; + if (res.indexOf('480') > -1 || res.indexOf('576') > -1) return 'SD'; + return res.toUpperCase(); + } + var CUT_LABEL = { '2160p': '4K', '1080p': '1080p', '720p': '720p', '480p': 'SD' }; + var SRC_META = { + soulseek: { name: 'Soulseek', emoji: '🎵' }, + torrent: { name: 'Torrent', emoji: '🧲' }, + usenet: { name: 'Usenet', emoji: '📰' }, + youtube: { name: 'YouTube', emoji: '▶' } + }; + + function getJSON(url) { + return fetch(url, { headers: { Accept: 'application/json' } }) + .then(function (r) { return r.ok ? r.json() : null; }).catch(function () { return null; }); + } + function postJSON(url, body) { + return fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, + body: JSON.stringify(body) }).then(function (r) { return r.ok ? r.json() : null; }).catch(function () { return null; }); + } + + function contentHTML() { + return '
' + + '
Quality target
' + + '
Loading…
' + + '
' + + '' + + '
' + + '
' + + '
Sources
' + + '' + + '
' + + '
Loading sources…
' + + '
Automatic searching arrives with the download engine — this is the layout it\'ll drive.
' + + '
'; + } + + function onClick(e) { + var container = e.currentTarget; + var sb = e.target.closest('[data-vdl-search]'); + if (sb) { stubSearch(container, sb.getAttribute('data-vdl-search')); return; } + if (e.target.closest('[data-vdl-search-all]')) { stubSearch(container, '*'); } + } + + // Render the download view into `container`. Re-callable (resets each time). + function render(container, opts) { + if (!container) return; + opts = opts || {}; + container.innerHTML = contentHTML(); + if (!container._vdlWired) { container._vdlWired = true; container.addEventListener('click', onClick); } + + var isYt = !!opts.isYt; + getJSON(isYt ? '/api/video/downloads/youtube-quality' : '/api/video/downloads/quality') + .then(function (p) { if (container.isConnected && p) renderTarget(container, p, isYt); }); + if (isYt) { + renderSources(container, ['youtube']); + } else { + getJSON('/api/video/downloads/config').then(function (c) { + if (container.isConnected) renderSources(container, sourcesFromConfig(c)); + }); + if (opts.file) renderOwned(container, opts.file); + } + } + + function sourcesFromConfig(c) { + c = c || {}; + if (c.download_mode === 'hybrid' && Array.isArray(c.hybrid_order) && c.hybrid_order.length) return c.hybrid_order; + if (c.download_mode) return [c.download_mode]; + return ['soulseek']; + } + + function chip(text, mod) { return '' + esc(text) + ''; } + + function renderTarget(container, p, isYt) { + var box = container.querySelector('[data-vdl-target]'); if (!box) return; + var chips = []; + if (isYt) { + chips.push(chip('Up to ' + (p.max_resolution === 'best' ? 'best' : (p.max_resolution || '1080p')))); + if (p.video_codec && p.video_codec !== 'any') chips.push(chip('Prefer ' + p.video_codec.toUpperCase())); + if (p.container) chips.push(chip(p.container.toUpperCase())); + if (p.prefer_60fps) chips.push(chip('60fps')); + chips.push(chip(p.allow_hdr ? 'HDR ok' : 'SDR')); + } else { + chips.push(chip(p.cutoff_resolution ? 'Stop at ' + (CUT_LABEL[p.cutoff_resolution] || p.cutoff_resolution) : 'Always upgrade')); + if (p.prefer_codec && p.prefer_codec !== 'any') chips.push(chip('Prefer ' + (p.prefer_codec === 'hevc' ? 'HEVC' : p.prefer_codec.toUpperCase()))); + if (p.prefer_hdr === 'prefer') chips.push(chip('Prefer HDR')); + else if (p.prefer_hdr === 'require') chips.push(chip('HDR required', 'req')); + if (Array.isArray(p.rejects) && p.rejects.length) chips.push(chip('Reject ' + p.rejects.join(', '), 'rej')); + if (p.max_movie_gb) chips.push(chip('Movie ≤ ' + p.max_movie_gb + ' GB')); + if (p.max_episode_gb) chips.push(chip('Episode ≤ ' + p.max_episode_gb + ' GB')); + } + box.innerHTML = chips.join(''); + } + + // Owned copy → "In your library · 720p · BluRay · X265" + a verdict against the + // quality target (real: /downloads/evaluate). meets → reassuring; else upgrade. + function renderOwned(container, file) { + var box = container.querySelector('[data-vdl-owned]'); if (!box) return; + var bits = [resLabel(file.resolution), file.release_source, (file.video_codec || '').toUpperCase()].filter(Boolean); + box.innerHTML = + '
' + + '' + + 'In your library' + (bits.length ? ' · ' + esc(bits.join(' · ')) : '') + '' + + 'checking…' + + '
' + + '
'; + box.hidden = false; + postJSON('/api/video/downloads/evaluate', { file: file }).then(function (v) { + if (!container.isConnected || !v) return; + var badge = box.querySelector('[data-vdl-verdict]'); + if (badge) { + badge.classList.remove('vdl-verdict--pending'); + badge.classList.add(v.meets ? 'vdl-verdict--ok' : 'vdl-verdict--up'); + badge.textContent = v.meets ? 'Meets your target' : 'Eligible for upgrade'; + } + var rs = box.querySelector('[data-vdl-reasons]'); + if (rs && v.reasons && v.reasons.length) { + rs.innerHTML = v.reasons.map(function (r) { + return '
' + + (r.ok ? '✓' : '↑') + ' ' + esc(r.text) + '
'; + }).join(''); + } + }); + } + + function renderSources(container, list) { + var box = container.querySelector('[data-vdl-sources]'); if (!box) return; + list = (list || []).filter(function (s) { return SRC_META[s]; }); + if (!list.length) { + box.innerHTML = '
No download source configured — pick one on Settings → Downloads.
'; + return; + } + box.innerHTML = list.map(function (s) { + var m = SRC_META[s]; + return '
' + + '' + m.emoji + '' + + '' + esc(m.name) + '' + + 'Ready' + + '' + + '
'; + }).join(''); + } + + // Scaffold: flip the targeted row(s) to a "coming soon" state. No backend yet. + function stubSearch(container, which) { + var sel = which === '*' ? '[data-vdl-src]' : '[data-vdl-src="' + which + '"]'; + var rows = container.querySelectorAll(sel); + for (var i = 0; i < rows.length; i++) { + var st = rows[i].querySelector('[data-vdl-status]'); + if (st) { st.textContent = 'Search engine coming soon'; st.className = 'vdl-src-status vdl-src-status--soon'; } + } + toast('Automatic search isn’t wired up yet — coming soon', 'info'); + } + + window.VideoDownload = { render: render }; +})(); diff --git a/webui/static/video/video-get-modal.js b/webui/static/video/video-get-modal.js index 091d8fa9..b8ca3d64 100644 --- a/webui/static/video/video-get-modal.js +++ b/webui/static/video/video-get-modal.js @@ -64,6 +64,38 @@ setTimeout(function () { if (el && el.parentNode) el.parentNode.removeChild(el); }, 220); } + // ── download view (in-place, same modal) ────────────────────────────────── + // Clicking "Download" swaps the detail body for the download view (target + + // owned verdict + per-source search) without leaving the modal; Back restores + // it. Selection-only sections collapse; the all-related movie bits stay. + var DL_HIDE = ['[data-vgm-eps]', '[data-vgm-next]', '[data-vgm-follow]']; + + function setDownloadMode(ov, on) { + DL_HIDE.forEach(function (sel) { + var el = ov.querySelector(sel); if (!el) return; + if (on) { el.setAttribute('data-vgm-washidden', el.hidden ? '1' : '0'); el.hidden = true; } + else { el.hidden = el.getAttribute('data-vgm-washidden') === '1'; el.removeAttribute('data-vgm-washidden'); } + }); + var act = ov.querySelector('.vgm-actions'); if (act) act.hidden = on; + ov.classList.toggle('vgm-mode-download', on); + } + + function enterDownload(ov, o) { + 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 }); + setDownloadMode(ov, true); + dl.hidden = false; + var modal = ov.querySelector('.vgm-modal'); if (modal) modal.scrollTop = 0; + } + + function exitDownload(ov) { + var dl = ov.querySelector('[data-vgm-dl]'); if (dl) dl.hidden = true; + setDownloadMode(ov, false); + } + // ── wishlist / watchlist writes ─────────────────────────────────────────── function postJSON(url, body) { return fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -161,6 +193,10 @@ '' + '' + '' + + '' + '' + '
' + '' + @@ -201,19 +237,8 @@ } return; } - if (e.target.closest('[data-vgm-download]')) { - // Hand off to the universal Download modal (movie/show/youtube). Pass - // what we know; it fetches its own detail + quality verdict. - var st = modalState || {}; - closeModal(); - if (window.VideoDownload) { - VideoDownload.open({ kind: o.kind, id: o.id, source: o.source || 'library', - title: st.title || o.title || '' }); - } else { - toast('Download module not loaded', 'error'); - } - return; - } + if (e.target.closest('[data-vgm-download]')) { enterDownload(ov, o); return; } + if (e.target.closest('[data-vgm-back]')) { exitDownload(ov); return; } if (e.target.closest('[data-vgm-wishlist]')) { submitWishlist(ov); } }); ov.addEventListener('change', function (e) { @@ -491,7 +516,8 @@ } } else { modalState = { kind: 'movie', owned: !!d.owned, tmdbId: d.tmdb_id, title: d.title, - year: d.year || null, poster: pUrl || null, libraryId: libId }; // owned → re-download + year: d.year || null, poster: pUrl || null, libraryId: libId, + file: d.file || null }; // owned → re-download; file feeds the download verdict renderOwned(d); } updateFooter(); diff --git a/webui/static/video/video-side.css b/webui/static/video/video-side.css index b1c00596..10617a0a 100644 --- a/webui/static/video/video-side.css +++ b/webui/static/video/video-side.css @@ -3019,39 +3019,15 @@ body[data-side="video"] #soulsync-toggle { display: none; } .vgm-download-btn { display: inline-flex; align-items: center; gap: 7px; } .vgm-download-btn .vgm-download-ic { font-size: 15px; font-weight: 900; line-height: 1; } -/* ── universal Download modal (.vdl-*) — mirrors the get-modal vibe ────────── */ -.vdl-overlay { position: fixed; inset: 0; z-index: 9200; display: flex; align-items: center; justify-content: center; - padding: 24px; background: rgba(4, 4, 7, 0.78); backdrop-filter: blur(10px); opacity: 0; transition: opacity 0.2s ease; } -.vdl-overlay.vdl-open { opacity: 1; } -.vdl-modal { position: relative; width: min(600px, 100%); max-height: 90vh; overflow-y: auto; - border-radius: 22px; background: #101015; border: 1px solid rgba(255, 255, 255, 0.08); - box-shadow: 0 50px 130px rgba(0, 0, 0, 0.72), 0 0 90px -30px hsla(var(--vdl-h, 220), 70%, 50%, 0.45); - transform: translateY(16px) scale(0.985); transition: transform 0.26s cubic-bezier(0.2, 0.7, 0.2, 1); } -.vdl-overlay.vdl-open .vdl-modal { transform: none; } -.vdl-modal::-webkit-scrollbar { width: 9px; } -.vdl-modal::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.14); border-radius: 5px; } -.vdl-close { position: absolute; top: 14px; right: 14px; z-index: 3; width: 36px; height: 36px; border-radius: 50%; - border: 1px solid rgba(255, 255, 255, 0.18); background: rgba(0, 0, 0, 0.45); color: #fff; font-size: 22px; - cursor: pointer; backdrop-filter: blur(6px); display: flex; align-items: center; justify-content: center; - line-height: 1; transition: all 0.15s ease; } -.vdl-close:hover { background: rgba(0, 0, 0, 0.72); border-color: rgba(255, 255, 255, 0.4); } -.vdl-hero { position: relative; min-height: 188px; border-radius: 22px 22px 0 0; overflow: hidden; - display: flex; align-items: flex-end; background-size: cover; background-position: center 24%; background-color: #16161d; } -.vdl-hero-scrim { position: absolute; inset: 0; - background: linear-gradient(0deg, #101015 5%, rgba(16, 16, 21, 0.55) 55%, rgba(16, 16, 21, 0.08)), - radial-gradient(130% 120% at 0% 100%, hsla(var(--vdl-h, 220), 70%, 50%, 0.34), transparent 60%); } -.vdl-hero-content { position: relative; z-index: 1; padding: 22px 26px 18px; width: 100%; } -.vdl-eyebrow { font-size: 12px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.06em; - color: hsla(var(--vdl-h, 220), 80%, 78%, 0.95); } -.vdl-title { font-size: 26px; font-weight: 900; letter-spacing: -0.025em; line-height: 1.06; margin: 7px 0 0; - text-shadow: 0 2px 18px rgba(0, 0, 0, 0.55); } -.vdl-poster { position: absolute; left: 26px; bottom: 16px; z-index: 2; width: 90px; aspect-ratio: 2 / 3; - object-fit: cover; border-radius: 9px; box-shadow: 0 12px 30px rgba(0, 0, 0, 0.6); border: 1px solid rgba(255, 255, 255, 0.12); } -.vdl-poster[hidden] { display: none; } -.vdl-hero.vdl-has-poster .vdl-hero-content { padding-left: 134px; } -.vdl-meta { display: flex; gap: 14px; margin-top: 9px; flex-wrap: wrap; font-size: 12.5px; font-weight: 700; - color: rgba(255, 255, 255, 0.72); } -.vdl-body { padding: 20px 26px 26px; } +/* ── in-place download VIEW inside the get-modal (.vgm-dl + .vdl-* content) ─── */ +.vgm-actions[hidden] { display: none; } +.vgm-dl[hidden] { display: none; } +.vgm-dl { animation: vgmRise 0.4s cubic-bezier(0.2, 0.7, 0.2, 1) both; } +.vgm-back { display: inline-flex; align-items: center; gap: 6px; margin-bottom: 16px; padding: 7px 13px; + border-radius: 9px; cursor: pointer; font-size: 12.5px; font-weight: 700; + background: rgba(255, 255, 255, 0.06); border: 1px solid rgba(255, 255, 255, 0.14); color: rgba(255, 255, 255, 0.85); + transition: all 0.15s ease; } +.vgm-back:hover { background: rgba(255, 255, 255, 0.12); border-color: rgba(255, 255, 255, 0.28); color: #fff; } .vdl-section { margin-bottom: 20px; } .vdl-section:last-child { margin-bottom: 0; } .vdl-sec-label { font-size: 12px; font-weight: 800; letter-spacing: 0.05em; text-transform: uppercase; @@ -3061,7 +3037,7 @@ body[data-side="video"] #soulsync-toggle { display: none; } /* target chips */ .vdl-chips { display: flex; flex-wrap: wrap; gap: 8px; } .vdl-chip { padding: 6px 12px; border-radius: 999px; font-size: 12.5px; font-weight: 700; - background: hsla(var(--vdl-h, 220), 45%, 50%, 0.16); border: 1px solid hsla(var(--vdl-h, 220), 50%, 60%, 0.32); + background: hsla(var(--vgm-h, 220), 45%, 50%, 0.16); border: 1px solid hsla(var(--vgm-h, 220), 50%, 60%, 0.32); color: #fff; } .vdl-chip--ghost { background: rgba(255, 255, 255, 0.05); border-color: rgba(255, 255, 255, 0.1); color: rgba(255, 255, 255, 0.5); } .vdl-chip--rej { background: rgba(239, 68, 68, 0.16); border-color: rgba(239, 68, 68, 0.4); color: #fecaca; } @@ -3104,6 +3080,4 @@ body[data-side="video"] #soulsync-toggle { display: none; } font-style: italic; } @media (max-width: 560px) { .vdl-src { flex-wrap: wrap; } - .vdl-hero.vdl-has-poster .vdl-hero-content { padding-left: 110px; } - .vdl-poster { width: 72px; left: 20px; } }