youtube playlists: same settings cog/modal as channels

playlist cards on the watchlist tab now get the same hover cog → the per-channel
settings modal (custom show-name + quality override). the modal is kind-aware so
it reads 'Playlist settings' + 'the playlist's real name'.

no backend change needed: playlist videos are wishlisted with channel_id =
playlist_id, so the existing get/set_channel_settings + enqueue override lookup
already key off the playlist id. UI-only: cog on playlistCard + data-kind passed
to openChannelSettings(id, title, kind). test covers both cogs.
This commit is contained in:
BoulderBadgeDad 2026-06-25 23:07:15 -07:00
parent f734905ba1
commit 23c64e000a
3 changed files with 23 additions and 12 deletions

View file

@ -12,14 +12,19 @@ _WL = (_ROOT / "webui" / "static" / "video" / "video-watchlist.js").read_text(en
_CSS = (_ROOT / "webui" / "static" / "video" / "video-side.css").read_text(encoding="utf-8")
def test_channel_card_has_a_settings_cog():
assert 'data-vyt-wsettings' in _WL # cog button on the watchlist channel card
assert 'vyt-wcard-cog' in _WL and '.vyt-wcard-cog' in _CSS
def test_channel_and_playlist_cards_both_have_a_settings_cog():
assert 'data-vyt-wsettings' in _WL # cog button on the watchlist cards
assert _WL.count('vyt-wcard-cog') >= 2 # channel + playlist cards
assert '.vyt-wcard-cog' in _CSS
assert 'data-kind="channel"' in _WL and 'data-kind="playlist"' in _WL
def test_cog_click_opens_the_settings_modal():
def test_cog_click_opens_the_settings_modal_with_kind():
assert 'VideoYoutube.openChannelSettings(' in _WL
assert "cog.getAttribute('data-vyt-wsettings')" in _WL
assert "cog.getAttribute('data-kind')" in _WL # kind passed through
# the modal reads the kind to label channel vs playlist
assert "kind === 'playlist' ? 'Playlist' : 'Channel'" in _YT
def test_modal_is_exposed_and_built():

View file

@ -25,7 +25,7 @@
return '<div class="vyt-wcard" data-vyt-open-channel="' + esc(ch.youtube_id) + '" title="Open channel">' +
'<div class="vyt-wcard-art">' + av + '</div>' +
'<button class="vyt-wcard-cog" type="button" data-vyt-wsettings="' + esc(ch.youtube_id) +
'" data-title="' + esc(ch.title) + '" title="Channel settings">&#9881;</button>' +
'" data-kind="channel" data-title="' + esc(ch.title) + '" title="Channel settings">&#9881;</button>' +
'<button class="vyt-wcard-unfollow" type="button" data-vyt-wunfollow="' + esc(ch.youtube_id) +
'" title="Unfollow">&#10005;</button>' +
'<div class="vyt-wcard-info"><span class="vyt-wcard-title" title="' + esc(ch.title) + '">' +
@ -38,6 +38,8 @@
? VideoYoutube.avatar({ poster_url: pl.poster_url, title: pl.title }, 'vyt-wcard-avatar') : '';
return '<div class="vyt-wcard vyt-wcard--pl" data-vyt-open-playlist="' + esc(pl.playlist_id) + '" title="Open playlist">' +
'<div class="vyt-wcard-art">' + av + '<span class="vyt-wcard-pl-ic" aria-hidden="true">▤</span></div>' +
'<button class="vyt-wcard-cog" type="button" data-vyt-wsettings="' + esc(pl.playlist_id) +
'" data-kind="playlist" data-title="' + esc(pl.title) + '" title="Playlist settings">&#9881;</button>' +
'<button class="vyt-wcard-unfollow" type="button" data-vyt-wunfollow-playlist="' + esc(pl.playlist_id) +
'" title="Unfollow">&#10005;</button>' +
'<div class="vyt-wcard-info"><span class="vyt-wcard-title" title="' + esc(pl.title) + '">' +
@ -262,7 +264,8 @@
var cog = e.target.closest('[data-vyt-wsettings]');
if (cog && window.VideoYoutube && VideoYoutube.openChannelSettings) {
e.preventDefault(); e.stopPropagation();
VideoYoutube.openChannelSettings(cog.getAttribute('data-vyt-wsettings'), cog.getAttribute('data-title'));
VideoYoutube.openChannelSettings(cog.getAttribute('data-vyt-wsettings'),
cog.getAttribute('data-title'), cog.getAttribute('data-kind') || 'channel');
return;
}
var unf = e.target.closest('[data-vyt-wunfollow]');

View file

@ -215,15 +215,17 @@
}).join('');
}
function _csetForm(title, s, q, dq) {
function _csetForm(title, s, q, dq, kind) {
var on = !!s.quality; // a stored override → start enabled
var b = (q && q.max_resolution) ? q : dq; // seed fields from override, else default
b = b || {};
var noun = kind === 'playlist' ? 'playlist' : 'channel';
return '' +
'<label class="vyt-cset-lbl">Show name (folder)</label>' +
'<input class="vyt-cset-in" data-cset-name type="text" value="' + esc(s.custom_name || '') +
'" placeholder="' + esc(title || '') + '">' +
'<div class="vyt-cset-hint">Overrides the <code>$channel</code> folder/show name when this channels videos download. Blank = use the channels real name.</div>' +
'<div class="vyt-cset-hint">Overrides the <code>$channel</code> folder/show name when this ' + noun +
's videos download. Blank = use the ' + noun + 's real name.</div>' +
'<label class="vyt-cset-toggle"><input type="checkbox" data-cset-qon' + (on ? ' checked' : '') +
'> Force a specific quality for this channel</label>' +
'<div class="vyt-cset-q" data-cset-q' + (on ? '' : ' hidden') + '>' +
@ -236,13 +238,14 @@
'<div class="vyt-cset-hint">Off = use the global YouTube quality from Settings.</div>';
}
function openChannelSettings(channelId, title) {
function openChannelSettings(channelId, title, kind) {
if (!channelId) return;
var heading = (kind === 'playlist' ? 'Playlist' : 'Channel') + ' settings';
var prev = document.getElementById('vyt-cset-overlay'); if (prev) prev.remove();
var ov = document.createElement('div');
ov.id = 'vyt-cset-overlay'; ov.className = 'vyt-cset-overlay';
ov.innerHTML = '<div class="vyt-cset" role="dialog" aria-label="Channel settings">' +
'<div class="vyt-cset-head"><span class="vyt-cset-h">Channel settings</span>' +
ov.innerHTML = '<div class="vyt-cset" role="dialog" aria-label="' + heading + '">' +
'<div class="vyt-cset-head"><span class="vyt-cset-h">' + heading + '</span>' +
'<button class="vyt-cset-x" type="button" title="Close">✕</button></div>' +
'<div class="vyt-cset-body">Loading…</div>' +
'<div class="vyt-cset-foot"><button class="vyt-cset-save" type="button" disabled>Save</button></div></div>';
@ -257,7 +260,7 @@
.then(function (r) { return r.json(); })
.then(function (d) {
var s = (d && d.settings) || {}, dq = (d && d.default_quality) || {};
body.innerHTML = _csetForm(title, s, s.quality || {}, dq);
body.innerHTML = _csetForm(title, s, s.quality || {}, dq, kind);
saveBtn.disabled = false;
var qon = body.querySelector('[data-cset-qon]'), qbox = body.querySelector('[data-cset-q]');
if (qon && qbox) qon.addEventListener('change', function () { qbox.hidden = !qon.checked; });