openvibers/openvibe.mediahub Commits

Video cards with no picture show who made it (#249)

OP OpenVibers committed 11 days ago · 5cb6de6948 · 3 files, +48 −1

Changes

ddiff --git a/public/css/video.css b/public/css/video.css
iindex 57343b4..f6a1fdf 100644
---- a/public/css/video.css
++++ b/public/css/video.css
@@@ -644,3 +644,10 @@ html.ov-motion .ovv-stat.is-live .ovv-dot { animation: ovv-pulse 1.6s ease-out i
/* A sign-in prompt: the sentence, then the button under it (phone sweep, 2026-11-01). */
.notice.signin-prompt { display: grid; gap: 12px; justify-items: start; }
.notice.signin-prompt p { margin: 0; }
++
++/* A card with no picture shows who made it: their face, round, over a blur of the same picture (server/video/pages.js
++ cardPlaceholder). */
++.ovv-card-who { position: relative; overflow: hidden; }
++.ovv-card-who .ovv-who-bg { position: absolute; inset: -12%; width: 124%; height: 124%; object-fit: cover; filter: blur(22px) saturate(1.2) brightness(.62); }
++.ovv-card-who .ovv-who-face { position: relative; width: 30%; max-width: 84px; aspect-ratio: 1; height: auto; border-radius: 50%; object-fit: cover;
++ box-shadow: 0 0 0 3px color-mix(in srgb, #fff 22%, transparent), 0 8px 24px rgba(0, 0, 0, .45); }
ddiff --git a/server/video/pages.js b/server/video/pages.js
iindex 1bc5c4e..d14e821 100644
---- a/server/video/pages.js
++++ b/server/video/pages.js
@@@ -514,11 +514,27 @@ function viewsLabel(n) {
return `${v.toLocaleString('en-US')} ${v === 1 ? 'view' : 'views'}`;
}
++/** Network's origin for faces (/avatar/:username answers the picture, or a neutral one): set by createVideoRoutes. */
++let faceBase = null;
++
++/**
++ * No picture: the maker's face over a blur of it (as Shared's kit.card and Live's cards show), when the channel is a
++ * username; else the kind's icon.
++ */
++function cardPlaceholder(o, icon) {
++ const name = String(o.channel || '');
++ if (faceBase && /^[A-Za-z0-9_]{1,32}$/.test(name)) {
++ const face = `${faceBase}/avatar/${name}?s=160`;
++ return html`<span class="ovv-card-placeholder ovv-card-who"><img class="ovv-who-bg" src="${face}" alt="" loading="lazy" decoding="async"><img class="ovv-who-face" src="${face}" alt="" loading="lazy" decoding="async"></span>`;
++ }
++ return html`<span class="ovv-card-placeholder"><i class="fa-solid ${icon}"></i></span>`;
++}
++
function card(o) {
const channel = o.channel ? html`<a href="${o.channelHref}">@${o.channel}</a>` : null;
const icon = o.live ? 'fa-tower-broadcast' : (o.kind === 'clip' ? 'fa-scissors' : 'fa-video');
return html`<article class="ovv-card">
--<a class="ovv-card-media" href="${o.href}" tabindex="-1" aria-hidden="true">${o.thumb ? thumbImg(o.thumb, CARD_SIZES) : html`<span class="ovv-card-placeholder"><i class="fa-solid ${icon}"></i></span>`}${o.duration ? html`<span class="ovv-dur">${o.duration}</span>` : ''}${o.badge ? html`<span class="ovv-tag${o.live ? ' ovv-tag--live' : ''}">${o.badge}</span>` : ''}</a>
++<a class="ovv-card-media" href="${o.href}" tabindex="-1" aria-hidden="true">${o.thumb ? thumbImg(o.thumb, CARD_SIZES) : cardPlaceholder(o, icon)}${o.duration ? html`<span class="ovv-dur">${o.duration}</span>` : ''}${o.badge ? html`<span class="ovv-tag${o.live ? ' ovv-tag--live' : ''}">${o.badge}</span>` : ''}</a>
<div class="ovv-card-body">
<h3 class="ovv-card-title"><a href="${o.href}">${o.title}</a></h3>
${meta([channel, o.extra || null, o.views ? viewsLabel(o.views) : null, o.date ? html`<time datetime="${o.iso}">${o.date}</time>` : null])}
@@@ -572,6 +588,7 @@ const grid = (cards, empty = 'Nothing here yet.') => (cards && cards.length ? ht
function createVideoRoutes(ctx) {
const { config, log, playlists } = ctx;
++ faceBase = /^https?:\/\//.test(String(config.networkUrl || '')) ? String(config.networkUrl).replace(/\/+$/, '') : null;
// A channel page lists that person's public playlists, which are stored by subject: the username is resolved
// through Network (identity.subject.resolve) and remembered for ten minutes (an unknown name for one), at most
ddiff --git a/test/card-face.test.js b/test/card-face.test.js
nnew file mode 100644
iindex 0000000..587409b
---- /dev/null
++++ b/test/card-face.test.js
@@@ -0,0 +1,23 @@
++'use strict';
++
++/**
++ * A recording or clip with no picture shows who made it (Shared's kit.card since 4.89.0, Live #546): the maker's face
++ * from Network's /avatar/:username over a blur of it, only for a real username, and the kind's icon otherwise.
++ */
++const assert = require('assert');
++const fs = require('fs');
++const path = require('path');
++
++const src = fs.readFileSync(path.join(__dirname, '..', 'server', 'video', 'pages.js'), 'utf8');
++const css = fs.readFileSync(path.join(__dirname, '..', 'public', 'css', 'video.css'), 'utf8');
++const fn = src.slice(src.indexOf('function cardPlaceholder('), src.indexOf('function card(o)'));
++
++assert.ok(fn.includes("/^[A-Za-z0-9_]{1,32}$/.test(name)"), 'only a username becomes an avatar address');
++assert.ok(fn.includes('${faceBase}/avatar/${name}?s=160'), 'Network\'s avatar, at the size it is drawn');
++assert.ok(fn.includes('class="ovv-who-bg"') && fn.includes('class="ovv-who-face"'), 'the face over a blur of it');
++assert.ok(fn.includes('<i class="fa-solid ${icon}"></i>'), 'the icon when there is no username');
++assert.ok(src.includes('${o.thumb ? thumbImg(o.thumb, CARD_SIZES) : cardPlaceholder(o, icon)}'), 'cards use it');
++assert.ok(/faceBase = \/\^https\?:\\\/\\\/\/\.test\(String\(config\.networkUrl/.test(src), 'the origin comes from the config, and only an http(s) one');
++for (const sel of ['.ovv-card-who .ovv-who-bg', '.ovv-card-who .ovv-who-face']) assert.ok(css.includes(sel), sel);
++
++console.log('card-face: all passed');