diff --git a/tests/test_video_side_shell.py b/tests/test_video_side_shell.py index 77c9f05b..14415a80 100644 --- a/tests/test_video_side_shell.py +++ b/tests/test_video_side_shell.py @@ -19,6 +19,7 @@ _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") _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") +_VSETTINGS_JS = (_ROOT / "webui" / "static" / "video" / "video-settings.js").read_text(encoding="utf-8") _CSS_PATH = _ROOT / "webui" / "static" / "video" / "video-side.css" EXPECTED_VIDEO_PAGES = { @@ -194,6 +195,28 @@ def test_video_settings_reuses_real_music_settings_page(): assert 'data-video-page="video-settings"] #video-page-host' in css +def test_video_library_mapping_ui_present_and_video_only(): + # Movies/TV selectors live next to the music library selector, marked + # data-video-only so they show only on the video side. + assert 'data-video-lib-select="movies"' in _INDEX + assert 'data-video-lib-select="tv"' in _INDEX + assert "data-video-lib-save" in _INDEX + assert "data-video-only" in _INDEX + css = _CSS_PATH.read_text(encoding="utf-8") + assert 'body[data-side="music"] [data-video-only]' in css # hidden on music side + + +def test_video_settings_module_referenced_and_isolated(): + assert "video/video-settings.js" in _INDEX + stripped = _VSETTINGS_JS.strip() + assert stripped.startswith("/*") or stripped.startswith("(function") + assert "(function" in _VSETTINGS_JS and "})();" in _VSETTINGS_JS + assert "window." not in _VSETTINGS_JS + assert "addEventListener" in _VSETTINGS_JS + assert "/api/video/libraries" in _VSETTINGS_JS + assert "soulsync:video-page-shown" in _VSETTINGS_JS + + def test_controller_is_isolated_iife_with_no_globals(): stripped = _JS.strip() # Wrapped in an IIFE → declares no module-level globals that could collide diff --git a/webui/index.html b/webui/index.html index 8b5f673d..cd9845a8 100644 --- a/webui/index.html +++ b/webui/index.html @@ -4795,6 +4795,25 @@ Select which music library to use (doesn't affect config file) + +
+ + + + + + Which server libraries the video side scans (doesn't affect config file) + +
+ + +
+
@@ -8889,6 +8908,8 @@ + + diff --git a/webui/static/video/video-settings.js b/webui/static/video/video-settings.js new file mode 100644 index 00000000..24b041cc --- /dev/null +++ b/webui/static/video/video-settings.js @@ -0,0 +1,80 @@ +/* + * SoulSync — Video settings additions (isolated). + * + * The video side shows the real music settings page; this module only drives the + * VIDEO-specific bits added to it — for now, the Movies/TV library mapping + * (which server library the scan reads). It populates the dropdowns from + * /api/video/libraries when the Settings page is shown on the video side and + * saves the choice back. Self-contained IIFE, no globals, no inline handlers. + */ +(function () { + 'use strict'; + + var PAGE_ID = 'video-settings'; + var URL = '/api/video/libraries'; + + function status(text) { + var n = document.querySelector('[data-video-lib-status]'); + if (n) n.textContent = text || ''; + } + + function fill(select, items, selected) { + if (!select) return; + select.textContent = ''; + var none = document.createElement('option'); + none.value = ''; + none.textContent = '— None —'; + select.appendChild(none); + for (var i = 0; i < items.length; i++) { + var opt = document.createElement('option'); + opt.value = items[i].title; + opt.textContent = items[i].title; + if (items[i].title === selected) opt.selected = true; + select.appendChild(opt); + } + } + + function load() { + status('Loading…'); + fetch(URL, { headers: { 'Accept': 'application/json' } }) + .then(function (r) { return r.ok ? r.json() : null; }) + .then(function (d) { + if (!d || d.error) { status('Could not load libraries'); return; } + var sel = d.selected || {}; + fill(document.querySelector('[data-video-lib-select="movies"]'), d.movies || [], sel.movies); + fill(document.querySelector('[data-video-lib-select="tv"]'), d.tv || [], sel.tv); + status(''); + }) + .catch(function () { status('Could not load libraries'); }); + } + + function save() { + var m = document.querySelector('[data-video-lib-select="movies"]'); + var t = document.querySelector('[data-video-lib-select="tv"]'); + status('Saving…'); + fetch(URL, { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, + body: JSON.stringify({ movies: m ? m.value : '', tv: t ? t.value : '' }) + }) + .then(function (r) { return r.json(); }) + .then(function () { status('Saved'); }) + .catch(function () { status('Save failed'); }); + } + + function onPageShown(e) { + if (e && e.detail === PAGE_ID) load(); + } + + function init() { + var btn = document.querySelector('[data-video-lib-save]'); + if (btn) btn.addEventListener('click', save); + document.addEventListener('soulsync:video-page-shown', onPageShown); + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); + } else { + init(); + } +})(); diff --git a/webui/static/video/video-side.css b/webui/static/video/video-side.css index b1d5fb50..43260e68 100644 --- a/webui/static/video/video-side.css +++ b/webui/static/video/video-side.css @@ -117,6 +117,11 @@ body[data-side="video"][data-video-page="video-settings"] #settings-page { body[data-side="video"][data-video-page="video-settings"] #video-page-host { display: none !important; } +/* Video-only additions inside the shared settings page (e.g. the Movies/TV + library mapping) — hidden on the music side. */ +body[data-side="music"] [data-video-only] { + display: none !important; +} /* ── Dashboard header: match music, minus the sweep animation ──────────── */ /* The header reuses music's .dashboard-header markup/CSS for an identical look;