openvibers/openvibe.live Commits
Stream cards wait in their streamer's colour, not a grey box (#501)
Changes
@@@ -811,7 +811,8 @@ body.ov-nav-overlay .navbar.ovnav-top {}.stream-card-thumb {aspect-ratio: 16/9;-- background: var(--bg-tertiary);++ /* Until the picture arrives, its streamer's colour (app-home.js _thumbTint), not a grey box. */++ background: radial-gradient(120% 95% at 28% 18%, color-mix(in srgb, var(--thumb-tint, var(--bg-tertiary)) 58%, var(--bg-tertiary)), var(--bg-tertiary));position: relative;display: flex; align-items: center; justify-content: center;overflow: hidden;@@@ -1745,6 +1745,18 @@ async function loadHomePastes(opts) {// Markup for a single stream card (live or recent). Extracted so the home page// can reconcile the live grid in place for real-time updates.++/**++ * The colour a card's picture sits on until it arrives (plan L52: "a fake thumbnail… so it is not jarring"): the++ * streamer's own profile colour, or one drawn from their name, so a grid fills in colour rather than grey boxes.++ */++function _thumbTint(s) {++ if (s && /^#[0-9a-f]{6}$/i.test(String(s.profile_color || ''))) return s.profile_color;++ const name = String((s && s.username) || '');++ let h = 0;++ for (let i = 0; i < name.length; i++) h = (h * 31 + name.charCodeAt(i)) % 360;++ return `hsl(${h} 45% 34%)`;++}++function streamCardHTML(s, isLive) {let navUrl;if (isLive && s.id) {@@@ -1760,7 +1772,7 @@ function streamCardHTML(s, isLive) {const endedAgo = !isLive && s.ended_at ? `<span class="stream-card-ago">${timeAgo(s.ended_at)}</span>` : '';return `<a class="stream-card" data-stream-id="${esc(String(s.id || ''))}" href="${esc(navUrl)}" onclick="return handleLinkClick(event, '${esc(navUrl)}')">-- <div class="stream-card-thumb${s.is_nsfw ? ' stream-card-nsfw-blur' : ''}">++ <div class="stream-card-thumb${s.is_nsfw ? ' stream-card-nsfw-blur' : ''}" style="--thumb-tint:${esc(_thumbTint(s))}">${thumbImg(thumb, 'fa-circle-nodes', s.title, !isLive && s.vod_id ? `/api/thumbnails/generate/vod/${s.vod_id}` : null)}${streamTypeBadge(s.browser_mode, s.streaming_method)}${s.is_nsfw ? '<span class="stream-card-nsfw">18+</span>' : ''}nnew file mode 100644@@@ -0,0 +1,22 @@++'use strict';++// A card's picture sits on its streamer's colour until it arrives (plan L52; the owner, 11-01: "placeholder elements++// while it loads, a fake thumbnail… so it is not jarring"). The function is public/js/app-home.js's own, run here.++// node test/thumb-tint.test.js++const assert = require('assert');++const fs = require('fs');++const path = require('path');++++const src = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'app-home.js'), 'utf8');++const body = /function _thumbTint\(s\) \{[\s\S]*?\n\}\n/.exec(src);++assert.ok(body, 'app-home.js has _thumbTint');++const _thumbTint = new Function(`${body[0]}; return _thumbTint;`)();++++assert.strictEqual(_thumbTint({ profile_color: '#3366ff', username: 'ana' }), '#3366ff', 'the streamer\'s own colour');++assert.match(_thumbTint({ profile_color: 'red;background:url(x)', username: 'ana' }), /^hsl\(\d+ 45% 34%\)$/, 'anything but a plain colour is not used');++assert.strictEqual(_thumbTint({ username: 'ana' }), _thumbTint({ username: 'ana' }), 'the same name, the same colour');++assert.notStrictEqual(_thumbTint({ username: 'ana' }), _thumbTint({ username: 'kai' }), 'another name, another colour');++assert.match(_thumbTint({}), /^hsl\(0 45% 34%\)$/);++assert.match(src, /<div class="stream-card-thumb\$\{[^}]+\}" style="--thumb-tint:\$\{esc\(_thumbTint\(s\)\)\}">/, 'the card wears it');++const css = fs.readFileSync(path.join(__dirname, '..', 'public', 'css', 'style.css'), 'utf8');++assert.match(css, /\.stream-card-thumb \{\s*aspect-ratio: 16\/9;[\s\S]{0,200}var\(--thumb-tint/, 'the box is drawn in it');++console.log('thumb tint: all checks passed');
@@ -811,7 +811,8 @@ body.ov-nav-overlay .navbar.ovnav-top {
@@ -811,7 +811,8 @@ body.ov-nav-overlay .navbar.ovnav-top {
}
}
.stream-card-thumb {
.stream-card-thumb {
aspect-ratio: 16/9;
aspect-ratio: 16/9;
- background: var(--bg-tertiary);
+ /* Until the picture arrives, its streamer's colour (app-home.js _thumbTint), not a grey box. */
+ background: radial-gradient(120% 95% at 28% 18%, color-mix(in srgb, var(--thumb-tint, var(--bg-tertiary)) 58%, var(--bg-tertiary)), var(--bg-tertiary));
position: relative;
position: relative;
display: flex; align-items: center; justify-content: center;
display: flex; align-items: center; justify-content: center;
overflow: hidden;
overflow: hidden;
@@ -1745,6 +1745,18 @@ async function loadHomePastes(opts) {
@@ -1745,6 +1745,18 @@ async function loadHomePastes(opts) {
// Markup for a single stream card (live or recent). Extracted so the home page
// Markup for a single stream card (live or recent). Extracted so the home page
// can reconcile the live grid in place for real-time updates.
// can reconcile the live grid in place for real-time updates.
+/**
+ * The colour a card's picture sits on until it arrives (plan L52: "a fake thumbnail… so it is not jarring"): the
+ * streamer's own profile colour, or one drawn from their name, so a grid fills in colour rather than grey boxes.
+ */
+function _thumbTint(s) {
+ if (s && /^#[0-9a-f]{6}$/i.test(String(s.profile_color || ''))) return s.profile_color;
+ const name = String((s && s.username) || '');
+ let h = 0;
+ for (let i = 0; i < name.length; i++) h = (h * 31 + name.charCodeAt(i)) % 360;
+ return `hsl(${h} 45% 34%)`;
+}
+
function streamCardHTML(s, isLive) {
function streamCardHTML(s, isLive) {
let navUrl;
let navUrl;
if (isLive && s.id) {
if (isLive && s.id) {
@@ -1760,7 +1772,7 @@ function streamCardHTML(s, isLive) {
@@ -1760,7 +1772,7 @@ function streamCardHTML(s, isLive) {
const endedAgo = !isLive && s.ended_at ? `<span class="stream-card-ago">${timeAgo(s.ended_at)}</span>` : '';
const endedAgo = !isLive && s.ended_at ? `<span class="stream-card-ago">${timeAgo(s.ended_at)}</span>` : '';
return `
return `
<a class="stream-card" data-stream-id="${esc(String(s.id || ''))}" href="${esc(navUrl)}" onclick="return handleLinkClick(event, '${esc(navUrl)}')">
<a class="stream-card" data-stream-id="${esc(String(s.id || ''))}" href="${esc(navUrl)}" onclick="return handleLinkClick(event, '${esc(navUrl)}')">
- <div class="stream-card-thumb${s.is_nsfw ? ' stream-card-nsfw-blur' : ''}">
+ <div class="stream-card-thumb${s.is_nsfw ? ' stream-card-nsfw-blur' : ''}" style="--thumb-tint:${esc(_thumbTint(s))}">
${thumbImg(thumb, 'fa-circle-nodes', s.title, !isLive && s.vod_id ? `/api/thumbnails/generate/vod/${s.vod_id}` : null)}
${thumbImg(thumb, 'fa-circle-nodes', s.title, !isLive && s.vod_id ? `/api/thumbnails/generate/vod/${s.vod_id}` : null)}
${streamTypeBadge(s.browser_mode, s.streaming_method)}
${streamTypeBadge(s.browser_mode, s.streaming_method)}
${s.is_nsfw ? '<span class="stream-card-nsfw">18+</span>' : ''}
${s.is_nsfw ? '<span class="stream-card-nsfw">18+</span>' : ''}
new file mode 100644
new file mode 100644
@@ -0,0 +1,22 @@
@@ -0,0 +1,22 @@
+'use strict';
+// A card's picture sits on its streamer's colour until it arrives (plan L52; the owner, 11-01: "placeholder elements
+// while it loads, a fake thumbnail… so it is not jarring"). The function is public/js/app-home.js's own, run here.
+// node test/thumb-tint.test.js
+const assert = require('assert');
+const fs = require('fs');
+const path = require('path');
+
+const src = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'app-home.js'), 'utf8');
+const body = /function _thumbTint\(s\) \{[\s\S]*?\n\}\n/.exec(src);
+assert.ok(body, 'app-home.js has _thumbTint');
+const _thumbTint = new Function(`${body[0]}; return _thumbTint;`)();
+
+assert.strictEqual(_thumbTint({ profile_color: '#3366ff', username: 'ana' }), '#3366ff', 'the streamer\'s own colour');
+assert.match(_thumbTint({ profile_color: 'red;background:url(x)', username: 'ana' }), /^hsl\(\d+ 45% 34%\)$/, 'anything but a plain colour is not used');
+assert.strictEqual(_thumbTint({ username: 'ana' }), _thumbTint({ username: 'ana' }), 'the same name, the same colour');
+assert.notStrictEqual(_thumbTint({ username: 'ana' }), _thumbTint({ username: 'kai' }), 'another name, another colour');
+assert.match(_thumbTint({}), /^hsl\(0 45% 34%\)$/);
+assert.match(src, /<div class="stream-card-thumb\$\{[^}]+\}" style="--thumb-tint:\$\{esc\(_thumbTint\(s\)\)\}">/, 'the card wears it');
+const css = fs.readFileSync(path.join(__dirname, '..', 'public', 'css', 'style.css'), 'utf8');
+assert.match(css, /\.stream-card-thumb \{\s*aspect-ratio: 16\/9;[\s\S]{0,200}var\(--thumb-tint/, 'the box is drawn in it');
+console.log('thumb tint: all checks passed');