' +
@@ -162,13 +183,18 @@
var panel = byId('vem-panel');
if (!panel) return;
// Theme the panel to the selected worker's accent (like the music modal).
+ // Music's shared .em-btn/.em-tab/.em-seg rules key off --accent-rgb, so set
+ // it here too (scoped to the panel) — that's what makes them recolor.
var w = WORKERS.find(function (x) { return x.id === state.selected; }) || WORKERS[0];
panel.style.setProperty('--em-accent', w.color);
panel.style.setProperty('--em-accent-rgb', w.rgb);
+ panel.style.setProperty('--accent-rgb', w.rgb);
+ panel.style.setProperty('--accent-light-rgb', w.rgb);
if (state.selected === 'youtube') { renderYoutubePanel(panel); return; }
panel.innerHTML =
'' +
- '
Coverage' +
+ '
Coverage & processing order ' +
+ '— click a group to enrich it first' +
'
' +
'
' +
'
' +
@@ -181,21 +207,34 @@
renderList();
}
+ function heroHtml(w, s) {
+ var info = statusInfo(s);
+ var live = info.cls === 'running' ? ' em-hero--live' : '';
+ var item = s && s.current_item;
+ var sub = (item && item.name)
+ ? 'Now enriching:
' + esc(item.name) + '' +
+ (item.type ? '
(' + esc(item.type) + ')' : '')
+ : '
No item processing';
+ var pauseLabel = s && s.paused ? '▶ Resume' : '⏸ Pause';
+ var go = s && s.paused ? ' em-btn--go' : '';
+ return '
' +
+ '
' +
+ workerIcon(w, true) +
+ '
' +
+ '' + esc(w.name) + ' enrichment' +
+ '' + info.label + '
' +
+ '
' + sub + '
' +
+ '
' +
+ '' +
+ '
';
+ }
+
function renderHeader() {
var host = byId('vem-panel-header');
if (!host) return;
var s = state.statuses[state.selected] || {};
- var info = statusInfo(s);
var w = WORKERS.find(function (x) { return x.id === state.selected; }) || {};
- var pauseLabel = s.paused ? '▶ Resume' : '⏸ Pause';
- var current = (s.current_item && s.current_item.name)
- ? esc((s.current_item.type || '') + ': ' + s.current_item.name) : '';
- host.innerHTML =
- '
' +
- '' + esc(w.name) + '' +
- '' + info.label + '' +
- (current ? '' + current + '' : '') + '
' +
- '
';
+ host.innerHTML = heroHtml(w, s);
}
function renderCards() {
@@ -213,10 +252,21 @@
var total = matched + nf + pend;
var pct = total ? Math.round(matched / total * 100) : 0;
var seg = function (n) { return total ? (n / total) * 100 : 0; };
- var active = e === state.kind ? ' em-card--current' : '';
- return '
' +
+ '
' + (isEpisode
+ ? 'Episode stills backfill automatically once their show is matched.'
+ : 'Failed lookups auto-retry later · “Retry” re-queues immediately.') + '
';
}
function renderList() {
var host = byId('vem-unmatched-list');
if (!host) return;
var data = state.unmatched || { items: [], total: 0 };
+ var isEpisode = state.kind === 'episode';
if (!data.items.length) {
- host.innerHTML = '
Nothing unmatched here 🎉
';
+ host.innerHTML = '
' +
+ (state.statusFilter === 'unmatched' ? '🎉' : '🔍') + '
' +
+ '
' + (state.statusFilter === 'unmatched'
+ ? 'Everything here is matched for this source.'
+ : 'Nothing matches this filter.') + '
';
} else {
+ // No per-item status flag, so colour the stripe from the active filter.
+ var rowCls = state.statusFilter === 'not_found' ? 'em-row em-row--nf' : 'em-row em-row--pend';
host.innerHTML = data.items.map(function (it) {
- var poster = it.has_poster
- ? '

'
- : '
' + (GLYPH[state.kind] || '•') + '';
- return '
' + poster +
- '' + esc(it.title) + '' +
- '' + (it.year || '') + '' +
- 'Retry
';
+ var glyph = GLYPH[state.kind] || '•';
+ var pic = it.has_poster
+ ? '

'
+ : '';
+ var img = '
' + glyph + pic + '
';
+ var rel = relTime(it.last_attempted);
+ var meta = [];
+ if (it.year) meta.push('
' + esc(String(it.year)) + '');
+ meta.push('
' + (rel ? 'tried ' + rel : 'never tried') + '');
+ var action = isEpisode ? ''
+ : '
Retry';
+ return '
' + img +
+ '
' + esc(it.title) + '
' +
+ '
' + meta.join(' ') + '
' +
+ '
' + action + '
';
}).join('');
}
var pager = byId('vem-pager');
if (pager) {
- var pages = Math.max(1, Math.ceil((data.total || 0) / state.pageSize));
- pager.innerHTML = (data.total || 0) > state.pageSize
- ? '
‹' +
- '
' + (state.page + 1) + ' / ' + pages + '' +
- '
= pages ? ' disabled' : '') + '>›'
+ var total = data.total || 0;
+ var from = total ? state.page * state.pageSize + 1 : 0;
+ var to = Math.min((state.page + 1) * state.pageSize, total);
+ pager.innerHTML = total > state.pageSize
+ ? '
‹ Prev' +
+ '
' + from + '–' + to + ' of ' + total.toLocaleString() + '' +
+ '
= total ? ' disabled' : '') + '>Next ›'
: '';
}
}
@@ -432,7 +502,15 @@
function open() {
var overlay = ensureOverlay();
- overlay.classList.remove('hidden');
+ overlay.classList.remove('hidden', 'em-closing');
+ // Re-trigger the music modal's entrance animation even on reuse. Drop the
+ // class once it's played so the 3s rail re-render doesn't replay the
+ // per-row stagger (.em-in .em-worker-row) on every poll.
+ var modal = overlay.querySelector('.enrichment-manager-modal');
+ if (modal) {
+ modal.classList.remove('em-in'); void modal.offsetWidth; modal.classList.add('em-in');
+ setTimeout(function () { modal.classList.remove('em-in'); }, 700);
+ }
document.body.classList.add('em-scroll-lock');
state.open = true;
refreshAll().then(function () {
@@ -456,8 +534,11 @@
var overlay = byId('vem-overlay');
state.open = false;
if (state.pollTimer) { clearInterval(state.pollTimer); state.pollTimer = null; }
- if (overlay) overlay.classList.add('hidden');
document.body.classList.remove('em-scroll-lock');
+ if (!overlay) return;
+ // Brief fade/scale-out (music's .em-closing), then hide.
+ overlay.classList.add('em-closing');
+ setTimeout(function () { overlay.classList.add('hidden'); overlay.classList.remove('em-closing'); }, 170);
}
document.addEventListener('soulsync:video-open-workers', open);
diff --git a/webui/static/video/video-side.css b/webui/static/video/video-side.css
index 230c5dde..54a066a2 100644
--- a/webui/static/video/video-side.css
+++ b/webui/static/video/video-side.css
@@ -267,99 +267,11 @@ body[data-side="video"] .dashboard-header-sweep {
.video-svc-logo { width: 18px; height: 18px; object-fit: contain; margin-right: 2px; }
.video-svc-logo--tvdb { filter: invert(1); }
-/* ── Video Manage-Workers modal: only the bits not covered by music's global
- .em-* classes, ALL scoped to #vem-overlay so music is never affected. ───── */
-#vem-overlay .vem-logo { width: 22px; height: 22px; object-fit: contain; }
-#vem-overlay .vem-logo--tvdb { filter: invert(1); }
-#vem-overlay .vem-icon {
- width: 34px;
- height: 34px;
- border-radius: 9px;
- background: rgba(255, 255, 255, 0.06);
- border: 1px solid rgba(255, 255, 255, 0.12);
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 11px;
- font-weight: 800;
- color: #fff;
- flex-shrink: 0;
-}
-#vem-overlay .em-panel-header { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 6px; }
-#vem-overlay .em-ph-main { display: flex; align-items: center; gap: 8px; font-size: 16px; }
-#vem-overlay .em-ph-status { color: var(--text-secondary, #9aa0aa); font-size: 13px; }
-#vem-overlay .em-ph-current { color: var(--text-secondary, #9aa0aa); font-size: 12px; opacity: 0.85; }
-#vem-overlay .em-pause-btn {
- margin-left: auto;
- padding: 7px 16px;
- border-radius: 9px;
- border: 1px solid rgba(255, 255, 255, 0.12);
- background: rgba(255, 255, 255, 0.06);
- color: var(--text-primary, #e8e8ea);
- font-size: 13px;
- font-weight: 600;
- cursor: pointer;
-}
-#vem-overlay .em-pause-btn:disabled { opacity: 0.45; cursor: default; }
-#vem-overlay .em-unmatched-controls { display: flex; align-items: center; justify-content: space-between; margin: 14px 0 8px; }
-#vem-overlay .em-section-sub { color: var(--text-secondary, #9aa0aa); font-size: 13px; }
-#vem-overlay .em-retry-all-btn {
- padding: 6px 14px;
- border-radius: 8px;
- border: 1px solid rgba(var(--accent-rgb, 88 101 242), 0.4);
- background: rgba(var(--accent-rgb, 88 101 242), 0.12);
- color: rgb(var(--accent-light-rgb, var(--accent-rgb, 88 101 242)));
- font-size: 12px;
- font-weight: 600;
- cursor: pointer;
-}
-#vem-overlay .em-item {
- display: flex;
- align-items: center;
- gap: 12px;
- padding: 8px 10px;
- border-radius: 10px;
- border: 1px solid rgba(255, 255, 255, 0.05);
- margin-bottom: 6px;
-}
-#vem-overlay .em-item-img {
- width: 38px;
- height: 56px;
- object-fit: cover;
- border-radius: 6px;
- background: rgba(255, 255, 255, 0.06);
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 18px;
- flex-shrink: 0;
-}
-#vem-overlay .em-item-meta { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1; }
-#vem-overlay .em-item-name { font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
-#vem-overlay .em-item-sub { font-size: 12px; color: var(--text-secondary, #9aa0aa); }
-#vem-overlay .em-item-retry {
- padding: 5px 12px;
- border-radius: 7px;
- border: 1px solid rgba(255, 255, 255, 0.12);
- background: transparent;
- color: var(--text-secondary, #9aa0aa);
- font-size: 12px;
- cursor: pointer;
-}
-#vem-overlay .em-item-retry:hover { color: #fff; border-color: rgba(255, 255, 255, 0.25); }
-#vem-overlay .em-empty { padding: 30px; text-align: center; color: var(--text-secondary, #9aa0aa); }
-#vem-overlay .em-pager { display: flex; align-items: center; justify-content: center; gap: 12px; margin-top: 12px; }
-#vem-overlay .em-pg {
- width: 30px;
- height: 30px;
- border-radius: 8px;
- border: 1px solid rgba(255, 255, 255, 0.12);
- background: rgba(255, 255, 255, 0.05);
- color: var(--text-primary, #e8e8ea);
- cursor: pointer;
-}
-#vem-overlay .em-pg:disabled { opacity: 0.4; cursor: default; }
-#vem-overlay .em-pg-info { font-size: 13px; color: var(--text-secondary, #9aa0aa); }
+/* ── Video Manage-Workers modal ─────────────────────────────────────────────
+ The modal now emits the music modal's exact markup (.em-hero / .em-row /
+ .em-icon / .em-btn / .em-pager / .em-card), so it inherits music's global
+ .em-* styling with no bespoke rules here. Live-status glows are further down;
+ the YouTube-dates panel (its own no-queue layout) keeps its .vem-yt-* block. */
/* ── Subpages: one built video page shown at a time ────────────────────── */
/* Each real page is a .video-subpage inside #video-page-host; the controller
@@ -685,13 +597,6 @@ body[data-side="video"] .dashboard-header-sweep {
box-shadow: inset 0 0 0 1px rgba(var(--row-accent), 0.45),
0 0 22px rgba(var(--row-accent), 0.20);
}
-/* "Now processing" chip pulses in the worker's accent — clear live signal. */
-#vem-overlay .em-ph-current {
- color: rgb(var(--em-accent-rgb));
- text-shadow: 0 0 12px rgba(var(--em-accent-rgb), 0.55);
- animation: vemNowPulse 1.8s ease-in-out infinite;
-}
-@keyframes vemNowPulse { 0%, 100% { opacity: 0.78; } 50% { opacity: 1; } }
/* Active "Process first" / kind selection glow in the worker accent. */
#vem-overlay .em-global-tabs button.active,
#vem-overlay .em-card--current {
@@ -849,7 +754,6 @@ a.vd-prov:hover img, a.vd-prov:hover .vd-prov-ph { border-color: rgba(var(--vd-a
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.4)); transition: transform 0.3s ease;
}
.video-enrich-button:hover .video-enrich-glyph { transform: scale(1.1); }
-#vem-overlay .vem-glyph { font-size: 20px; line-height: 1; }
/* ══════════════════════════════════════════════════════════════════════════