openvibers/openvibe.live Commits
Channel: the offline header shows the streamer's picture in a circle (it only ever got the first letter, unstyled); one DOM helper for both headers (#190)
Changes
@@@ -1070,6 +1070,12 @@padding: 14px clamp(12px, 2vw, 20px); background: var(--bg-card); border-bottom: 1px solid var(--border);}.ch-avatar-lg { width: 64px; height: 64px; font-size: 1.6rem; }++/* The offline header's picture (its size is .ch-avatar-lg's): a circle like the live bar's, the letter on the accent. */++.ch-offline-header .ch-avatar {++ border-radius: 50%; overflow: hidden; flex-shrink: 0;++ background: var(--primary, var(--accent)); color: var(--bg-main, #000);++ display: flex; align-items: center; justify-content: center; font-weight: 700;++}.ch-offline-header .ch-username { margin-bottom: 2px; }.ch-offline-header .ch-description:empty { display: none; }.ch-offline-header .ch-actions { display: flex; gap: 6px; margin-left: auto; }@@@ -1127,7 +1133,7 @@#page-channel .stream-layout { height: auto; }}.ch-info-bar .ch-avatar { width: 52px; height: 52px; overflow: hidden; cursor: pointer; }--.ch-info-bar .ch-avatar img { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; display: block; }++.ch-info-bar .ch-avatar img, .ch-offline-header .ch-avatar img { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; display: block; }.ch-info-bar-top h2 a:hover { text-decoration: underline !important; }.ch-stream-title { font-size: 1.02rem; color: var(--text); white-space: normal; margin-top: 4px; }.ch-stream-description { font-size: 0.9rem; }@@@ -158,6 +158,22 @@ async function renderChannelVodsSection(username, liveStreams, vods, meta = {})// Can the current user manage a channel's content (its owner, or any admin)?// OpenVibe staff badge for a user (admin/owner -> Staff · Admin, mod -> Staff · Mod).++// The streamer's picture in a channel header (the live info bar and the offline header), or the first letter of the++// name when there is none or it fails to load. `path` makes it a link (the live bar); the offline header has none.++function _fillChannelAvatar(el, ch, path) {++ if (!el) return;++ const letter = ((ch.username || '?')[0] || '?').toUpperCase();++ el.textContent = letter;++ if (ch.avatar_url) {++ const img = document.createElement('img');++ img.alt = '';++ img.onerror = () => { el.textContent = letter; };++ img.src = ch.avatar_url;++ el.replaceChildren(img);++ }++ if (path) { el.style.cursor = 'pointer'; el.onclick = () => navigate(path); }++}++function _staffBadge(role, isOwner) {if (role === 'admin' || isOwner) return '<span class="staff-badge staff-badge-admin" data-tip="Staff - Admin"><i class="fa-solid fa-shield-halved"></i></span>';if (role === 'global_mod') return '<span class="staff-badge staff-badge-mod" data-tip="Staff - Mod"><i class="fa-solid fa-shield"></i></span>';@@@ -732,14 +748,7 @@ async function loadChannelPage(username, managedStreamRef = null, legacySessionI// Populate streamer info bar (below video). Avatar image (letter fallback)// + display name both link to the channel; the @handle is dropped.const _chPath = channelPath(ch.username);-- const _chAvatar = document.getElementById('ch-avatar');-- if (_chAvatar) {-- _chAvatar.innerHTML = ch.avatar_url-- ? `<img src="${esc(ch.avatar_url)}" alt="" onerror="this.style.display='none';this.parentNode.textContent='${((ch.username || '?')[0] || '?').toUpperCase()}'">`-- : ((ch.username || '?')[0] || '?').toUpperCase();-- _chAvatar.style.cursor = 'pointer';-- _chAvatar.onclick = () => navigate(_chPath);-- }++ _fillChannelAvatar(document.getElementById('ch-avatar'), ch, _chPath);const _chName = document.getElementById('ch-display-name');if (_chName) {// Name only — the h2 clips with ellipsis, so the staff badge lives in@@@ -810,7 +819,7 @@ async function loadChannelPage(username, managedStreamRef = null, legacySessionI_placeBelowFold();// Populate offline header-- document.getElementById('ch-avatar-offline').textContent = (ch.username || '?')[0].toUpperCase();++ _fillChannelAvatar(document.getElementById('ch-avatar-offline'), ch);document.getElementById('ch-display-name-offline').innerHTML = `${esc(ch.display_name || ch.username)} ${_staffBadge(ch.role, ch.is_owner)}`;_activeChannelIsOwnerRank = !!ch.is_owner;document.getElementById('ch-username-offline').textContent = '@' + ch.username;
@@ -1070,6 +1070,12 @@
@@ -1070,6 +1070,12 @@
padding: 14px clamp(12px, 2vw, 20px); background: var(--bg-card); border-bottom: 1px solid var(--border);
padding: 14px clamp(12px, 2vw, 20px); background: var(--bg-card); border-bottom: 1px solid var(--border);
}
}
.ch-avatar-lg { width: 64px; height: 64px; font-size: 1.6rem; }
.ch-avatar-lg { width: 64px; height: 64px; font-size: 1.6rem; }
+/* The offline header's picture (its size is .ch-avatar-lg's): a circle like the live bar's, the letter on the accent. */
+.ch-offline-header .ch-avatar {
+ border-radius: 50%; overflow: hidden; flex-shrink: 0;
+ background: var(--primary, var(--accent)); color: var(--bg-main, #000);
+ display: flex; align-items: center; justify-content: center; font-weight: 700;
+}
.ch-offline-header .ch-username { margin-bottom: 2px; }
.ch-offline-header .ch-username { margin-bottom: 2px; }
.ch-offline-header .ch-description:empty { display: none; }
.ch-offline-header .ch-description:empty { display: none; }
.ch-offline-header .ch-actions { display: flex; gap: 6px; margin-left: auto; }
.ch-offline-header .ch-actions { display: flex; gap: 6px; margin-left: auto; }
@@ -1127,7 +1133,7 @@
@@ -1127,7 +1133,7 @@
#page-channel .stream-layout { height: auto; }
#page-channel .stream-layout { height: auto; }
}
}
.ch-info-bar .ch-avatar { width: 52px; height: 52px; overflow: hidden; cursor: pointer; }
.ch-info-bar .ch-avatar { width: 52px; height: 52px; overflow: hidden; cursor: pointer; }
-.ch-info-bar .ch-avatar img { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; display: block; }
+.ch-info-bar .ch-avatar img, .ch-offline-header .ch-avatar img { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; display: block; }
.ch-info-bar-top h2 a:hover { text-decoration: underline !important; }
.ch-info-bar-top h2 a:hover { text-decoration: underline !important; }
.ch-stream-title { font-size: 1.02rem; color: var(--text); white-space: normal; margin-top: 4px; }
.ch-stream-title { font-size: 1.02rem; color: var(--text); white-space: normal; margin-top: 4px; }
.ch-stream-description { font-size: 0.9rem; }
.ch-stream-description { font-size: 0.9rem; }
@@ -158,6 +158,22 @@ async function renderChannelVodsSection(username, liveStreams, vods, meta = {})
@@ -158,6 +158,22 @@ async function renderChannelVodsSection(username, liveStreams, vods, meta = {})
// Can the current user manage a channel's content (its owner, or any admin)?
// Can the current user manage a channel's content (its owner, or any admin)?
// OpenVibe staff badge for a user (admin/owner -> Staff · Admin, mod -> Staff · Mod).
// OpenVibe staff badge for a user (admin/owner -> Staff · Admin, mod -> Staff · Mod).
+// The streamer's picture in a channel header (the live info bar and the offline header), or the first letter of the
+// name when there is none or it fails to load. `path` makes it a link (the live bar); the offline header has none.
+function _fillChannelAvatar(el, ch, path) {
+ if (!el) return;
+ const letter = ((ch.username || '?')[0] || '?').toUpperCase();
+ el.textContent = letter;
+ if (ch.avatar_url) {
+ const img = document.createElement('img');
+ img.alt = '';
+ img.onerror = () => { el.textContent = letter; };
+ img.src = ch.avatar_url;
+ el.replaceChildren(img);
+ }
+ if (path) { el.style.cursor = 'pointer'; el.onclick = () => navigate(path); }
+}
+
function _staffBadge(role, isOwner) {
function _staffBadge(role, isOwner) {
if (role === 'admin' || isOwner) return '<span class="staff-badge staff-badge-admin" data-tip="Staff - Admin"><i class="fa-solid fa-shield-halved"></i></span>';
if (role === 'admin' || isOwner) return '<span class="staff-badge staff-badge-admin" data-tip="Staff - Admin"><i class="fa-solid fa-shield-halved"></i></span>';
if (role === 'global_mod') return '<span class="staff-badge staff-badge-mod" data-tip="Staff - Mod"><i class="fa-solid fa-shield"></i></span>';
if (role === 'global_mod') return '<span class="staff-badge staff-badge-mod" data-tip="Staff - Mod"><i class="fa-solid fa-shield"></i></span>';
@@ -732,14 +748,7 @@ async function loadChannelPage(username, managedStreamRef = null, legacySessionI
@@ -732,14 +748,7 @@ async function loadChannelPage(username, managedStreamRef = null, legacySessionI
// Populate streamer info bar (below video). Avatar image (letter fallback)
// Populate streamer info bar (below video). Avatar image (letter fallback)
// + display name both link to the channel; the @handle is dropped.
// + display name both link to the channel; the @handle is dropped.
const _chPath = channelPath(ch.username);
const _chPath = channelPath(ch.username);
- const _chAvatar = document.getElementById('ch-avatar');
- if (_chAvatar) {
- _chAvatar.innerHTML = ch.avatar_url
- ? `<img src="${esc(ch.avatar_url)}" alt="" onerror="this.style.display='none';this.parentNode.textContent='${((ch.username || '?')[0] || '?').toUpperCase()}'">`
- : ((ch.username || '?')[0] || '?').toUpperCase();
- _chAvatar.style.cursor = 'pointer';
- _chAvatar.onclick = () => navigate(_chPath);
- }
+ _fillChannelAvatar(document.getElementById('ch-avatar'), ch, _chPath);
const _chName = document.getElementById('ch-display-name');
const _chName = document.getElementById('ch-display-name');
if (_chName) {
if (_chName) {
// Name only — the h2 clips with ellipsis, so the staff badge lives in
// Name only — the h2 clips with ellipsis, so the staff badge lives in
@@ -810,7 +819,7 @@ async function loadChannelPage(username, managedStreamRef = null, legacySessionI
@@ -810,7 +819,7 @@ async function loadChannelPage(username, managedStreamRef = null, legacySessionI
_placeBelowFold();
_placeBelowFold();
// Populate offline header
// Populate offline header
- document.getElementById('ch-avatar-offline').textContent = (ch.username || '?')[0].toUpperCase();
+ _fillChannelAvatar(document.getElementById('ch-avatar-offline'), ch);
document.getElementById('ch-display-name-offline').innerHTML = `${esc(ch.display_name || ch.username)} ${_staffBadge(ch.role, ch.is_owner)}`;
document.getElementById('ch-display-name-offline').innerHTML = `${esc(ch.display_name || ch.username)} ${_staffBadge(ch.role, ch.is_owner)}`;
_activeChannelIsOwnerRank = !!ch.is_owner;
_activeChannelIsOwnerRank = !!ch.is_owner;
document.getElementById('ch-username-offline').textContent = '@' + ch.username;
document.getElementById('ch-username-offline').textContent = '@' + ch.username;