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 '
' +
- '
' +
- '
' +
- '
' +
- '
![]()
' +
- '
' +
- '
β€ Download
' +
- '
' + esc(o.title || 'Loadingβ¦') + '
' +
- '
' +
- '
' +
- '
' +
- '
' +
- '
' +
- '
Quality target
' +
- '
Loadingβ¦
' +
- '
' +
- '
' +
- '
' +
- '
' +
- '
Sources
' +
- '
' +
- '
' +
- '
' +
- '' +
- '
' +
- '
' +
- '
';
- }
-
- 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
' +
+ '
' +
+ '
' +
+ '
' +
+ '' +
+ '
';
+ }
+
+ 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; }
}