video search: per-source result panels + redesigned, readable cards

- Each source now has its OWN results panel under its row (movie + episode), and
  the search passes the source through — so Soulseek / Torrent / Usenet each show
  their own distinct hits instead of one shared identical list.
- Card redesign for readability: a big colour-coded RESOLUTION tile (4K gold /
  1080p accent / 720p blue / SD) anchors each row; a plain-English quality summary
  leads (e.g. 'BluRay · X265 · DTS-HD' + an HDR/REPACK tag); the raw release name is
  demoted to a single muted truncated line; size · seeders · group sit below; the
  verdict pill (✓ Meets profile / ✕ reason) and ⤓ Grab stay on the right.

Dropped the old break-all mono title + badge-row layout. JS/CSS balance clean.
This commit is contained in:
BoulderBadgeDad 2026-06-19 13:58:43 -07:00
parent 3490092e3d
commit bf17ef3fab
2 changed files with 97 additions and 75 deletions

View file

@ -58,20 +58,42 @@
'<button class="vdl-search-all" type="button" data-vdl-search-all>⌕ Search all</button>' +
'</div>' +
'<div class="vdl-sources" data-vdl-sources><div class="vdl-src-empty">Loading sources…</div></div>' +
'<div class="vdl-results" data-vdl-results hidden></div>' +
'</div>';
}
// One source = a row + its OWN results panel (so each source shows its own hits).
function srcRowHTML(s, mini) {
var m = SRC_META[s];
return '<div class="vdl-src' + (mini ? ' 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>';
}
function srcBlockHTML(s, mini) {
return '<div class="vdl-src-block" data-vdl-src-block="' + s + '">' +
srcRowHTML(s, mini) +
'<div class="vdl-results" data-vdl-results-for="' + s + '" hidden></div>' +
'</div>';
}
function _movieSearch(container, block) {
var o = container._opts || {};
var s = block.getAttribute('data-vdl-src-block');
searchInto(container, block.querySelector('[data-vdl-results-for="' + s + '"]'),
{ scope: 'movie', title: o.title || '', year: o.year || null, source: s },
[block.querySelector('.vdl-src')]);
}
function onClick(e) {
var container = e.currentTarget;
if (e.target.closest('[data-vdl-grab]')) { toast('Grabbing isnt wired up yet — coming soon', 'info'); return; }
var o = container._opts || {};
var params = { scope: 'movie', title: o.title || '', year: o.year || null };
var results = container.querySelector('[data-vdl-results]');
var sb = e.target.closest('[data-vdl-search]');
if (sb) { searchInto(container, results, params, [sb.closest('.vdl-src')]); return; }
if (sb) { _movieSearch(container, sb.closest('[data-vdl-src-block]')); return; }
if (e.target.closest('[data-vdl-search-all]')) {
searchInto(container, results, params, Array.prototype.slice.call(container.querySelectorAll('.vdl-src')));
Array.prototype.forEach.call(container.querySelectorAll('[data-vdl-src-block]'),
function (block) { _movieSearch(container, block); });
}
}
@ -165,18 +187,7 @@
box.innerHTML = '<div class="vdl-src-empty">No download source configured — pick one on Settings → Downloads.</div>';
return;
}
box.innerHTML = list.map(function (s) {
var m = SRC_META[s];
return '<div class="vdl-src" 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('');
box.innerHTML = list.map(function (s) { return srcBlockHTML(s, false); }).join('');
}
// Scaffold: a satisfying faux-scan (animated) that resolves to "coming soon".
@ -222,30 +233,37 @@
: s === 'episode' ? 'this episode' : 'for the movie';
}
function rb(text, mod) { return '<span class="vdl-rb' + (mod ? ' vdl-rb--' + mod : '') + '">' + esc(text) + '</span>'; }
function resKind(res) {
return res === '2160p' ? '4k' : res === '1080p' ? '1080' : res === '720p' ? '720' : 'sd';
}
// Readable card: a big resolution tile anchors it, a plain-English quality summary
// leads, the raw release name is demoted to a muted one-liner, then size/seeders.
function resultCardHTML(r) {
var badges = [];
if (r.resolution) badges.push(rb(RES_LABEL[r.resolution] || r.resolution, 'res'));
if (r.source) badges.push(rb(SRC_LABEL[r.source] || r.source, 'src'));
if (r.codec) badges.push(rb(String(r.codec).toUpperCase()));
if (r.hdr) badges.push(rb(String(r.hdr).toUpperCase(), 'hdr'));
if (r.audio) badges.push(rb(String(r.audio).toUpperCase().replace('-', ' ')));
if (r.repack) badges.push(rb('REPACK'));
var summary = [SRC_LABEL[r.source] || r.source,
r.codec ? String(r.codec).toUpperCase() : '',
r.audio ? String(r.audio).toUpperCase().replace('-', ' ') : ''].filter(Boolean).join(' · ');
var tags = '';
if (r.hdr) tags += '<span class="vdl-res-tag vdl-res-tag--hdr">' + esc(String(r.hdr).toUpperCase()) + '</span>';
if (r.repack) tags += '<span class="vdl-res-tag">REPACK</span>';
var verdict = r.accepted
? '<span class="vdl-res-verdict vdl-res-verdict--ok">✓ Meets profile</span>'
: '<span class="vdl-res-verdict vdl-res-verdict--no">✕ ' + esc(r.rejected || 'Filtered') + '</span>';
: '<span class="vdl-res-verdict vdl-res-verdict--no" title="' + esc(r.rejected || '') + '">✕ ' + esc(r.rejected || 'Filtered') + '</span>';
return '<div class="vdl-res' + (r.accepted ? '' : ' vdl-res--rejected') + '">' +
'<div class="vdl-res-top">' +
'<div class="vdl-res-title" title="' + esc(r.title) + '">' + esc(r.title) + '</div>' +
(r.accepted ? '<button class="vdl-res-grab" type="button" data-vdl-grab title="Grab this release">⤓</button>' : '') +
'</div>' +
'<div class="vdl-res-badges">' + badges.join('') + '</div>' +
'<div class="vdl-res-foot">' +
'<span class="vdl-res-stat">' + r.size_gb + ' GB</span>' +
'<span class="vdl-res-stat vdl-res-seed">▲ ' + (r.seeders || 0) + '</span>' +
verdict +
'<div class="vdl-res-res vdl-res-res--' + resKind(r.resolution) + '">' + esc(RES_LABEL[r.resolution] || r.resolution || '?') + '</div>' +
'<div class="vdl-res-body">' +
'<div class="vdl-res-line1">' +
'<span class="vdl-res-summary">' + esc(summary) + '</span>' + tags +
verdict +
'</div>' +
'<div class="vdl-res-name" title="' + esc(r.title) + '">' + esc(r.title) + '</div>' +
'<div class="vdl-res-meta">' +
'<span class="vdl-res-stat"><span class="vdl-res-ico">💾</span>' + r.size_gb + ' GB</span>' +
'<span class="vdl-res-stat vdl-res-seed">▲ ' + (r.seeders || 0) + ' seeders</span>' +
(r.group ? '<span class="vdl-res-stat vdl-res-grp">' + esc(r.group) + '</span>' : '') +
'</div>' +
'</div>' +
(r.accepted ? '<button class="vdl-res-grab" type="button" data-vdl-grab title="Grab this release">⤓</button>' : '') +
'</div>';
}
@ -384,13 +402,15 @@
function onShowClick(e) {
var container = e.currentTarget; var st = container._dl; if (!st) return;
if (e.target.closest('[data-vdl-grab]')) { toast('Grabbing isnt wired up yet — coming soon', 'info'); return; }
// Episode-scope search (a source row inside an expanded episode).
// Episode-scope search (a source row inside an expanded episode → its own panel).
var srch = e.target.closest('[data-vdl-search]');
if (srch) {
var epEl = srch.closest('.vdl-ep'); if (!epEl) return;
var parts = (epEl.getAttribute('data-vdl-ep') || '').split('_');
searchInto(container, epEl.querySelector('[data-vdl-ep-results]'),
{ scope: 'episode', title: st.title, season: +parts[0], episode: +parts[1] },
var s = srch.getAttribute('data-vdl-search');
var block = srch.closest('[data-vdl-src-block]');
searchInto(container, block.querySelector('[data-vdl-results-for="' + s + '"]'),
{ scope: 'episode', title: st.title, season: +parts[0], episode: +parts[1], source: s },
[srch.closest('.vdl-src')]);
return;
}
@ -444,16 +464,7 @@
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>' +
'<div class="vdl-results" data-vdl-ep-results hidden></div>';
panel.innerHTML = '<div class="vdl-ep-srcs">' + srcs.map(function (s) { return srcBlockHTML(s, true); }).join('') + '</div>';
}
function setSeasonSel(container, sn, on) {

View file

@ -3253,8 +3253,7 @@ body[data-side="video"] #soulsync-toggle { display: none; }
.vdl-results { margin-top: 12px; }
.vdl-results[hidden] { display: none; }
.vdl-results--season { margin: 2px 14px 12px 14px; }
.vdl-ep-search .vdl-results { padding: 0 14px 12px 42px; margin-top: 2px; }
.vdl-ep--open .vdl-ep-search { max-height: 1800px; } /* room for result cards */
.vdl-ep--open .vdl-ep-search { max-height: 2400px; } /* room for result cards */
.vdl-res-loading { display: flex; align-items: center; gap: 9px; font-size: 12.5px; font-weight: 600;
color: rgba(255, 255, 255, 0.6); padding: 9px 2px; }
.vdl-res-spin { width: 14px; height: 14px; border-radius: 50%; flex-shrink: 0;
@ -3264,32 +3263,44 @@ body[data-side="video"] #soulsync-toggle { display: none; }
.vdl-res-head { font-size: 11.5px; font-weight: 700; color: rgba(255, 255, 255, 0.5); margin: 4px 0 10px; }
.vdl-res-head strong { color: #fff; }
.vdl-res-okn { color: #8fe7af; }
.vdl-res { border-radius: 12px; border: 1px solid rgba(255, 255, 255, 0.08); background: rgba(255, 255, 255, 0.03);
padding: 11px 13px; margin-bottom: 8px; animation: vdlRise 0.4s both;
/* result row: [resolution tile] [body: summary + name + meta] [grab] */
.vdl-res { display: flex; align-items: stretch; gap: 12px; border-radius: 13px;
border: 1px solid rgba(255, 255, 255, 0.08); background: rgba(255, 255, 255, 0.035);
padding: 11px 12px; margin-bottom: 8px; animation: vdlRise 0.4s both;
transition: border-color 0.15s ease, background 0.15s ease, transform 0.15s ease; }
.vdl-res:hover { border-color: rgba(var(--accent-rgb), 0.4); background: rgba(255, 255, 255, 0.055); transform: translateY(-1px); }
.vdl-res:hover { border-color: rgba(var(--accent-rgb), 0.4); background: rgba(255, 255, 255, 0.06); transform: translateY(-1px); }
.vdl-res--rejected { opacity: 0.5; }
.vdl-res--rejected:hover { opacity: 0.78; }
.vdl-res-top { display: flex; align-items: flex-start; gap: 10px; }
.vdl-res-title { flex: 1; min-width: 0; font: 600 11.5px/1.45 'JetBrains Mono', ui-monospace, monospace;
color: rgba(255, 255, 255, 0.9); word-break: break-all; }
.vdl-res-grab { flex-shrink: 0; width: 30px; height: 30px; border-radius: 9px; cursor: pointer; font-size: 15px;
font-weight: 900; background: rgb(var(--accent-rgb)); color: #06210f; border: none;
transition: filter 0.15s ease, transform 0.15s ease; }
.vdl-res-grab:hover { filter: brightness(1.12); transform: translateY(-1px); }
.vdl-res-badges { display: flex; flex-wrap: wrap; gap: 6px; margin: 9px 0; }
.vdl-rb { font-size: 10px; font-weight: 800; letter-spacing: 0.02em; padding: 2px 8px; border-radius: 6px;
text-transform: uppercase; background: rgba(255, 255, 255, 0.07); border: 1px solid rgba(255, 255, 255, 0.1);
color: rgba(255, 255, 255, 0.8); }
.vdl-rb--res { background: rgba(var(--accent-rgb), 0.2); border-color: rgba(var(--accent-rgb), 0.42); color: #fff; }
.vdl-rb--src { background: rgba(255, 255, 255, 0.12); color: #fff; }
.vdl-rb--hdr { background: linear-gradient(100deg, rgba(245, 158, 11, 0.28), rgba(168, 85, 247, 0.28));
border-color: rgba(245, 158, 11, 0.4); color: #fde2b3; }
.vdl-res-foot { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
.vdl-res-stat { font-size: 11.5px; font-weight: 700; color: rgba(255, 255, 255, 0.55); }
.vdl-res--rejected:hover { opacity: 0.82; }
/* resolution tile — the quick-scan anchor */
.vdl-res-res { flex-shrink: 0; width: 52px; display: flex; align-items: center; justify-content: center;
border-radius: 10px; font-size: 14px; font-weight: 900; letter-spacing: -0.02em; color: #06210f; }
.vdl-res-res--4k { background: linear-gradient(140deg, #ffd76a, #f0a830); }
.vdl-res-res--1080 { background: linear-gradient(140deg, rgba(var(--accent-rgb), 1), rgba(var(--accent-rgb), 0.7)); }
.vdl-res-res--720 { background: linear-gradient(140deg, #8fb3ff, #5f86d6); }
.vdl-res-res--sd { background: linear-gradient(140deg, rgba(255,255,255,0.4), rgba(255,255,255,0.22)); color: #11141a; }
.vdl-res-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 5px; }
.vdl-res-line1 { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.vdl-res-summary { font-size: 13.5px; font-weight: 800; color: #fff; letter-spacing: -0.01em; }
.vdl-res-tag { font-size: 9.5px; font-weight: 800; letter-spacing: 0.03em; padding: 2px 7px; border-radius: 5px;
text-transform: uppercase; background: rgba(255, 255, 255, 0.1); color: rgba(255, 255, 255, 0.85); }
.vdl-res-tag--hdr { background: linear-gradient(100deg, rgba(245, 158, 11, 0.4), rgba(168, 85, 247, 0.4)); color: #fff; }
.vdl-res-verdict { margin-left: auto; font-size: 10.5px; font-weight: 800; padding: 3px 9px; border-radius: 999px; white-space: nowrap; }
.vdl-res-verdict--ok { background: rgba(108, 211, 145, 0.18); color: #8fe7af; }
.vdl-res-verdict--no { background: rgba(245, 158, 11, 0.14); color: #fbcd7a; max-width: 60%; overflow: hidden; text-overflow: ellipsis; }
.vdl-res-name { font: 500 11px/1.3 'JetBrains Mono', ui-monospace, monospace; color: rgba(255, 255, 255, 0.42);
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.vdl-res-meta { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; margin-top: 1px; }
.vdl-res-stat { display: inline-flex; align-items: center; gap: 4px; font-size: 11.5px; font-weight: 700;
color: rgba(255, 255, 255, 0.6); }
.vdl-res-ico { font-size: 11px; opacity: 0.8; }
.vdl-res-seed { color: #8fe7af; }
.vdl-res-verdict { margin-left: auto; font-size: 10.5px; font-weight: 800; padding: 3px 9px; border-radius: 999px; }
.vdl-res-verdict--ok { background: rgba(108, 211, 145, 0.16); color: #8fe7af; }
.vdl-res-verdict--no { background: rgba(255, 255, 255, 0.06); color: rgba(255, 255, 255, 0.5); }
.vdl-res-grp { font-family: 'JetBrains Mono', ui-monospace, monospace; color: rgba(255, 255, 255, 0.4); font-weight: 600; }
.vdl-res-grab { flex-shrink: 0; align-self: center; width: 34px; height: 34px; border-radius: 10px; cursor: pointer;
font-size: 16px; font-weight: 900; background: rgb(var(--accent-rgb)); color: #06210f; border: none;
transition: filter 0.15s ease, transform 0.15s ease; }
.vdl-res-grab:hover { filter: brightness(1.12); transform: translateY(-1px) scale(1.04); }
.vdl-btn--busy { opacity: 0.7; cursor: progress; }
/* per-source block (row + its own results) */
.vdl-src-block { display: flex; flex-direction: column; }
.vdl-src-block .vdl-results { margin: 8px 0 2px; }
@media (prefers-reduced-motion: reduce) { .vdl-res, .vdl-res-spin { animation: none !important; } }