video side: reuse music styling for Tools + dashboard scan controls
The visuals were off because I'd invented CSS/markup instead of reusing the shared design system. Fixed to match music exactly: - Dashboard Library card now uses music's full markup — header icon, Refresh/ Deep Scan buttons WITH their icons, and stat rows with icons (movies/shows/ episodes/disk). Same .library-status-* classes, no custom CSS. - Tools 'Library Scan' card now mirrors the music Database Updater: a mode dropdown (Incremental/Full Refresh/Deep Scan) + one Scan button inside .tool-card-controls + the standard progress bar. Styling comes for free from the generic music classes. - Dropped bespoke .video-tool-btn/.video-scan-controls CSS and folded the separate video-tools.js into the shared video-scan.js (one fewer file). JS stays isolated only because it must hit /api/video + update video DOM. 110 tests green.
This commit is contained in:
parent
71f126f9d2
commit
7ab62674ed
5 changed files with 111 additions and 122 deletions
|
|
@ -19,7 +19,6 @@ _JS = (_ROOT / "webui" / "static" / "video" / "video-side.js").read_text(encodin
|
||||||
_DASH_JS = (_ROOT / "webui" / "static" / "video" / "video-dashboard.js").read_text(encoding="utf-8")
|
_DASH_JS = (_ROOT / "webui" / "static" / "video" / "video-dashboard.js").read_text(encoding="utf-8")
|
||||||
_LIB_JS = (_ROOT / "webui" / "static" / "video" / "video-library.js").read_text(encoding="utf-8")
|
_LIB_JS = (_ROOT / "webui" / "static" / "video" / "video-library.js").read_text(encoding="utf-8")
|
||||||
_SCAN_JS = (_ROOT / "webui" / "static" / "video" / "video-scan.js").read_text(encoding="utf-8")
|
_SCAN_JS = (_ROOT / "webui" / "static" / "video" / "video-scan.js").read_text(encoding="utf-8")
|
||||||
_TOOLS_JS = (_ROOT / "webui" / "static" / "video" / "video-tools.js").read_text(encoding="utf-8")
|
|
||||||
_CSS_PATH = _ROOT / "webui" / "static" / "video" / "video-side.css"
|
_CSS_PATH = _ROOT / "webui" / "static" / "video" / "video-side.css"
|
||||||
|
|
||||||
EXPECTED_VIDEO_PAGES = {
|
EXPECTED_VIDEO_PAGES = {
|
||||||
|
|
@ -157,9 +156,12 @@ def test_video_library_module_referenced_and_isolated():
|
||||||
def test_video_tools_page_has_three_scan_modes():
|
def test_video_tools_page_has_three_scan_modes():
|
||||||
block = _block(
|
block = _block(
|
||||||
_INDEX, r'<section class="video-subpage" data-video-subpage="video-tools"', "</section>")
|
_INDEX, r'<section class="video-subpage" data-video-subpage="video-tools"', "</section>")
|
||||||
|
# Mode dropdown + single Scan button, same shape as the music tool card.
|
||||||
for mode in ("incremental", "full", "deep"):
|
for mode in ("incremental", "full", "deep"):
|
||||||
assert f'data-video-scan-mode="{mode}"' in block, f"tools page missing scan mode {mode}"
|
assert f'value="{mode}"' in block, f"tools page missing scan mode {mode}"
|
||||||
assert "data-video-tools-scan-status" in block
|
assert "data-video-scan-run" in block and "data-video-scan-select" in block
|
||||||
|
assert "data-video-scan-phase" in block # reuses music progress markup
|
||||||
|
assert "tool-card" in block and "tool-card-controls" in block # reuses music classes
|
||||||
assert "onclick" not in block
|
assert "onclick" not in block
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -171,15 +173,13 @@ def test_dashboard_library_card_has_refresh_and_deep_buttons():
|
||||||
assert "library-status-actions" in block
|
assert "library-status-actions" in block
|
||||||
|
|
||||||
|
|
||||||
def test_scan_and_tools_modules_referenced_and_isolated():
|
def test_scan_module_referenced_and_isolated():
|
||||||
assert "video/video-scan.js" in _INDEX and "video/video-tools.js" in _INDEX
|
assert "video/video-scan.js" in _INDEX
|
||||||
for js in (_SCAN_JS, _TOOLS_JS):
|
stripped = _SCAN_JS.strip()
|
||||||
stripped = js.strip()
|
assert stripped.startswith("/*") or stripped.startswith("(function")
|
||||||
assert stripped.startswith("/*") or stripped.startswith("(function")
|
assert "(function" in _SCAN_JS and "})();" in _SCAN_JS
|
||||||
assert "(function" in js and "})();" in js
|
assert "window." not in _SCAN_JS
|
||||||
assert "window." not in js
|
assert "addEventListener" in _SCAN_JS
|
||||||
assert "addEventListener" in js
|
|
||||||
# The shared controller drives scans via the documented events.
|
|
||||||
assert "soulsync:video-scan-done" in _SCAN_JS
|
assert "soulsync:video-scan-done" in _SCAN_JS
|
||||||
assert "/api/video/scan/request" in _SCAN_JS
|
assert "/api/video/scan/request" in _SCAN_JS
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -552,39 +552,56 @@
|
||||||
<div class="library-status-card">
|
<div class="library-status-card">
|
||||||
<div class="library-status-glow"></div>
|
<div class="library-status-glow"></div>
|
||||||
<div class="library-status-header">
|
<div class="library-status-header">
|
||||||
|
<div class="library-status-icon">
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/><line x1="9" y1="7" x2="16" y2="7"/><line x1="9" y1="11" x2="14" y2="11"/></svg>
|
||||||
|
</div>
|
||||||
<div class="library-status-info">
|
<div class="library-status-info">
|
||||||
<h4 class="library-status-title">Video Library</h4>
|
<h4 class="library-status-title">Video Library</h4>
|
||||||
<p class="library-status-subtitle">Not scanned yet</p>
|
<p class="library-status-subtitle">Movies, shows & episodes</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="library-status-actions">
|
<div class="library-status-actions">
|
||||||
<button class="library-status-btn" type="button" data-video-scan-mode="full">
|
<button class="library-status-btn" type="button" data-video-scan-mode="full">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>
|
||||||
<span>Refresh</span>
|
<span>Refresh</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="library-status-btn library-status-btn-secondary" type="button" data-video-scan-mode="deep">
|
<button class="library-status-btn library-status-btn-secondary" type="button" data-video-scan-mode="deep">
|
||||||
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/><line x1="11" y1="8" x2="11" y2="14"/><line x1="8" y1="11" x2="14" y2="11"/></svg>
|
||||||
<span>Deep Scan</span>
|
<span>Deep Scan</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="library-status-stats">
|
<div class="library-status-stats">
|
||||||
<div class="library-status-stat">
|
<div class="library-status-stat">
|
||||||
|
<div class="library-status-stat-icon">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="2" width="20" height="20" rx="2.18"/><line x1="7" y1="2" x2="7" y2="22"/><line x1="17" y1="2" x2="17" y2="22"/><line x1="2" y1="12" x2="22" y2="12"/><line x1="2" y1="7" x2="7" y2="7"/><line x1="2" y1="17" x2="7" y2="17"/><line x1="17" y1="17" x2="22" y2="17"/><line x1="17" y1="7" x2="22" y2="7"/></svg>
|
||||||
|
</div>
|
||||||
<div class="library-status-stat-text">
|
<div class="library-status-stat-text">
|
||||||
<span class="library-status-stat-value" data-video-stat="movies">0</span>
|
<span class="library-status-stat-value" data-video-stat="movies">0</span>
|
||||||
<span class="library-status-stat-label">Movies</span>
|
<span class="library-status-stat-label">Movies</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="library-status-stat">
|
<div class="library-status-stat">
|
||||||
|
<div class="library-status-stat-icon">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="7" width="20" height="15" rx="2"/><polyline points="17 2 12 7 7 2"/></svg>
|
||||||
|
</div>
|
||||||
<div class="library-status-stat-text">
|
<div class="library-status-stat-text">
|
||||||
<span class="library-status-stat-value" data-video-stat="shows">0</span>
|
<span class="library-status-stat-value" data-video-stat="shows">0</span>
|
||||||
<span class="library-status-stat-label">Shows</span>
|
<span class="library-status-stat-label">Shows</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="library-status-stat">
|
<div class="library-status-stat">
|
||||||
|
<div class="library-status-stat-icon">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="5 3 19 12 5 21 5 3"/></svg>
|
||||||
|
</div>
|
||||||
<div class="library-status-stat-text">
|
<div class="library-status-stat-text">
|
||||||
<span class="library-status-stat-value" data-video-stat="episodes">0</span>
|
<span class="library-status-stat-value" data-video-stat="episodes">0</span>
|
||||||
<span class="library-status-stat-label">Episodes</span>
|
<span class="library-status-stat-label">Episodes</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="library-status-stat">
|
<div class="library-status-stat">
|
||||||
|
<div class="library-status-stat-icon">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/></svg>
|
||||||
|
</div>
|
||||||
<div class="library-status-stat-text">
|
<div class="library-status-stat-text">
|
||||||
<span class="library-status-stat-value" data-video-stat="library-size">--</span>
|
<span class="library-status-stat-value" data-video-stat="library-size">--</span>
|
||||||
<span class="library-status-stat-label">Disk Size</span>
|
<span class="library-status-stat-label">Disk Size</span>
|
||||||
|
|
@ -721,14 +738,21 @@
|
||||||
<div class="tool-card-header">
|
<div class="tool-card-header">
|
||||||
<h4 class="tool-card-title">Library Scan</h4>
|
<h4 class="tool-card-title">Library Scan</h4>
|
||||||
</div>
|
</div>
|
||||||
<p class="tool-card-info">Pull movies & shows from your media server into the video library. <strong>Incremental</strong>: recently-added only (fast). <strong>Full Refresh</strong>: re-read everything. <strong>Deep Scan</strong>: full re-read and remove items no longer on the server.</p>
|
<p class="tool-card-info">Pull movies & shows from your media server. Incremental = recently-added only; Full Refresh = re-read everything; Deep Scan = also remove items no longer on the server.</p>
|
||||||
<div class="tool-card-controls video-scan-controls">
|
<div class="tool-card-controls">
|
||||||
<button class="video-tool-btn" type="button" data-video-scan-mode="incremental">Incremental</button>
|
<select data-video-scan-select>
|
||||||
<button class="video-tool-btn" type="button" data-video-scan-mode="full">Full Refresh</button>
|
<option value="incremental">Incremental Update</option>
|
||||||
<button class="video-tool-btn video-tool-btn--warn" type="button" data-video-scan-mode="deep">Deep Scan</button>
|
<option value="full">Full Refresh</option>
|
||||||
|
<option value="deep">Deep Scan</option>
|
||||||
|
</select>
|
||||||
|
<button type="button" data-video-scan-run>Scan Library</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="tool-card-progress-section">
|
<div class="tool-card-progress-section">
|
||||||
<p class="progress-phase-label" data-video-tools-scan-status>Idle</p>
|
<p class="progress-phase-label" data-video-scan-phase>Idle</p>
|
||||||
|
<div class="progress-bar-container">
|
||||||
|
<div class="progress-bar-fill" data-video-scan-bar style="width: 0%;"></div>
|
||||||
|
</div>
|
||||||
|
<p class="progress-details-label" data-video-scan-detail>0 movies, 0 shows</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -8865,8 +8889,6 @@
|
||||||
<script src="{{ url_for('static', filename='video/video-side.js', v=static_v) }}"></script>
|
<script src="{{ url_for('static', filename='video/video-side.js', v=static_v) }}"></script>
|
||||||
<!-- Shared video scan controller (triggers + polls scans for all video pages) -->
|
<!-- Shared video scan controller (triggers + polls scans for all video pages) -->
|
||||||
<script src="{{ url_for('static', filename='video/video-scan.js', v=static_v) }}"></script>
|
<script src="{{ url_for('static', filename='video/video-scan.js', v=static_v) }}"></script>
|
||||||
<!-- Video tools page (isolated; reflects scan progress) -->
|
|
||||||
<script src="{{ url_for('static', filename='video/video-tools.js', v=static_v) }}"></script>
|
|
||||||
<!-- Video dashboard data layer (isolated; populates the dashboard from the video DB) -->
|
<!-- Video dashboard data layer (isolated; populates the dashboard from the video DB) -->
|
||||||
<script src="{{ url_for('static', filename='video/video-dashboard.js', v=static_v) }}"></script>
|
<script src="{{ url_for('static', filename='video/video-dashboard.js', v=static_v) }}"></script>
|
||||||
<!-- Video library page (isolated; lists movies/shows + scan trigger) -->
|
<!-- Video library page (isolated; lists movies/shows + scan trigger) -->
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,19 @@
|
||||||
/*
|
/*
|
||||||
* SoulSync — shared VIDEO scan controller (isolated).
|
* SoulSync — shared VIDEO scan controller (isolated).
|
||||||
*
|
*
|
||||||
* One place that triggers + polls library scans, so the Library page, the Tools
|
* One place triggers + polls library scans for every surface (Library page,
|
||||||
* page, and the Dashboard card don't each duplicate the fetch/poll logic. Other
|
* Tools page, Dashboard card), so nothing duplicates the fetch/poll logic. It
|
||||||
* modules stay decoupled: they just listen for the events below.
|
* reuses the music CSS/markup — it just targets /api/video/* and updates the
|
||||||
|
* video DOM.
|
||||||
*
|
*
|
||||||
* Wires any element with data-video-scan-mode="full|incremental|deep" (or
|
* Triggers:
|
||||||
* data-video-scan, = full) to start a scan on click. Also starts on a
|
* - click on [data-video-scan-mode="full|incremental|deep"] (or [data-video-scan] = full)
|
||||||
* 'soulsync:video-scan-start' event {detail:{mode}}.
|
* - click on [data-video-scan-run], reading the mode from [data-video-scan-select]
|
||||||
|
* - a 'soulsync:video-scan-start' event {detail:{mode}}
|
||||||
*
|
*
|
||||||
* Emits:
|
* Updates the Tools progress widgets ([data-video-scan-phase|bar|detail]) and
|
||||||
* soulsync:video-scan-progress {detail: <status>} (repeatedly while scanning)
|
* emits 'soulsync:video-scan-progress' / 'soulsync:video-scan-done' so the
|
||||||
* soulsync:video-scan-done {detail: <status>} (once, on finish/error)
|
* Library page and Dashboard can react too. Self-contained IIFE, no globals.
|
||||||
*
|
|
||||||
* Self-contained IIFE, no globals, no inline handlers, under static/video/.
|
|
||||||
*/
|
*/
|
||||||
(function () {
|
(function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
@ -26,20 +26,59 @@
|
||||||
document.dispatchEvent(new CustomEvent(name, { detail: detail }));
|
document.dispatchEvent(new CustomEvent(name, { detail: detail }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setText(sel, text) {
|
||||||
|
var n = document.querySelector(sel);
|
||||||
|
if (n) n.textContent = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setBar(pct) {
|
||||||
|
var bar = document.querySelector('[data-video-scan-bar]');
|
||||||
|
if (bar) bar.style.width = pct + '%';
|
||||||
|
}
|
||||||
|
|
||||||
|
function counts(s) {
|
||||||
|
return (s.movies || 0) + ' movies, ' + (s.shows || 0) + ' shows';
|
||||||
|
}
|
||||||
|
|
||||||
|
function reflectProgress(s) {
|
||||||
|
var phase = (s.phase || 'scanning');
|
||||||
|
setText('[data-video-scan-phase]', phase.charAt(0).toUpperCase() + phase.slice(1));
|
||||||
|
setText('[data-video-scan-detail]', counts(s));
|
||||||
|
setBar(100);
|
||||||
|
}
|
||||||
|
|
||||||
|
function reflectDone(s) {
|
||||||
|
var run = document.querySelector('[data-video-scan-run]');
|
||||||
|
if (run) run.disabled = false;
|
||||||
|
if (s.state === 'error') {
|
||||||
|
setText('[data-video-scan-phase]', 'Failed');
|
||||||
|
setText('[data-video-scan-detail]', s.error || 'Scan failed');
|
||||||
|
setBar(0);
|
||||||
|
} else {
|
||||||
|
setText('[data-video-scan-phase]', 'Complete');
|
||||||
|
setText('[data-video-scan-detail]',
|
||||||
|
counts(s) + (s.removed ? ', ' + s.removed + ' removed' : ''));
|
||||||
|
setBar(100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function poll() {
|
function poll() {
|
||||||
fetch(STATUS_URL, { headers: { 'Accept': 'application/json' } })
|
fetch(STATUS_URL, { headers: { 'Accept': 'application/json' } })
|
||||||
.then(function (r) { return r.json(); })
|
.then(function (r) { return r.json(); })
|
||||||
.then(function (s) {
|
.then(function (s) {
|
||||||
if (s && s.state === 'scanning') {
|
if (s && s.state === 'scanning') {
|
||||||
|
reflectProgress(s);
|
||||||
emit('soulsync:video-scan-progress', s);
|
emit('soulsync:video-scan-progress', s);
|
||||||
setTimeout(poll, 1500);
|
setTimeout(poll, 1500);
|
||||||
} else {
|
} else {
|
||||||
scanning = false;
|
scanning = false;
|
||||||
|
reflectDone(s || { state: 'idle' });
|
||||||
emit('soulsync:video-scan-done', s || { state: 'idle' });
|
emit('soulsync:video-scan-done', s || { state: 'idle' });
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(function () {
|
.catch(function () {
|
||||||
scanning = false;
|
scanning = false;
|
||||||
|
reflectDone({ state: 'error', error: 'Could not reach server' });
|
||||||
emit('soulsync:video-scan-done', { state: 'error' });
|
emit('soulsync:video-scan-done', { state: 'error' });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -47,8 +86,11 @@
|
||||||
function start(mode) {
|
function start(mode) {
|
||||||
if (scanning) return;
|
if (scanning) return;
|
||||||
scanning = true;
|
scanning = true;
|
||||||
emit('soulsync:video-scan-progress',
|
var run = document.querySelector('[data-video-scan-run]');
|
||||||
{ state: 'scanning', phase: 'starting', mode: mode, movies: 0, shows: 0 });
|
if (run) run.disabled = true;
|
||||||
|
var pending = { state: 'scanning', phase: 'starting', mode: mode, movies: 0, shows: 0 };
|
||||||
|
reflectProgress(pending);
|
||||||
|
emit('soulsync:video-scan-progress', pending);
|
||||||
fetch(REQUEST_URL, {
|
fetch(REQUEST_URL, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
|
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
|
||||||
|
|
@ -57,24 +99,29 @@
|
||||||
.then(function () { setTimeout(poll, 600); })
|
.then(function () { setTimeout(poll, 600); })
|
||||||
.catch(function () {
|
.catch(function () {
|
||||||
scanning = false;
|
scanning = false;
|
||||||
|
reflectDone({ state: 'error' });
|
||||||
emit('soulsync:video-scan-done', { state: 'error' });
|
emit('soulsync:video-scan-done', { state: 'error' });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function wire() {
|
function init() {
|
||||||
var els = document.querySelectorAll('[data-video-scan-mode],[data-video-scan]');
|
var triggers = document.querySelectorAll('[data-video-scan-mode],[data-video-scan]');
|
||||||
for (var i = 0; i < els.length; i++) {
|
for (var i = 0; i < triggers.length; i++) {
|
||||||
(function (el) {
|
(function (el) {
|
||||||
el.addEventListener('click', function (e) {
|
el.addEventListener('click', function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
start(el.getAttribute('data-video-scan-mode') || 'full');
|
start(el.getAttribute('data-video-scan-mode') || 'full');
|
||||||
});
|
});
|
||||||
})(els[i]);
|
})(triggers[i]);
|
||||||
|
}
|
||||||
|
var run = document.querySelector('[data-video-scan-run]');
|
||||||
|
if (run) {
|
||||||
|
run.addEventListener('click', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
var sel = document.querySelector('[data-video-scan-select]');
|
||||||
|
start(sel ? sel.value : 'full');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function init() {
|
|
||||||
wire();
|
|
||||||
document.addEventListener('soulsync:video-scan-start', function (e) {
|
document.addEventListener('soulsync:video-scan-start', function (e) {
|
||||||
start((e.detail && e.detail.mode) || 'full');
|
start((e.detail && e.detail.mode) || 'full');
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -226,27 +226,3 @@ body[data-side="video"] .dashboard-header-sweep {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--text-secondary, #9aa0aa);
|
color: var(--text-secondary, #9aa0aa);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Tools page: scan buttons ──────────────────────────────────────────── */
|
|
||||||
.video-scan-controls {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
.video-tool-btn {
|
|
||||||
padding: 9px 18px;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #fff;
|
|
||||||
background: rgb(var(--accent-rgb, 88 101 242));
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
||||||
border-radius: 9px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: filter 0.15s ease, opacity 0.15s ease;
|
|
||||||
}
|
|
||||||
.video-tool-btn:hover { filter: brightness(1.1); }
|
|
||||||
.video-tool-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
||||||
.video-tool-btn--warn {
|
|
||||||
background: rgba(255, 255, 255, 0.06);
|
|
||||||
color: var(--text-primary, #e8e8ea);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,56 +0,0 @@
|
||||||
/*
|
|
||||||
* SoulSync — Video Tools page (isolated).
|
|
||||||
*
|
|
||||||
* The scan buttons (data-video-scan-mode) are wired by video-scan.js; this
|
|
||||||
* module just reflects scan progress/result into the tool card's status line
|
|
||||||
* and disables the buttons while a scan runs. Self-contained IIFE, no globals.
|
|
||||||
*/
|
|
||||||
(function () {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
function statusNode() {
|
|
||||||
return document.querySelector('[data-video-tools-scan-status]');
|
|
||||||
}
|
|
||||||
|
|
||||||
function setStatus(text) {
|
|
||||||
var n = statusNode();
|
|
||||||
if (n) n.textContent = text;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setButtonsDisabled(disabled) {
|
|
||||||
var btns = document.querySelectorAll(
|
|
||||||
'[data-video-subpage="video-tools"] [data-video-scan-mode]');
|
|
||||||
for (var i = 0; i < btns.length; i++) btns[i].disabled = disabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
function onProgress(e) {
|
|
||||||
var s = e.detail || {};
|
|
||||||
if (s.state !== 'scanning') return;
|
|
||||||
setButtonsDisabled(true);
|
|
||||||
setStatus((s.phase || 'Scanning') + '… '
|
|
||||||
+ (s.movies || 0) + ' movies, ' + (s.shows || 0) + ' shows');
|
|
||||||
}
|
|
||||||
|
|
||||||
function onDone(e) {
|
|
||||||
var s = e.detail || {};
|
|
||||||
setButtonsDisabled(false);
|
|
||||||
if (s.state === 'error') {
|
|
||||||
setStatus('Failed' + (s.error ? ': ' + s.error : ''));
|
|
||||||
} else {
|
|
||||||
var msg = 'Done — ' + (s.movies || 0) + ' movies, ' + (s.shows || 0) + ' shows';
|
|
||||||
if (s.removed) msg += ', ' + s.removed + ' removed';
|
|
||||||
setStatus(msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function init() {
|
|
||||||
document.addEventListener('soulsync:video-scan-progress', onProgress);
|
|
||||||
document.addEventListener('soulsync:video-scan-done', onDone);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (document.readyState === 'loading') {
|
|
||||||
document.addEventListener('DOMContentLoaded', init);
|
|
||||||
} else {
|
|
||||||
init();
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
Loading…
Reference in a new issue