openvibers/openvibe.live Commits

Go Live page reorganized, and the popout chat fixed.

OP OpenVibers committed 29 Sept 2026 · ad5e47f435 · 4 files, +103 −49

Go Live: no box around the page (the main column is transparent while the slot manager shows) and one scroll, a thin themed scrollbar at the window edge instead of one in the middle of the page. The slot list and the stream panel are two cards side by side; the list is as tall as its slots and stays in view while the panel scrolls (on phones it stays a row of chips above the panel). On wide panels (container query, 960px+) the form splits in two: the stream's details on the left, the streaming method and capture mode (2 x 2) on the right, so fields are no longer a thousand pixels wide. The category picker is gone: the AI decides the category from the stream itself (streams.ai_category already wins wherever a category shows; saving a slot sets it to Auto). NSFW is a themed switch that says what it does. Video quality reads as a row that opens: a chevron and the current values (720p · 30 fps · 2500 kbps). The selected method card uses the theme's accent instead of a fixed yellow tint that read brown on dark themes.

Popout chat: the Chat button on a stream slot opens the channel's own chat (/popout/<username>, which follows the live state), not global chat, before going live too. The popout page loads i18n-star.css, so the translate button is a hover control there as in the app instead of a bare box on every line.

Changes

ddiff --git a/public/css/broadcast.css b/public/css/broadcast.css
iindex 666ebaae..13f7167d 100644
---- a/public/css/broadcast.css
++++ b/public/css/broadcast.css
@@@ -1067,8 +1067,9 @@
.bc-method-card.selected,
.bc-method-card-sm.selected {
border-color: var(--accent);
-- background: rgba(241, 196, 15, 0.08);
-- box-shadow: 0 0 0 1px var(--accent);
++ /* The theme's accent (a fixed yellow tint read brown on dark themes). */
++ background: color-mix(in srgb, var(--accent) 12%, var(--bg-tertiary));
++ box-shadow: 0 0 0 1px var(--accent), 0 6px 18px color-mix(in srgb, var(--accent) 18%, transparent);
}
.bc-method-card i {
ddiff --git a/public/css/features/broadcast.css b/public/css/features/broadcast.css
iindex ebe81d8e..95afa20f 100644
---- a/public/css/features/broadcast.css
++++ b/public/css/features/broadcast.css
@@@ -2373,3 +2373,82 @@
font-size: 0.72rem; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase;
color: var(--text-secondary); margin-bottom: 6px; display: flex; align-items: center; gap: 6px;
}
++
++/* ═══ Go Live workspace layout (2026-09-28) ═══════════════════════════════════
++ No box around the page: while the slot manager shows, the main column is transparent and is the one scroll
++ (a thin themed scrollbar at the window edge, not one in the middle of the page). The slot list and the panel
++ are two cards side by side: the list is as tall as its slots and stays in view while the panel scrolls. */
++.broadcast-main:has(> #bc-stream-manager:not([style*="display: none"])) {
++ background: transparent; border: 0; border-radius: 0; box-shadow: none;
++}
++.broadcast-main { scrollbar-width: thin; scrollbar-color: color-mix(in srgb, var(--accent) 38%, transparent) transparent; }
++.broadcast-main::-webkit-scrollbar { width: 8px; }
++.broadcast-main::-webkit-scrollbar-thumb { background: color-mix(in srgb, var(--accent) 38%, transparent); border-radius: 8px; }
++.broadcast-main::-webkit-scrollbar-track { background: transparent; }
++#bc-stream-manager { max-width: 1480px; padding: 4px clamp(0px, 1vw, 12px) 28px; flex: none; min-height: auto; overflow: visible; }
++#bc-stream-manager > h2 { margin: 0 0 2px; font-size: 1.22rem; flex-wrap: wrap; row-gap: 6px; }
++#bc-stream-manager > h2 .glw-open-btn { margin-left: 6px; }
++#bc-stream-manager > .bc-page-subtitle { margin: 0 0 10px; font-size: 0.86rem; }
++.bc-workspace { flex: none; border: 0; border-radius: 0; background: transparent; overflow: visible; gap: 14px; align-items: flex-start; margin-top: 6px; }
++.bc-ws-sidebar, .bc-ws-main {
++ border: 1px solid var(--border); border-radius: 14px;
++ background: color-mix(in srgb, var(--bg-card, var(--surface)) 92%, transparent);
++ box-shadow: 0 1px 0 color-mix(in srgb, var(--text-primary) 4%, transparent) inset;
++}
++.bc-ws-sidebar { width: clamp(200px, 17vw, 264px); border-right: 1px solid var(--border); overflow: hidden; }
++.bc-ws-sidebar-hd { padding: 10px 10px 10px 14px; }
++.bc-ws-list { padding: 4px; flex: none; }
++.bc-ws-item { border-radius: 10px; padding: 9px 10px; }
++.bc-ws-item.selected { box-shadow: inset 3px 0 0 var(--accent); }
++.bc-ws-main { min-height: 0; }
++.bc-ws-panel { overflow: visible; }
++@media (min-width: 681px) {
++ .bc-ws-sidebar { position: sticky; top: 0; align-self: flex-start; max-height: calc(100dvh - var(--navbar-h, 56px) - 40px); }
++ .bc-ws-list { overflow-y: auto; flex: 1 1 auto; }
++}
++@media (max-width: 680px) {
++ .bc-workspace { gap: 10px; }
++ .bc-ws-sidebar { width: 100%; border-bottom: 1px solid var(--border); }
++ .bc-ws-main { width: 100%; }
++}
++
++/* NSFW: a themed switch with what it does, instead of a bare checkbox floating beside a select. */
++.bc-switch { display: flex; align-items: flex-start; gap: 12px; margin: 2px 0 16px; padding: 10px 12px; cursor: pointer;
++ border: 1px solid var(--border); border-radius: 12px; background: color-mix(in srgb, var(--bg-input, var(--bg)) 70%, transparent);
++ transition: border-color .18s, background-color .18s; }
++.bc-switch:hover { border-color: color-mix(in srgb, var(--accent) 40%, var(--border)); }
++.bc-switch input { position: absolute; opacity: 0; width: 1px; height: 1px; pointer-events: none; }
++.bc-switch-track { flex: none; position: relative; width: 38px; height: 22px; margin-top: 1px; border-radius: 999px;
++ background: color-mix(in srgb, var(--text-muted) 35%, transparent); border: 1px solid var(--border); transition: background-color .2s, border-color .2s; }
++.bc-switch-thumb { position: absolute; top: 2px; left: 2px; width: 16px; height: 16px; border-radius: 50%; background: var(--text-primary);
++ box-shadow: 0 1px 3px rgba(0,0,0,.35); transition: transform .22s cubic-bezier(.22,1,.36,1), background-color .2s; }
++.bc-switch input:checked + .bc-switch-track { background: var(--danger, #ef4444); border-color: var(--danger, #ef4444); }
++.bc-switch input:checked + .bc-switch-track .bc-switch-thumb { transform: translateX(16px); background: #fff; }
++.bc-switch input:focus-visible + .bc-switch-track { outline: 2px solid var(--accent); outline-offset: 2px; }
++.bc-switch:has(input:checked) { border-color: color-mix(in srgb, var(--danger, #ef4444) 45%, var(--border)); background: color-mix(in srgb, var(--danger, #ef4444) 7%, transparent); }
++.bc-switch-text { display: grid; gap: 2px; min-width: 0; }
++.bc-switch-text b { font-size: 0.88rem; font-weight: 700; color: var(--text-primary); }
++.bc-switch-text b i { color: var(--danger, #ef4444); margin-right: 3px; }
++.bc-switch-text small { font-size: 0.76rem; color: var(--text-muted); line-height: 1.4; }
++
++/* Video quality: reads as a row that opens (chevron, current values), not a static label. */
++.bc-ws-quality { padding: 0; border-radius: 12px; overflow: hidden; }
++.bc-ws-quality > summary { display: flex; align-items: center; gap: 8px; padding: 11px 12px; cursor: pointer; list-style: none; user-select: none;
++ font-size: 0.86rem; font-weight: 650; color: var(--text-primary); transition: background-color .15s; }
++.bc-ws-quality > summary::-webkit-details-marker { display: none; }
++.bc-ws-quality > summary:hover { background: color-mix(in srgb, var(--accent) 7%, transparent); }
++.bc-ws-quality > summary > i:first-child { color: var(--accent); margin: 0; }
++.bc-sum-val { margin-left: auto; font-size: 0.76rem; font-weight: 500; color: var(--text-muted); white-space: nowrap; }
++.bc-sum-chev { color: var(--text-muted); font-size: 0.75rem; transition: transform .22s ease; }
++.bc-ws-quality[open] .bc-sum-chev { transform: rotate(180deg); color: var(--accent); }
++.bc-ws-quality[open] > summary { border-bottom: 1px solid var(--border); }
++.bc-ws-quality .bc-ws-quality-inner { margin: 0; padding: 12px; }
++@media (prefers-reduced-motion: reduce) { .bc-switch-thumb, .bc-sum-chev { transition: none; } }
++
++/* Wide panels: details on the left, how you stream on the right, so fields are not a thousand pixels wide. */
++.bc-ws-main { container-type: inline-size; container-name: bcws; }
++@container bcws (min-width: 960px) {
++ .bc-ws-profile-split { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.2fr); gap: 8px 28px; align-items: start; }
++ .bc-ws-profile-split > .bc-ws-col--method .bc-method-picker-sm { grid-template-columns: repeat(2, minmax(0, 1fr)); }
++ .bc-ws-profile-split > .bc-ws-col--details .bc-switch { margin-bottom: 14px; }
++}
ddiff --git a/public/js/broadcast-workspace.js b/public/js/broadcast-workspace.js
iindex b8e40389..5dbca000 100644
---- a/public/js/broadcast-workspace.js
++++ b/public/js/broadcast-workspace.js
@@@ -403,7 +403,8 @@ function _wsRenderPanel() {
<!-- ═══ TAB: Stream Profile ═══ -->
<div class="bc-ws-tab-panel active" data-wstabpanel="profile">
-- <div class="bc-ws-profile-section">
++ <div class="bc-ws-profile-section bc-ws-profile-split">
++ <div class="bc-ws-col bc-ws-col--details">
<div class="form-group">
<label>Title</label>
@@@ -420,22 +421,12 @@ function _wsRenderPanel() {
oninput="_wsMarkDirty()">${esc(ms.description || '')}</textarea>
</div>
-- <div class="bc-ws-row">
-- <div class="form-group" style="flex:2">
-- <label>Category</label>
-- <select id="bc-category" class="form-input" onchange="_wsMarkDirty()">
-- ${_wsRenderCategoryOptions(ms.category || '')}
-- </select>
-- </div>
-- <div class="form-group" style="flex:1">
-- <label>&nbsp;</label>
-- <label class="bc-toggle-label">
-- <input type="checkbox" id="bc-nsfw" ${ms.is_nsfw ? 'checked' : ''}
-- onchange="_wsMarkDirty()">
-- <i class="fa-solid fa-triangle-exclamation" style="color:var(--danger)"></i> NSFW
-- </label>
-- </div>
-- </div>
++ <!-- No category picker: the AI decides it from the stream itself (streams.ai_category wins everywhere). -->
++ <label class="bc-switch">
++ <input type="checkbox" id="bc-nsfw" ${ms.is_nsfw ? 'checked' : ''} onchange="_wsMarkDirty()">
++ <span class="bc-switch-track" aria-hidden="true"><span class="bc-switch-thumb"></span></span>
++ <span class="bc-switch-text"><b><i class="fa-solid fa-triangle-exclamation"></i> Mature content (NSFW)</b><small>Viewers confirm their age before the stream plays. The category is set automatically from what is on stream.</small></span>
++ </label>
<!-- URL Slug -->
<div class="form-group">
@@@ -448,6 +439,8 @@ function _wsRenderPanel() {
</div>
</div>
++ </div>
++ <div class="bc-ws-col bc-ws-col--method">
<!-- ═══ Streaming Method ═══ -->
<div class="form-group">
<label>Streaming Method</label>
@@@ -604,7 +597,7 @@ function _wsRenderPanel() {
<!-- Video quality defaults (Class A blob, browser method only) -->
<div id="bc-ws-quality-wrap" style="${method === 'browser' ? '' : 'display:none'}">
<details class="bc-ws-quality">
-- <summary><i class="fa-solid fa-film"></i> Video Quality Defaults</summary>
++ <summary><i class="fa-solid fa-film"></i><span class="bc-sum-title">Video quality</span><small class="bc-sum-val">${esc(p.resolution || '720')}p · ${esc(p.fps || '30')} fps · ${esc(p.bitrate || 2500)} kbps</small><i class="fa-solid fa-chevron-down bc-sum-chev" aria-hidden="true"></i></summary>
<div class="bc-ws-quality-inner bc-ws-row">
<div class="form-group" style="margin:0;flex:1">
<label style="font-size:0.82rem">Resolution</label>
@@@ -630,6 +623,8 @@ function _wsRenderPanel() {
</details>
</div>
++ </div>
++
<!-- Control profile lives under Settings; createNewStream() and the save
payload both read #bc-control-config, which is rendered there. -->
@@@ -1339,21 +1334,13 @@ async function _wsEndStream() {
function _wsOpenPopoutChat() {
const ms = _wsState.selectedMs;
if (!ms) return;
--
-- // Use the managed stream's live session ID if live, otherwise open global chat for the channel
-- const liveSessionId = _wsGetLiveSessionId(ms.id);
-- if (liveSessionId && typeof popoutChat === 'function') {
-- popoutChat('stream', liveSessionId, (typeof currentUser !== 'undefined' && currentUser && currentUser.username) || null);
-- } else {
-- // Even when not live, open a popout chat scoped to the channel
-- if (typeof popoutChat === 'function') {
-- popoutChat('global');
-- } else {
-- const params = new URLSearchParams({ popout: '1', mode: 'global' });
-- window.open(`/popout-chat.html?${params.toString()}`, `openvibe-chat-global`,
-- 'width=400,height=600,menubar=no,toolbar=no,resizable=yes');
-- }
-- }
++ // The channel's own chat: /popout/<username>[/<live session>] follows the channel's live state, so it is the
++ // right window before going live too (never global chat).
++ const liveSessionId = _wsGetLiveSessionId(ms.id) || null;
++ const username = (typeof currentUser !== 'undefined' && currentUser && currentUser.username) || null;
++ if (typeof popoutChat === 'function' && (username || liveSessionId)) { popoutChat('stream', liveSessionId, username); return; }
++ if (!username) return;
++ window.open(`/popout/${encodeURIComponent(username)}`, `openvibe_chat_chan-${username}`, 'width=400,height=600,menubar=no,toolbar=no,resizable=yes');
}
/* ── Method cards ────────────────────────────────────────────── */
@@@ -1390,20 +1377,6 @@ function _wsRenderBrowserModeCards(selected) {
</div>`).join('');
}
--/* ── Category options ────────────────────────────────────────── */
--
--function _wsRenderCategoryOptions(selected) {
-- const cats = [
-- ['', 'Auto — the AI decides from the stream'],
-- ['outdoors', 'Outdoors'], ['travel', 'Travel'], ['building', 'Building/Craft'],
-- ['music', 'Music'], ['gaming', 'Gaming'], ['robot', 'Robot'],
-- ['desktop', 'Desktop'], ['irl', 'IRL'], ['other', 'Other'],
-- ];
-- return cats.map(([v, l]) =>
-- `<option value="${v}"${v === selected ? ' selected' : ''}>${l}</option>`
-- ).join('');
--}
--
/* ── Method-specific endpoint info ───────────────────────────── */
function _wsRenderMethodEndpoint(method, streamKey, managedStreamId) {
ddiff --git a/public/popout-chat.html b/public/popout-chat.html
iindex 157e0d0e..b7de9424 100644
---- a/public/popout-chat.html
++++ b/public/popout-chat.html
@@@ -9,6 +9,7 @@
inline next to the username as "broken badges" until their cleanup timer). -->
<link rel="stylesheet" href="/css/style.css">
<link rel="stylesheet" href="/css/chat-shared.css">
++ <link rel="stylesheet" href="/css/i18n-star.css">
<link rel="stylesheet" href="/css/cosmetics.css">
<link rel="stylesheet" href="/css/vendor/fontawesome-6.5.1.min.css">
<style>