video side: Movies/TV library mapping UI on the settings page
Right next to music's 'Music Library' selector, the video side now shows 'Movies Library' + 'TV Shows Library' dropdowns (data-video-only, hidden on the music side). video-settings.js populates them from /api/video/libraries when Settings opens on the video side and saves the choice back; the scanner then reads only those libraries. Isolated IIFE, data-attr wired. 83 tests green.
This commit is contained in:
parent
5b0b64bf3b
commit
576199bb2f
4 changed files with 129 additions and 0 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -4795,6 +4795,25 @@
|
|||
Select which music library to use (doesn't affect config file)
|
||||
</small>
|
||||
</div>
|
||||
<!-- Video side: which libraries are Movies / TV (shown only on the
|
||||
video side; populated + saved by video/video-settings.js). -->
|
||||
<div class="form-group video-library-selectors" data-video-only>
|
||||
<label>Movies Library:</label>
|
||||
<select data-video-lib-select="movies">
|
||||
<option value="">Loading...</option>
|
||||
</select>
|
||||
<label style="margin-top: 12px;">TV Shows Library:</label>
|
||||
<select data-video-lib-select="tv">
|
||||
<option value="">Loading...</option>
|
||||
</select>
|
||||
<small style="color: #999; font-size: 0.9em; display: block; margin-top: 5px;">
|
||||
Which server libraries the video side scans (doesn't affect config file)
|
||||
</small>
|
||||
<div style="margin-top: 8px;">
|
||||
<button type="button" class="detect-button" data-video-lib-save>Save Libraries</button>
|
||||
<span data-video-lib-status style="margin-left: 10px; font-size: 0.9em; color: #999;"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button id="plex-config-action-button" class="detect-button" onclick="clearPlexConfiguration()">Clear Configuration</button>
|
||||
</div>
|
||||
|
|
@ -8889,6 +8908,8 @@
|
|||
<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) -->
|
||||
<script src="{{ url_for('static', filename='video/video-scan.js', v=static_v) }}"></script>
|
||||
<!-- Video settings additions (isolated; library mapping on the settings page) -->
|
||||
<script src="{{ url_for('static', filename='video/video-settings.js', v=static_v) }}"></script>
|
||||
<!-- 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>
|
||||
<!-- Video library page (isolated; lists movies/shows + scan trigger) -->
|
||||
|
|
|
|||
80
webui/static/video/video-settings.js
Normal file
80
webui/static/video/video-settings.js
Normal file
|
|
@ -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();
|
||||
}
|
||||
})();
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue