diff --git a/webui/static/video/video-enrichment-manager.js b/webui/static/video/video-enrichment-manager.js index 1dabf288..327f273f 100644 --- a/webui/static/video/video-enrichment-manager.js +++ b/webui/static/video/video-enrichment-manager.js @@ -49,6 +49,35 @@ } function byId(id) { return document.getElementById(id); } + // The music modal's circular accent icon chip (.em-icon), reused verbatim so + // the video rail/hero match it. Logo → , otherwise a colored glyph/letter. + function workerIcon(w, lg) { + var cls = 'em-icon' + (lg ? ' em-icon--lg' : ''); + var inner; + if (LOGOS[w.id]) { + var filt = w.id === 'tvdb' ? ' style="filter:invert(1)"' : ''; + inner = ''; + } else { + inner = '' + esc(w.glyph || w.name.charAt(0)) + ''; + } + return '' + inner + ''; + } + + // "3d ago" for a SQLite UTC timestamp; '' when never attempted (mirrors music). + function relTime(value) { + if (!value) return ''; + var raw = String(value); + var ts = Date.parse(raw.replace(' ', 'T') + (raw.indexOf('Z') >= 0 ? '' : 'Z')); + if (isNaN(ts)) return ''; + var secs = Math.max(0, (Date.now() - ts) / 1000); + if (secs < 60) return 'just now'; + var mins = secs / 60; if (mins < 60) return Math.floor(mins) + 'm ago'; + var hrs = mins / 60; if (hrs < 24) return Math.floor(hrs) + 'h ago'; + var days = hrs / 24; if (days < 30) return Math.floor(days) + 'd ago'; + var months = days / 30; if (months < 12) return Math.floor(months) + 'mo ago'; + return Math.floor(months / 12) + 'y ago'; + } + function statusInfo(s) { if (!s || !s.enabled) return { cls: 'disabled', label: 'Not configured' }; if (s.running && !s.paused && !s.idle) return { cls: 'running', label: 'Running' }; @@ -118,17 +147,14 @@ function renderRail() { var rail = byId('vem-rail'); if (!rail) return; - rail.innerHTML = WORKERS.map(function (w) { + rail.innerHTML = WORKERS.map(function (w, i) { var s = state.statuses[w.id]; var info = statusInfo(s); var pct = overallPct(s); var cov = pct == null ? '' : ''; - var icon = LOGOS[w.id] - ? '' - : '' + (w.glyph || '★') + ''; - return ''; @@ -140,17 +166,12 @@ } function renderYoutubePanel(panel) { + var w = workerDef('youtube') || {}; var s = state.statuses.youtube || {}; - var info = statusInfo(s); var prog = (s.progress && s.progress.channels) || {}; - var current = (s.current_item && s.current_item.name) - ? 'channel: ' + esc(s.current_item.name) + '' : ''; panel.innerHTML = - '
' + - '
' + - 'YouTube Dates' + info.label + '' + current + '
' + - '
' + - '
Fetches real upload dates for the YouTube channels you follow, so the channel page can group videos into year-seasons. Runs in the background when you follow or open a channel — bulk via a no-key proxy, falling back to per-video; cached so it\'s a one-time pass per channel.
' + + '
' + heroHtml(w, s) + '
' + + '
Fetches real upload dates for the YouTube channels you follow, so the channel page can group videos into year-seasons. Runs in the background when you follow or open a channel — bulk via YouTube\'s own InnerTube API, falling back to per-video; cached so it\'s a one-time pass per channel.
' + '
' + '
' + (prog.matched || 0) + 'channels enriched
' + '
' + (s.dates_cached || 0) + 'dates cached
' + @@ -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' + + '' + '
' + '
' + @@ -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 ? '' : '') + '
' + '
' + (isEpisode ? '' : @@ -267,33 +317,53 @@ opt('not_found', 'Not found') + opt('pending', 'Pending') + '') + '
' + '
' + - '
'; + '
' + + '
' + (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 || '') + '' + - '
'; + 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 ? '' + : ''; + 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 + '' + - '' + 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 + ? '' + + '' + from + '–' + to + ' of ' + total.toLocaleString() + '' + + '' : ''; } } @@ -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; } /* ══════════════════════════════════════════════════════════════════════════