Channel page (UI): stats+tags, sort/filter/load-more, inline video detail, playlists
The channel detail page is now best-in-class, treated like a real show: - Hero: stats ribbon (subs · videos · views) + the channel's topic tags as chips. - Video grid: sort (newest/oldest/most-viewed), 'Wished only' filter, and a Load more that pages deeper than the first 60 uploads. - Click any video → inline panel with its full description + likes/views (lazy /youtube/video fetch, cached); wish state syncs across every card with that id. - Playlists rendered as collapsible 'seasons' that lazy-load their videos on expand (each with its own wish toggle). .vc-* CSS; JS brace-balanced; music untouched.
This commit is contained in:
parent
87750e2464
commit
0456fa893f
3 changed files with 210 additions and 44 deletions
|
|
@ -1368,6 +1368,7 @@
|
|||
<h1 class="vc-name" data-vc-name></h1>
|
||||
<div class="vc-meta" data-vc-meta></div>
|
||||
<p class="vc-desc" data-vc-desc></p>
|
||||
<div class="vc-tags" data-vc-tags></div>
|
||||
<div class="vc-actions">
|
||||
<button class="vc-follow" type="button" data-vc-follow>+ Follow</button>
|
||||
<a class="vc-yt-link" data-vc-yt target="_blank" rel="noopener">Open on YouTube ↗</a>
|
||||
|
|
@ -1375,11 +1376,24 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="vc-body">
|
||||
<div class="vc-pl-section" data-vc-pl-section hidden>
|
||||
<h2 class="vc-section-title">Playlists</h2>
|
||||
<div class="vc-playlists" data-vc-playlists></div>
|
||||
</div>
|
||||
<div class="vc-videos-head">
|
||||
<h2 class="vc-videos-title">Videos</h2>
|
||||
<span class="vc-videos-count" data-vc-count></span>
|
||||
<div class="vc-tools">
|
||||
<button class="vc-tool-btn" type="button" data-vc-wished-toggle>Wished only</button>
|
||||
<select class="vc-sort" data-vc-sort aria-label="Sort videos">
|
||||
<option value="newest">Newest</option>
|
||||
<option value="oldest">Oldest</option>
|
||||
<option value="views">Most viewed</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="vc-videos" data-vc-videos></div>
|
||||
<button class="vc-more" type="button" data-vc-more hidden>Load more</button>
|
||||
<div class="vc-empty" data-vc-empty hidden>No videos found for this channel.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,25 +2,27 @@
|
|||
* SoulSync — Video YouTube Channel detail page (isolated).
|
||||
*
|
||||
* Opens in-app like a show/movie via soulsync:video-open-detail {kind:'channel',
|
||||
* source:'youtube', id:<channelId>}. Renders a banner hero (avatar, stats,
|
||||
* description, Follow), and the channel's uploads as a grid where each video can
|
||||
* be wished individually. Separate module — mirrors video-person.js — listening
|
||||
* only for kind==='channel'. Styled by .vc-* in video-side.css.
|
||||
* source:'youtube', id:<channelId>}. Banner hero (avatar, stats, tags, Follow),
|
||||
* a sortable/filterable upload grid with Load-more + per-video inline detail, and
|
||||
* the channel's playlists as collapsible "seasons". Sibling of video-person.js;
|
||||
* listens only for kind==='channel'. Styled by .vc-* in video-side.css.
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var PAGE_ID = 'video-channel-detail';
|
||||
var YT = function () { return window.VideoYoutube; };
|
||||
var state = { id: null, channel: null, videos: {} };
|
||||
var PAGE_SIZE = 60;
|
||||
var state = { id: null, channel: null, videos: {}, all: [], sort: 'newest',
|
||||
wishedOnly: false, limit: PAGE_SIZE, plLoaded: {} };
|
||||
|
||||
function $(s, r) { return (r || document).querySelector(s); }
|
||||
function esc(s) { return YT() ? YT().esc(s) : String(s == null ? '' : s); }
|
||||
function show(el, on) { if (el) el.hidden = !on; }
|
||||
|
||||
function watchUrl(vid) { return 'https://www.youtube.com/watch?v=' + encodeURIComponent(vid); }
|
||||
function watchUrl(v) { return 'https://www.youtube.com/watch?v=' + encodeURIComponent(v); }
|
||||
function channelUrl(id) { return 'https://www.youtube.com/channel/' + encodeURIComponent(id); }
|
||||
|
||||
// ── a video tile (used in the main grid AND inside playlist sections) ──────
|
||||
function videoCard(v) {
|
||||
var dur = YT().fmtDuration(v.duration_seconds);
|
||||
var thumb = v.thumbnail_url
|
||||
|
|
@ -30,26 +32,36 @@
|
|||
var bits = [];
|
||||
var d = YT().fmtDate(v.published_at); if (d) bits.push(esc(d));
|
||||
var vc = YT().compactCount(v.view_count); if (vc) bits.push(esc(vc) + ' views');
|
||||
var wished = !!v.wished;
|
||||
return '<div class="vc-vid" data-vc-vid="' + esc(v.youtube_id) + '">' +
|
||||
'<a class="vc-vid-thumb' + (v.thumbnail_url ? '' : ' vc-vid-thumb--none') + '" href="' + watchUrl(v.youtube_id) +
|
||||
'" target="_blank" rel="noopener" data-vc-ext>' + thumb +
|
||||
(dur ? '<span class="vc-vid-dur">' + esc(dur) + '</span>' : '') +
|
||||
'<span class="vc-vid-play" aria-hidden="true">▶</span></a>' +
|
||||
'<div class="vc-vid-body">' +
|
||||
'<div class="vc-vid-title" title="' + esc(v.title) + '">' + esc(v.title || 'Untitled') + '</div>' +
|
||||
'<div class="vc-vid-body" data-vc-expand="' + esc(v.youtube_id) + '" title="Show details">' +
|
||||
'<div class="vc-vid-title">' + esc(v.title || 'Untitled') + '</div>' +
|
||||
(bits.length ? '<div class="vc-vid-meta">' + bits.join(' · ') + '</div>' : '') +
|
||||
'</div>' +
|
||||
'<button class="vc-wish' + (wished ? ' vc-wish--on' : '') + '" type="button" data-vc-wish="' +
|
||||
esc(v.youtube_id) + '">' + (wished ? '✓ Wished' : '+ Wish') + '</button>' +
|
||||
'<div class="vc-vid-detail" data-vc-detail="' + esc(v.youtube_id) + '" hidden></div>' +
|
||||
'<button class="vc-wish' + (v.wished ? ' vc-wish--on' : '') + '" type="button" data-vc-wish="' +
|
||||
esc(v.youtube_id) + '">' + (v.wished ? '✓ Wished' : '+ Wish') + '</button>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
function applyGrid() {
|
||||
var list = state.all.slice();
|
||||
if (state.wishedOnly) list = list.filter(function (v) { return v.wished; });
|
||||
if (state.sort === 'oldest') list.reverse(); // state.all is newest-first
|
||||
else if (state.sort === 'views') list.sort(function (a, b) { return (b.view_count || 0) - (a.view_count || 0); });
|
||||
var grid = $('[data-vc-videos]'); if (grid) grid.innerHTML = list.map(videoCard).join('');
|
||||
var count = $('[data-vc-count]');
|
||||
if (count) count.textContent = list.length + (state.wishedOnly ? ' wished' : ' shown');
|
||||
show($('[data-vc-empty]'), !list.length);
|
||||
}
|
||||
|
||||
function render(d) {
|
||||
var ch = d.channel || {};
|
||||
state.channel = ch;
|
||||
state.videos = {};
|
||||
(ch.videos || []).forEach(function (v) { state.videos[v.youtube_id] = v; });
|
||||
state.channel = ch; state.videos = {}; state.all = (ch.videos || []).slice();
|
||||
state.all.forEach(function (v) { state.videos[v.youtube_id] = v; });
|
||||
|
||||
var banner = $('[data-vc-banner]');
|
||||
if (banner) banner.style.backgroundImage = ch.banner_url ? "url('" + YT().img(ch.banner_url) + "')" : '';
|
||||
|
|
@ -67,17 +79,23 @@
|
|||
if (ch.handle) m.push(esc(ch.handle));
|
||||
var subs = YT().compactCount(ch.subscriber_count); if (subs) m.push(subs + ' subscribers');
|
||||
if (ch.video_count != null) m.push(esc(ch.video_count) + ' videos');
|
||||
var views = YT().compactCount(ch.view_count); if (views) m.push(views + ' views');
|
||||
meta.innerHTML = m.join('<span class="vc-dot">·</span>');
|
||||
}
|
||||
var tags = $('[data-vc-tags]');
|
||||
if (tags) {
|
||||
tags.innerHTML = (ch.tags || []).map(function (t) { return '<span class="vc-tag">' + esc(t) + '</span>'; }).join('');
|
||||
tags.hidden = !(ch.tags && ch.tags.length);
|
||||
}
|
||||
var desc = $('[data-vc-desc]');
|
||||
if (desc) { desc.textContent = ch.description || ''; desc.hidden = !ch.description; }
|
||||
var yt = $('[data-vc-yt]'); if (yt) yt.href = channelUrl(ch.youtube_id);
|
||||
setFollow(!!d.following);
|
||||
|
||||
var count = $('[data-vc-count]'); if (count) count.textContent = (ch.videos || []).length + ' shown';
|
||||
var grid = $('[data-vc-videos]');
|
||||
if (grid) grid.innerHTML = (ch.videos || []).map(videoCard).join('');
|
||||
show($('[data-vc-empty]'), !(ch.videos || []).length);
|
||||
applyGrid();
|
||||
// More uploads likely exist if we got a full page back.
|
||||
show($('[data-vc-more]'), state.all.length >= state.limit);
|
||||
loadPlaylists(ch.youtube_id);
|
||||
}
|
||||
|
||||
function setFollow(on) {
|
||||
|
|
@ -86,14 +104,60 @@
|
|||
b.textContent = on ? '✓ Following' : '+ Follow';
|
||||
}
|
||||
|
||||
function load(id) {
|
||||
// ── playlists ("seasons") ──────────────────────────────────────────────────
|
||||
function playlistRow(p) {
|
||||
var thumb = p.thumbnail_url
|
||||
? '<img class="vc-pl-thumb" src="' + esc(YT().img(p.thumbnail_url)) + '" alt="" loading="lazy">'
|
||||
: '<span class="vc-pl-thumb vc-pl-thumb--none">▶</span>';
|
||||
var n = p.video_count != null ? p.video_count + ' video' + (p.video_count === 1 ? '' : 's') : '';
|
||||
return '<div class="vc-pl vc-pl--collapsed" data-vc-pl="' + esc(p.playlist_id) + '">' +
|
||||
'<div class="vc-pl-hd" data-vc-pl-toggle="' + esc(p.playlist_id) + '">' + thumb +
|
||||
'<div class="vc-pl-meta"><span class="vc-pl-title">' + esc(p.title) + '</span>' +
|
||||
(n ? '<span class="vc-pl-count">' + n + '</span>' : '') + '</div>' +
|
||||
'<span class="vc-pl-chev" aria-hidden="true">▾</span>' +
|
||||
'</div>' +
|
||||
'<div class="vc-pl-vids" data-vc-pl-vids="' + esc(p.playlist_id) + '"></div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
function loadPlaylists(cid) {
|
||||
var sec = $('[data-vc-pl-section]'), host = $('[data-vc-playlists]');
|
||||
if (host) host.innerHTML = '';
|
||||
if (sec) sec.hidden = true;
|
||||
if (!cid) return;
|
||||
fetch('/api/video/youtube/playlists/' + encodeURIComponent(cid), { headers: { Accept: 'application/json' } })
|
||||
.then(function (r) { return r.ok ? r.json() : null; })
|
||||
.then(function (d) {
|
||||
var pls = (d && d.playlists) || [];
|
||||
if (!pls.length || !host) return;
|
||||
host.innerHTML = pls.map(playlistRow).join('');
|
||||
if (sec) sec.hidden = false;
|
||||
})
|
||||
.catch(function () { /* best-effort */ });
|
||||
}
|
||||
|
||||
function loadPlaylistVideos(pid) {
|
||||
if (state.plLoaded[pid]) return;
|
||||
state.plLoaded[pid] = true;
|
||||
var host = $('[data-vc-pl-vids="' + pid + '"]'); if (!host) return;
|
||||
host.innerHTML = '<div class="vc-pl-loading">Loading…</div>';
|
||||
fetch('/api/video/youtube/playlist/' + encodeURIComponent(pid), { headers: { Accept: 'application/json' } })
|
||||
.then(function (r) { return r.ok ? r.json() : null; })
|
||||
.then(function (d) {
|
||||
var vids = (d && d.videos) || [];
|
||||
vids.forEach(function (v) { state.videos[v.youtube_id] = v; });
|
||||
host.innerHTML = vids.length ? vids.map(videoCard).join('') : '<div class="vc-pl-loading">No videos.</div>';
|
||||
})
|
||||
.catch(function () { state.plLoaded[pid] = false; host.innerHTML = ''; });
|
||||
}
|
||||
|
||||
// ── load ───────────────────────────────────────────────────────────────────
|
||||
function load(id, keepLimit) {
|
||||
state.id = id;
|
||||
if (!keepLimit) { state.limit = PAGE_SIZE; state.plLoaded = {}; }
|
||||
var ld = $('[data-vc-loading]'); show(ld, true);
|
||||
var grid = $('[data-vc-videos]'); if (grid) grid.innerHTML = '';
|
||||
var name = $('[data-vc-name]'); if (name) name.textContent = '';
|
||||
var meta = $('[data-vc-meta]'); if (meta) meta.innerHTML = '';
|
||||
var desc = $('[data-vc-desc]'); if (desc) { desc.textContent = ''; desc.hidden = true; }
|
||||
fetch('/api/video/youtube/channel/' + encodeURIComponent(id) + '?limit=60',
|
||||
if (!keepLimit) { var g = $('[data-vc-videos]'); if (g) g.innerHTML = ''; }
|
||||
fetch('/api/video/youtube/channel/' + encodeURIComponent(id) + '?limit=' + state.limit,
|
||||
{ headers: { Accept: 'application/json' } })
|
||||
.then(function (r) { return r.ok ? r.json() : null; })
|
||||
.then(function (d) {
|
||||
|
|
@ -121,40 +185,75 @@
|
|||
var on = btn.classList.contains('vc-wish--on');
|
||||
btn.disabled = true;
|
||||
var setOn = function (val) {
|
||||
btn.classList.toggle('vc-wish--on', val); btn.textContent = val ? '✓ Wished' : '+ Wish';
|
||||
btn.disabled = false; v.wished = val;
|
||||
v.wished = val; btn.disabled = false;
|
||||
// reflect on every card with this id (grid + playlists)
|
||||
var btns = document.querySelectorAll('[data-vc-wish="' + id + '"]');
|
||||
for (var i = 0; i < btns.length; i++) {
|
||||
btns[i].classList.toggle('vc-wish--on', val);
|
||||
btns[i].textContent = val ? '✓ Wished' : '+ Wish';
|
||||
}
|
||||
document.dispatchEvent(new CustomEvent('soulsync:video-wishlist-changed'));
|
||||
};
|
||||
if (on) {
|
||||
YT().removeWish('video', id).then(function (d) { setOn(!(d && d.success)); if (d && d.success && typeof showToast === 'function') showToast('Removed from wishlist', 'info'); })
|
||||
.catch(function () { btn.disabled = false; });
|
||||
} else {
|
||||
YT().addVideos(channelStub(), [v]).then(function (d) {
|
||||
setOn(!!(d && d.success));
|
||||
if (d && d.success && typeof showToast === 'function') showToast('Added to wishlist', 'success');
|
||||
}).catch(function () { btn.disabled = false; });
|
||||
}
|
||||
if (on) YT().removeWish('video', id).then(function (d) { setOn(!(d && d.success)); }).catch(function () { btn.disabled = false; });
|
||||
else YT().addVideos(channelStub(), [v]).then(function (d) {
|
||||
setOn(!!(d && d.success));
|
||||
if (d && d.success && typeof showToast === 'function') showToast('Added to wishlist', 'success');
|
||||
}).catch(function () { btn.disabled = false; });
|
||||
}
|
||||
|
||||
function toggleFollow(btn) {
|
||||
if (!YT()) return;
|
||||
var on = btn.classList.contains('vc-follow--on');
|
||||
btn.disabled = true;
|
||||
var done = function () { btn.disabled = false; document.dispatchEvent(new CustomEvent('soulsync:video-wishlist-changed')); load(state.id); };
|
||||
var done = function () { btn.disabled = false; document.dispatchEvent(new CustomEvent('soulsync:video-wishlist-changed')); load(state.id, true); };
|
||||
if (on) YT().unfollow(state.id).then(done).catch(function () { btn.disabled = false; });
|
||||
else YT().follow(channelStub()).then(function (d) {
|
||||
if (d && d.success && typeof showToast === 'function')
|
||||
showToast('Following · ' + (d.added_videos || 0) + ' videos added', 'success');
|
||||
if (d && d.success && typeof showToast === 'function') showToast('Following · ' + (d.added_videos || 0) + ' videos added', 'success');
|
||||
done();
|
||||
}).catch(function () { btn.disabled = false; });
|
||||
}
|
||||
|
||||
// expand a video's full description/stats inline (lazy, cached on the object)
|
||||
function toggleExpand(body) {
|
||||
var id = body.getAttribute('data-vc-expand');
|
||||
var panel = $('[data-vc-detail="' + id + '"]', body.parentNode); if (!panel) return;
|
||||
if (!panel.hidden) { panel.hidden = true; return; }
|
||||
panel.hidden = false;
|
||||
var v = state.videos[id] || {};
|
||||
if (v._full) { panel.innerHTML = detailHTML(v._full); return; }
|
||||
if (v._loading) return;
|
||||
v._loading = true;
|
||||
panel.innerHTML = '<div class="vc-vid-detail-load">Loading details…</div>';
|
||||
fetch('/api/video/youtube/video/' + encodeURIComponent(id), { headers: { Accept: 'application/json' } })
|
||||
.then(function (r) { return r.ok ? r.json() : null; })
|
||||
.then(function (d) {
|
||||
v._loading = false; v._full = (d && d.video) || {};
|
||||
if (!panel.hidden) panel.innerHTML = detailHTML(v._full);
|
||||
})
|
||||
.catch(function () { v._loading = false; panel.innerHTML = '<div class="vc-vid-detail-load">No details.</div>'; });
|
||||
}
|
||||
function detailHTML(f) {
|
||||
var stats = [];
|
||||
var likes = YT().compactCount(f.like_count); if (likes) stats.push(likes + ' likes');
|
||||
var vc = YT().compactCount(f.view_count); if (vc) stats.push(vc + ' views');
|
||||
return (stats.length ? '<div class="vc-vid-stats">' + esc(stats.join(' · ')) + '</div>' : '') +
|
||||
(f.description ? '<div class="vc-vid-desc">' + esc(f.description) + '</div>' : '<div class="vc-vid-desc">No description.</div>');
|
||||
}
|
||||
|
||||
function onClick(e) {
|
||||
var wish = e.target.closest('[data-vc-wish]');
|
||||
if (wish) { e.preventDefault(); toggleWish(wish); return; }
|
||||
var fb = e.target.closest('[data-vc-follow]');
|
||||
if (fb) { e.preventDefault(); toggleFollow(fb); return; }
|
||||
// [data-vc-ext] anchors (thumbnail / Open on YouTube) fall through to navigate.
|
||||
var t = e.target;
|
||||
var wish = t.closest('[data-vc-wish]'); if (wish) { e.preventDefault(); toggleWish(wish); return; }
|
||||
var fb = t.closest('[data-vc-follow]'); if (fb) { e.preventDefault(); toggleFollow(fb); return; }
|
||||
if (t.closest('[data-vc-ext]')) return; // thumbnail / YouTube link
|
||||
var more = t.closest('[data-vc-more]'); if (more) { e.preventDefault(); state.limit += PAGE_SIZE; load(state.id, true); return; }
|
||||
var plt = t.closest('[data-vc-pl-toggle]');
|
||||
if (plt) {
|
||||
var pid = plt.getAttribute('data-vc-pl-toggle');
|
||||
var blk = plt.closest('.vc-pl');
|
||||
if (blk && blk.classList.toggle('vc-pl--collapsed') === false) loadPlaylistVideos(pid);
|
||||
return;
|
||||
}
|
||||
var exp = t.closest('[data-vc-expand]'); if (exp) { e.preventDefault(); toggleExpand(exp); return; }
|
||||
}
|
||||
|
||||
function onOpen(e) {
|
||||
|
|
@ -165,6 +264,14 @@
|
|||
function init() {
|
||||
var page = $('[data-video-channel]');
|
||||
if (page) page.addEventListener('click', onClick);
|
||||
var sort = $('[data-vc-sort]');
|
||||
if (sort) sort.addEventListener('change', function () { state.sort = sort.value; applyGrid(); });
|
||||
var wt = $('[data-vc-wished-toggle]');
|
||||
if (wt) wt.addEventListener('click', function () {
|
||||
state.wishedOnly = !state.wishedOnly;
|
||||
wt.classList.toggle('vc-tool-btn--on', state.wishedOnly);
|
||||
applyGrid();
|
||||
});
|
||||
document.addEventListener('soulsync:video-open-detail', onOpen);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2874,3 +2874,48 @@ body[data-side="video"] #soulsync-toggle { display: none; }
|
|||
color: #ff6b6b; text-decoration: none; padding: 6px 13px; border-radius: 999px;
|
||||
border: 1px solid rgba(255, 59, 59, 0.35); transition: all 0.15s ease; }
|
||||
.vwsh-nebula .vwsh-yt-watch:hover { background: rgba(255, 59, 59, 0.14); color: #fff; }
|
||||
|
||||
/* ── channel page: tags ribbon, toolbar, load-more, inline detail, playlists ── */
|
||||
.vc-tags { display: flex; flex-wrap: wrap; gap: 7px; margin-top: 14px; }
|
||||
.vc-tag { font-size: 11px; font-weight: 700; color: rgba(255,255,255,0.7); background: rgba(255,255,255,0.06);
|
||||
border: 1px solid rgba(255,255,255,0.1); padding: 4px 10px; border-radius: 999px; }
|
||||
|
||||
.vc-section-title { font-size: 20px; font-weight: 800; color: #fff; margin: 0 0 16px; }
|
||||
.vc-pl-section { margin-bottom: 40px; }
|
||||
.vc-videos-head { display: flex; align-items: baseline; gap: 12px; }
|
||||
.vc-tools { margin-left: auto; display: flex; align-items: center; gap: 10px; }
|
||||
.vc-tool-btn { border: 1px solid rgba(255,255,255,0.14); background: rgba(255,255,255,0.05);
|
||||
color: rgba(255,255,255,0.72); font-size: 12px; font-weight: 700; padding: 7px 13px; border-radius: 999px; cursor: pointer; transition: all 0.15s ease; }
|
||||
.vc-tool-btn:hover { color: #fff; background: rgba(255,255,255,0.1); }
|
||||
.vc-tool-btn--on { background: rgba(108,211,145,0.16); border-color: rgba(108,211,145,0.4); color: #6cd391; }
|
||||
.vc-sort { background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.12); color: #fff;
|
||||
font-size: 12px; font-weight: 700; padding: 7px 11px; border-radius: 9px; cursor: pointer; }
|
||||
.vc-more { display: block; margin: 22px auto 0; border: 1px solid rgba(255,59,59,0.35); background: rgba(255,59,59,0.08);
|
||||
color: #ff6b6b; font-size: 13px; font-weight: 800; padding: 11px 28px; border-radius: 999px; cursor: pointer; transition: all 0.15s ease; }
|
||||
.vc-more:hover { background: rgba(255,59,59,0.16); color: #fff; }
|
||||
|
||||
/* clickable card body → inline detail panel */
|
||||
.vc-vid-body { margin-top: 9px; flex: 1; cursor: pointer; }
|
||||
.vc-vid-body:hover .vc-vid-title { color: #ff6b6b; }
|
||||
.vc-vid-detail { margin-top: 8px; padding: 11px 12px; border-radius: 10px; background: rgba(255,255,255,0.04);
|
||||
border: 1px solid rgba(255,255,255,0.07); }
|
||||
.vc-vid-detail-load { font-size: 12px; color: rgba(255,255,255,0.5); }
|
||||
.vc-vid-stats { font-size: 11.5px; font-weight: 700; color: #ff8a8a; margin-bottom: 6px; }
|
||||
.vc-vid-desc { font-size: 12px; line-height: 1.55; color: rgba(255,255,255,0.62); white-space: pre-line;
|
||||
max-height: 220px; overflow-y: auto; scrollbar-width: thin; }
|
||||
|
||||
/* playlists as collapsible "seasons" */
|
||||
.vc-playlists { display: flex; flex-direction: column; gap: 10px; }
|
||||
.vc-pl { border-radius: 12px; border: 1px solid rgba(255,255,255,0.07); background: rgba(255,255,255,0.02); overflow: hidden; }
|
||||
.vc-pl-hd { display: flex; align-items: center; gap: 13px; padding: 10px 14px; cursor: pointer; transition: background 0.15s ease; }
|
||||
.vc-pl-hd:hover { background: rgba(255,59,59,0.06); }
|
||||
.vc-pl-thumb { flex-shrink: 0; width: 64px; height: 36px; border-radius: 6px; object-fit: cover; background: #16161d;
|
||||
display: flex; align-items: center; justify-content: center; color: rgba(255,59,59,0.5); font-size: 14px; }
|
||||
.vc-pl-meta { flex: 1; min-width: 0; display: flex; flex-direction: column; }
|
||||
.vc-pl-title { font-size: 14px; font-weight: 800; color: #fff; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.vc-pl-count { font-size: 11.5px; font-weight: 600; color: rgba(255,255,255,0.42); }
|
||||
.vc-pl-chev { flex-shrink: 0; color: rgba(255,255,255,0.35); transition: transform 0.2s ease; }
|
||||
.vc-pl--collapsed .vc-pl-chev { transform: rotate(-90deg); }
|
||||
.vc-pl-vids { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 16px; padding: 6px 14px 16px; }
|
||||
.vc-pl--collapsed .vc-pl-vids { display: none; }
|
||||
.vc-pl-loading { padding: 14px; color: rgba(255,255,255,0.5); font-size: 13px; }
|
||||
|
|
|
|||
Loading…
Reference in a new issue