diff --git a/tests/test_youtube_channel_settings_ui.py b/tests/test_youtube_channel_settings_ui.py new file mode 100644 index 00000000..08690501 --- /dev/null +++ b/tests/test_youtube_channel_settings_ui.py @@ -0,0 +1,36 @@ +"""Per-channel settings modal — UI wiring (string-contract level, like the other video-JS +tests). The functional core (storage/API/download wiring) is in test_youtube_channel_settings. +""" + +from __future__ import annotations + +from pathlib import Path + +_ROOT = Path(__file__).resolve().parent.parent +_YT = (_ROOT / "webui" / "static" / "video" / "video-youtube.js").read_text(encoding="utf-8") +_WL = (_ROOT / "webui" / "static" / "video" / "video-watchlist.js").read_text(encoding="utf-8") +_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_cog_click_opens_the_settings_modal(): + assert 'VideoYoutube.openChannelSettings(' in _WL + assert "cog.getAttribute('data-vyt-wsettings')" in _WL + + +def test_modal_is_exposed_and_built(): + assert 'openChannelSettings: openChannelSettings' in _YT + assert 'function openChannelSettings(' in _YT + assert '.vyt-cset-overlay' in _CSS # modal styles exist + + +def test_modal_loads_and_saves_via_the_settings_api(): + assert "/channel/' + encodeURIComponent(channelId) + '/settings'" in _YT + assert "method: 'POST'" in _YT + # the form carries the custom name + the optional quality override + assert 'data-cset-name' in _YT and 'data-cset-qon' in _YT + assert 'custom_name' in _YT and 'max_resolution' in _YT diff --git a/webui/static/video/video-side.css b/webui/static/video/video-side.css index 6635f338..83c41b84 100644 --- a/webui/static/video/video-side.css +++ b/webui/static/video/video-side.css @@ -3006,6 +3006,44 @@ body[data-side="video"] #soulsync-toggle { display: none; } font-size: 12px; cursor: pointer; opacity: 0; transition: all 0.15s ease; } .vyt-wcard:hover .vyt-wcard-unfollow { opacity: 1; } .vyt-wcard-unfollow:hover { background: rgba(239, 68, 68, 0.9); color: #fff; } +/* settings cog: mirror of the unfollow ✕, top-left of the avatar, hover-revealed */ +.vyt-wcard-cog { position: absolute; top: 4px; left: 50%; margin-left: -59px; width: 26px; height: 26px; + border-radius: 50%; border: none; background: rgba(0, 0, 0, 0.6); color: rgba(255, 255, 255, 0.7); + font-size: 13px; line-height: 1; cursor: pointer; opacity: 0; transition: all 0.15s ease; } +.vyt-wcard:hover .vyt-wcard-cog { opacity: 1; } +.vyt-wcard-cog:hover { background: rgba(255, 59, 59, 0.85); color: #fff; } + +/* per-channel settings modal */ +.vyt-cset-overlay { position: fixed; inset: 0; z-index: 10000; display: flex; align-items: center; + justify-content: center; background: rgba(0, 0, 0, 0.65); backdrop-filter: blur(3px); } +.vyt-cset { width: min(440px, 92vw); max-height: 88vh; overflow: auto; background: #16181d; + border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 16px; box-shadow: 0 24px 60px rgba(0, 0, 0, 0.5); } +.vyt-cset-head { display: flex; align-items: center; justify-content: space-between; + padding: 16px 18px; border-bottom: 1px solid rgba(255, 255, 255, 0.08); } +.vyt-cset-h { font-size: 15px; font-weight: 800; color: #fff; } +.vyt-cset-x { background: none; border: none; color: rgba(255, 255, 255, 0.6); font-size: 15px; cursor: pointer; } +.vyt-cset-x:hover { color: #fff; } +.vyt-cset-body { padding: 18px; display: flex; flex-direction: column; gap: 8px; } +.vyt-cset-lbl { font-size: 12px; font-weight: 700; color: rgba(255, 255, 255, 0.85); } +.vyt-cset-in { width: 100%; padding: 9px 11px; border-radius: 9px; border: 1px solid rgba(255, 255, 255, 0.14); + background: rgba(255, 255, 255, 0.04); color: #fff; font-size: 13.5px; box-sizing: border-box; } +.vyt-cset-hint { font-size: 11.5px; color: rgba(255, 255, 255, 0.5); line-height: 1.45; } +.vyt-cset-hint code { background: rgba(255, 255, 255, 0.08); padding: 1px 4px; border-radius: 4px; } +.vyt-cset-toggle { display: flex; align-items: center; gap: 8px; margin-top: 6px; font-size: 13px; + font-weight: 700; color: #fff; cursor: pointer; } +.vyt-cset-q { display: flex; flex-direction: column; gap: 9px; padding: 12px; margin-top: 2px; + border-radius: 11px; background: rgba(255, 255, 255, 0.03); border: 1px solid rgba(255, 255, 255, 0.07); } +.vyt-cset-q[hidden] { display: none; } +.vyt-cset-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; + font-size: 13px; color: rgba(255, 255, 255, 0.85); } +.vyt-cset-row select { padding: 6px 9px; border-radius: 8px; border: 1px solid rgba(255, 255, 255, 0.14); + background: #1f2229; color: #fff; font-size: 13px; } +.vyt-cset-ck { display: flex; align-items: center; gap: 8px; font-size: 13px; color: rgba(255, 255, 255, 0.85); cursor: pointer; } +.vyt-cset-foot { padding: 14px 18px; border-top: 1px solid rgba(255, 255, 255, 0.08); display: flex; justify-content: flex-end; } +.vyt-cset-save { padding: 9px 22px; border-radius: 10px; border: none; background: #ff3b3b; color: #fff; + font-size: 13.5px; font-weight: 800; cursor: pointer; } +.vyt-cset-save:disabled { opacity: 0.5; cursor: default; } +.vyt-cset-save:hover:not(:disabled) { background: #ff5252; } .vyt-wcard-info { margin-top: 11px; display: flex; flex-direction: column; gap: 2px; max-width: 100%; } .vyt-wcard-title { font-size: 13.5px; font-weight: 800; color: #fff; line-height: 1.2; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } diff --git a/webui/static/video/video-watchlist.js b/webui/static/video/video-watchlist.js index 3e7ac55d..25fc9f12 100644 --- a/webui/static/video/video-watchlist.js +++ b/webui/static/video/video-watchlist.js @@ -24,6 +24,8 @@ var meta = n > 0 ? (n + ' video' + (n === 1 ? '' : 's')) : 'Channel'; return '
$channel folder/show name when this channel’s videos download. Blank = use the channel’s real name.