openvibers/openvibe.mediahub Commits
Video home: a taste and a door on phones (#193)
Changes
@@@ -582,3 +582,23 @@ html.ov-motion .ovv-stat.is-live .ovv-dot { animation: ovv-pulse 1.6s ease-out i.ovv-stat b { font-size: 1.2rem; }}@media (prefers-reduced-motion: reduce) { .ovv-stat { transition: none; } .ovv-stat:hover { transform: none; } }++++/* The home on phones (2026-10-29): a taste and a door. Each video list is one row that scrolls sideways (every video is++ still there), eight channels show, the figures are one strip, and how it works folds under its heading. */++.ovv-fold { border: 1px solid var(--border, #1f2d47); border-radius: 14px; background: var(--bg-card, #131c2e); }++.ovv-fold > summary { display: flex; align-items: center; justify-content: space-between; gap: 10px; min-height: 52px; padding: 0 16px; cursor: pointer; list-style: none; }++.ovv-fold > summary::-webkit-details-marker { display: none; }++.ovv-fold > summary h2 { margin: 0; }++.ovv-fold > summary span { color: var(--text-muted, #7386a3); font-weight: 600; font-size: .86rem; text-align: right; }++.ovv-fold > summary:focus-visible { outline: 2px solid var(--accent, #3b82f6); outline-offset: -2px; border-radius: 14px; }++.ovv-fold > .ovv-steps { padding: 0 16px 16px; }++@media (max-width: 599px) {++ .ovv-row .ovv-grid { display: flex; overflow-x: auto; scroll-snap-type: x mandatory; scroll-padding-inline: 16px; scrollbar-width: none; gap: 12px; margin-left: -16px; margin-right: -16px; padding: 0 16px 4px; }++ .ovv-row .ovv-grid::-webkit-scrollbar { display: none; }++ .ovv-row .ovv-grid > * { flex: 0 0 46%; min-width: 0; scroll-snap-align: start; }++ #channels .ovv-people > li:nth-child(n+9) { display: none; }++ .ovv-hero .ovv-stats { display: flex; overflow-x: auto; scrollbar-width: none; gap: 8px; margin-left: -16px; margin-right: -16px; padding: 0 16px 2px; }++ .ovv-hero .ovv-stats::-webkit-scrollbar { display: none; }++ .ovv-hero .ovv-stats > li { flex: 0 0 auto; width: 132px; }++ .ovv-fold > summary span { display: none; }++}@@@ -979,18 +979,18 @@ ${visibilityChoices(item.visibility || 'public')}<p class="ovv-hero-actions"><a class="ovv-btn ovv-btn--primary" href="/upload"><i class="fa-solid fa-upload" aria-hidden="true"></i> Upload a video</a> <a class="ovv-btn" href="/record"><i class="fa-solid fa-video" aria-hidden="true"></i> Record in the browser</a> <a class="ovv-btn" href="${livePublic}/docs/go-live-in-your-browser"><i class="fa-solid fa-tower-broadcast" aria-hidden="true"></i> Go live from your browser</a></p>${searchForm()}${stats.length ? html`<ul class="ovv-stats" role="list">${stats.map((s) => html`<li><a class="${s.live ? 'ovv-stat is-live' : 'ovv-stat'}" href="${s.href}"${s.title ? html` title="${s.title}"` : ''}><b>${s.value}</b><span>${s.live ? html`<i class="ovv-dot" aria-hidden="true"></i> ` : ''}${s.label}</span></a></li>`)}</ul>` : ''}</header>--${onAirCards.length ? html`<section class="ovv-section" aria-labelledby="h-live"><div class="ovv-section-head"><h2 id="h-live"><span class="ovv-dot" aria-hidden="true"></span> Live now on OpenVibe.Live</h2><a href="${livePublic}/">Everyone live</a></div>${grid(onAirCards)}</section>` : ''}--${uploaded.length ? html`<section class="ovv-section" aria-labelledby="h-uploads"><h2 id="h-uploads">Latest uploads</h2>${grid(uploaded.map(uploadCard))}</section>` : ''}--${topItems.length ? html`<section class="ovv-section" aria-labelledby="h-top"><h2 id="h-top">Most watched this month</h2>${grid(topItems.map((it) => feedCard(it, { views: true })))}</section>` : ''}--<section class="ovv-section" id="recordings" aria-labelledby="h-vods"><h2 id="h-vods">Latest stream recordings</h2>${grid(vodItems.map(feedCard), 'No recordings yet.')}</section>++${onAirCards.length ? html`<section class="ovv-section ovv-row" aria-labelledby="h-live"><div class="ovv-section-head"><h2 id="h-live"><span class="ovv-dot" aria-hidden="true"></span> Live now on OpenVibe.Live</h2><a href="${livePublic}/">Everyone live</a></div>${grid(onAirCards)}</section>` : ''}++${uploaded.length ? html`<section class="ovv-section ovv-row" aria-labelledby="h-uploads"><h2 id="h-uploads">Latest uploads</h2>${grid(uploaded.map(uploadCard))}</section>` : ''}++${topItems.length ? html`<section class="ovv-section ovv-row" aria-labelledby="h-top"><h2 id="h-top">Most watched this month</h2>${grid(topItems.map((it) => feedCard(it, { views: true })))}</section>` : ''}++<section class="ovv-section ovv-row" id="recordings" aria-labelledby="h-vods"><h2 id="h-vods">Latest stream recordings</h2>${grid(vodItems.map(feedCard), 'No recordings yet.')}</section>${categories.length ? html`<section class="ovv-section" id="categories" aria-labelledby="h-cats"><h2 id="h-cats">Browse by category</h2><ul class="ovv-shelf" role="list">${categories.map((c) => html`<li><a class="ovv-shelf-tile" href="/category/${c.slug}"><i class="fa-solid ${c.icon}" aria-hidden="true"></i><span class="ovv-shelf-name">${c.label}</span><span class="ovv-shelf-count">${n(c.count)} ${c.count === 1 ? 'recording' : 'recordings'}</span></a></li>`)}</ul></section>` : ''}--<section class="ovv-section" id="clips" aria-labelledby="h-clips"><h2 id="h-clips">Latest clips</h2>${grid(clipItems.map(feedCard), 'No clips yet.')}</section>++<section class="ovv-section ovv-row" id="clips" aria-labelledby="h-clips"><h2 id="h-clips">Latest clips</h2>${grid(clipItems.map(feedCard), 'No clips yet.')}</section>${channels.length ? html`<section class="ovv-section" id="channels" aria-labelledby="h-channels"><h2 id="h-channels">Channels</h2><ul class="ovv-people" role="list">${channels.map((c) => html`<li><a class="ovv-person" href="/@${c.username}"><span class="ovv-avatar" aria-hidden="true">${c.username.slice(0, 1).toUpperCase()}<img src="${config.networkUrl}/avatar/${c.username}" alt="" loading="lazy" width="42" height="42"></span><span class="ovv-person-text"><span class="ovv-person-name">@${c.username}</span><span class="ovv-person-count">${n(c.count)} ${c.count === 1 ? 'recording' : 'recordings'}</span></span></a></li>`)}</ul></section>` : ''}--<section class="ovv-section" aria-labelledby="h-how"><h2 id="h-how">How it works</h2><ol class="ovv-steps">++<section class="ovv-section" aria-labelledby="h-how"><details class="ovv-fold"><summary><h2 id="h-how">How it works</h2><span>stream or upload, AI captions, share</span></summary><ol class="ovv-steps"><li><h3>Stream or upload</h3><p>Go live on OpenVibe.Live from your browser and the recording comes here, or upload a file: MP4, MOV, WebM or MKV up to ${formatBytes(config.videoUploads.maxBytes)}.</p></li><li><h3>Let AI do the busywork</h3><p>OpenVibe.AI writes captions, chapters and a summary. On an upload, the AI cut can make an edited copy with the pauses taken out.</p></li><li><h3>Share it</h3><p>Every video gets a watch page and a place on your channel. Save videos to playlists, and cut clips of up to ${CLIP_MAX_SECONDS} seconds from any recording.</p></li>--</ol></section>++</ol></details></section><section class="ovv-section" aria-labelledby="h-faq"><h2 id="h-faq">Questions</h2><div class="ovv-faq">${faq.map((f) => html`<details><summary>${f.q}</summary><p>${f.a}</p></details>`)}</div></section><section class="ovv-section" aria-labelledby="h-more"><h2 id="h-more">More on OpenVibe</h2><ul class="ovv-more" role="list"><li><a href="${livePublic}/"><strong>OpenVibe.Live</strong><span>Go live from your browser, no OBS needed.</span></a></li>
@@ -582,3 +582,23 @@ html.ov-motion .ovv-stat.is-live .ovv-dot { animation: ovv-pulse 1.6s ease-out i
@@ -582,3 +582,23 @@ html.ov-motion .ovv-stat.is-live .ovv-dot { animation: ovv-pulse 1.6s ease-out i
.ovv-stat b { font-size: 1.2rem; }
.ovv-stat b { font-size: 1.2rem; }
}
}
@media (prefers-reduced-motion: reduce) { .ovv-stat { transition: none; } .ovv-stat:hover { transform: none; } }
@media (prefers-reduced-motion: reduce) { .ovv-stat { transition: none; } .ovv-stat:hover { transform: none; } }
+
+/* The home on phones (2026-10-29): a taste and a door. Each video list is one row that scrolls sideways (every video is
+ still there), eight channels show, the figures are one strip, and how it works folds under its heading. */
+.ovv-fold { border: 1px solid var(--border, #1f2d47); border-radius: 14px; background: var(--bg-card, #131c2e); }
+.ovv-fold > summary { display: flex; align-items: center; justify-content: space-between; gap: 10px; min-height: 52px; padding: 0 16px; cursor: pointer; list-style: none; }
+.ovv-fold > summary::-webkit-details-marker { display: none; }
+.ovv-fold > summary h2 { margin: 0; }
+.ovv-fold > summary span { color: var(--text-muted, #7386a3); font-weight: 600; font-size: .86rem; text-align: right; }
+.ovv-fold > summary:focus-visible { outline: 2px solid var(--accent, #3b82f6); outline-offset: -2px; border-radius: 14px; }
+.ovv-fold > .ovv-steps { padding: 0 16px 16px; }
+@media (max-width: 599px) {
+ .ovv-row .ovv-grid { display: flex; overflow-x: auto; scroll-snap-type: x mandatory; scroll-padding-inline: 16px; scrollbar-width: none; gap: 12px; margin-left: -16px; margin-right: -16px; padding: 0 16px 4px; }
+ .ovv-row .ovv-grid::-webkit-scrollbar { display: none; }
+ .ovv-row .ovv-grid > * { flex: 0 0 46%; min-width: 0; scroll-snap-align: start; }
+ #channels .ovv-people > li:nth-child(n+9) { display: none; }
+ .ovv-hero .ovv-stats { display: flex; overflow-x: auto; scrollbar-width: none; gap: 8px; margin-left: -16px; margin-right: -16px; padding: 0 16px 2px; }
+ .ovv-hero .ovv-stats::-webkit-scrollbar { display: none; }
+ .ovv-hero .ovv-stats > li { flex: 0 0 auto; width: 132px; }
+ .ovv-fold > summary span { display: none; }
+}
@@ -979,18 +979,18 @@ ${visibilityChoices(item.visibility || 'public')}
@@ -979,18 +979,18 @@ ${visibilityChoices(item.visibility || 'public')}
<p class="ovv-hero-actions"><a class="ovv-btn ovv-btn--primary" href="/upload"><i class="fa-solid fa-upload" aria-hidden="true"></i> Upload a video</a> <a class="ovv-btn" href="/record"><i class="fa-solid fa-video" aria-hidden="true"></i> Record in the browser</a> <a class="ovv-btn" href="${livePublic}/docs/go-live-in-your-browser"><i class="fa-solid fa-tower-broadcast" aria-hidden="true"></i> Go live from your browser</a></p>
<p class="ovv-hero-actions"><a class="ovv-btn ovv-btn--primary" href="/upload"><i class="fa-solid fa-upload" aria-hidden="true"></i> Upload a video</a> <a class="ovv-btn" href="/record"><i class="fa-solid fa-video" aria-hidden="true"></i> Record in the browser</a> <a class="ovv-btn" href="${livePublic}/docs/go-live-in-your-browser"><i class="fa-solid fa-tower-broadcast" aria-hidden="true"></i> Go live from your browser</a></p>
${searchForm()}
${searchForm()}
${stats.length ? html`<ul class="ovv-stats" role="list">${stats.map((s) => html`<li><a class="${s.live ? 'ovv-stat is-live' : 'ovv-stat'}" href="${s.href}"${s.title ? html` title="${s.title}"` : ''}><b>${s.value}</b><span>${s.live ? html`<i class="ovv-dot" aria-hidden="true"></i> ` : ''}${s.label}</span></a></li>`)}</ul>` : ''}</header>
${stats.length ? html`<ul class="ovv-stats" role="list">${stats.map((s) => html`<li><a class="${s.live ? 'ovv-stat is-live' : 'ovv-stat'}" href="${s.href}"${s.title ? html` title="${s.title}"` : ''}><b>${s.value}</b><span>${s.live ? html`<i class="ovv-dot" aria-hidden="true"></i> ` : ''}${s.label}</span></a></li>`)}</ul>` : ''}</header>
-${onAirCards.length ? html`<section class="ovv-section" aria-labelledby="h-live"><div class="ovv-section-head"><h2 id="h-live"><span class="ovv-dot" aria-hidden="true"></span> Live now on OpenVibe.Live</h2><a href="${livePublic}/">Everyone live</a></div>${grid(onAirCards)}</section>` : ''}
-${uploaded.length ? html`<section class="ovv-section" aria-labelledby="h-uploads"><h2 id="h-uploads">Latest uploads</h2>${grid(uploaded.map(uploadCard))}</section>` : ''}
-${topItems.length ? html`<section class="ovv-section" aria-labelledby="h-top"><h2 id="h-top">Most watched this month</h2>${grid(topItems.map((it) => feedCard(it, { views: true })))}</section>` : ''}
-<section class="ovv-section" id="recordings" aria-labelledby="h-vods"><h2 id="h-vods">Latest stream recordings</h2>${grid(vodItems.map(feedCard), 'No recordings yet.')}</section>
+${onAirCards.length ? html`<section class="ovv-section ovv-row" aria-labelledby="h-live"><div class="ovv-section-head"><h2 id="h-live"><span class="ovv-dot" aria-hidden="true"></span> Live now on OpenVibe.Live</h2><a href="${livePublic}/">Everyone live</a></div>${grid(onAirCards)}</section>` : ''}
+${uploaded.length ? html`<section class="ovv-section ovv-row" aria-labelledby="h-uploads"><h2 id="h-uploads">Latest uploads</h2>${grid(uploaded.map(uploadCard))}</section>` : ''}
+${topItems.length ? html`<section class="ovv-section ovv-row" aria-labelledby="h-top"><h2 id="h-top">Most watched this month</h2>${grid(topItems.map((it) => feedCard(it, { views: true })))}</section>` : ''}
+<section class="ovv-section ovv-row" id="recordings" aria-labelledby="h-vods"><h2 id="h-vods">Latest stream recordings</h2>${grid(vodItems.map(feedCard), 'No recordings yet.')}</section>
${categories.length ? html`<section class="ovv-section" id="categories" aria-labelledby="h-cats"><h2 id="h-cats">Browse by category</h2><ul class="ovv-shelf" role="list">${categories.map((c) => html`<li><a class="ovv-shelf-tile" href="/category/${c.slug}"><i class="fa-solid ${c.icon}" aria-hidden="true"></i><span class="ovv-shelf-name">${c.label}</span><span class="ovv-shelf-count">${n(c.count)} ${c.count === 1 ? 'recording' : 'recordings'}</span></a></li>`)}</ul></section>` : ''}
${categories.length ? html`<section class="ovv-section" id="categories" aria-labelledby="h-cats"><h2 id="h-cats">Browse by category</h2><ul class="ovv-shelf" role="list">${categories.map((c) => html`<li><a class="ovv-shelf-tile" href="/category/${c.slug}"><i class="fa-solid ${c.icon}" aria-hidden="true"></i><span class="ovv-shelf-name">${c.label}</span><span class="ovv-shelf-count">${n(c.count)} ${c.count === 1 ? 'recording' : 'recordings'}</span></a></li>`)}</ul></section>` : ''}
-<section class="ovv-section" id="clips" aria-labelledby="h-clips"><h2 id="h-clips">Latest clips</h2>${grid(clipItems.map(feedCard), 'No clips yet.')}</section>
+<section class="ovv-section ovv-row" id="clips" aria-labelledby="h-clips"><h2 id="h-clips">Latest clips</h2>${grid(clipItems.map(feedCard), 'No clips yet.')}</section>
${channels.length ? html`<section class="ovv-section" id="channels" aria-labelledby="h-channels"><h2 id="h-channels">Channels</h2><ul class="ovv-people" role="list">${channels.map((c) => html`<li><a class="ovv-person" href="/@${c.username}"><span class="ovv-avatar" aria-hidden="true">${c.username.slice(0, 1).toUpperCase()}<img src="${config.networkUrl}/avatar/${c.username}" alt="" loading="lazy" width="42" height="42"></span><span class="ovv-person-text"><span class="ovv-person-name">@${c.username}</span><span class="ovv-person-count">${n(c.count)} ${c.count === 1 ? 'recording' : 'recordings'}</span></span></a></li>`)}</ul></section>` : ''}
${channels.length ? html`<section class="ovv-section" id="channels" aria-labelledby="h-channels"><h2 id="h-channels">Channels</h2><ul class="ovv-people" role="list">${channels.map((c) => html`<li><a class="ovv-person" href="/@${c.username}"><span class="ovv-avatar" aria-hidden="true">${c.username.slice(0, 1).toUpperCase()}<img src="${config.networkUrl}/avatar/${c.username}" alt="" loading="lazy" width="42" height="42"></span><span class="ovv-person-text"><span class="ovv-person-name">@${c.username}</span><span class="ovv-person-count">${n(c.count)} ${c.count === 1 ? 'recording' : 'recordings'}</span></span></a></li>`)}</ul></section>` : ''}
-<section class="ovv-section" aria-labelledby="h-how"><h2 id="h-how">How it works</h2><ol class="ovv-steps">
+<section class="ovv-section" aria-labelledby="h-how"><details class="ovv-fold"><summary><h2 id="h-how">How it works</h2><span>stream or upload, AI captions, share</span></summary><ol class="ovv-steps">
<li><h3>Stream or upload</h3><p>Go live on OpenVibe.Live from your browser and the recording comes here, or upload a file: MP4, MOV, WebM or MKV up to ${formatBytes(config.videoUploads.maxBytes)}.</p></li>
<li><h3>Stream or upload</h3><p>Go live on OpenVibe.Live from your browser and the recording comes here, or upload a file: MP4, MOV, WebM or MKV up to ${formatBytes(config.videoUploads.maxBytes)}.</p></li>
<li><h3>Let AI do the busywork</h3><p>OpenVibe.AI writes captions, chapters and a summary. On an upload, the AI cut can make an edited copy with the pauses taken out.</p></li>
<li><h3>Let AI do the busywork</h3><p>OpenVibe.AI writes captions, chapters and a summary. On an upload, the AI cut can make an edited copy with the pauses taken out.</p></li>
<li><h3>Share it</h3><p>Every video gets a watch page and a place on your channel. Save videos to playlists, and cut clips of up to ${CLIP_MAX_SECONDS} seconds from any recording.</p></li>
<li><h3>Share it</h3><p>Every video gets a watch page and a place on your channel. Save videos to playlists, and cut clips of up to ${CLIP_MAX_SECONDS} seconds from any recording.</p></li>
-</ol></section>
+</ol></details></section>
<section class="ovv-section" aria-labelledby="h-faq"><h2 id="h-faq">Questions</h2><div class="ovv-faq">${faq.map((f) => html`<details><summary>${f.q}</summary><p>${f.a}</p></details>`)}</div></section>
<section class="ovv-section" aria-labelledby="h-faq"><h2 id="h-faq">Questions</h2><div class="ovv-faq">${faq.map((f) => html`<details><summary>${f.q}</summary><p>${f.a}</p></details>`)}</div></section>
<section class="ovv-section" aria-labelledby="h-more"><h2 id="h-more">More on OpenVibe</h2><ul class="ovv-more" role="list">
<section class="ovv-section" aria-labelledby="h-more"><h2 id="h-more">More on OpenVibe</h2><ul class="ovv-more" role="list">
<li><a href="${livePublic}/"><strong>OpenVibe.Live</strong><span>Go live from your browser, no OBS needed.</span></a></li>
<li><a href="${livePublic}/"><strong>OpenVibe.Live</strong><span>Go live from your browser, no OBS needed.</span></a></li>