openvibers/openvibe.live Commits

Card thumbnails arrive over their own colour (L52 step 6) (#564)

OP OpenVibers committed 11 days ago · 5201ae9b23 · 3 files, +24 −3

Media #290 names a new VOD or clip thumbnail with its average colour (<kind>-<id>-<time>-<rrggbb>.jpg). ovThumbColour
reads it (only after the time, so a stream's stable stream-<app>-<id>.jpg never reads its id as a colour), and the
cards paint it behind the picture: thumbImg (home and channel cards) and the content feed's thumb. A slow image now
fills in over its own colour instead of a grey hole; an older name has none and none is invented.

Changes

ddiff --git a/public/js/app.js b/public/js/app.js
iindex ff4289cd..4c6effa2 100644
---- a/public/js/app.js
++++ b/public/js/app.js
@@@ -75,6 +75,14 @@ function ovImgW(url, w) {
}
const OV_IMG_RETRY = 'if(this.dataset.full&&this.src!==this.dataset.full){this.src=this.dataset.full;return}';
++/** The colour a Media thumbnail's name carries (Media #290: <kind>-<id>-<time>-<rrggbb>.jpg), as '#rrggbb', or ''.
++ A card paints it behind the picture, so a slow image arrives over its own colour, not a grey hole (plan L52). */
++function ovThumbColour(url) {
++ // After the time (10+ digits), so a stream's stable stream-<app>-<id>.jpg never reads its id as a colour.
++ const m = /-\d{10,}-([0-9a-f]{6})\.(?:jpe?g|webp|avif|png)(?:[?#].*)?$/i.exec(String(url || ''));
++ return m ? `#${m[1].toLowerCase()}` : '';
++}
++
/** Inner img-or-letter for an element that is itself the avatar circle. */
function _avatarInner(url, name) {
const letter = ((String(name || '?'))[0] || '?').toUpperCase();
@@@ -2136,7 +2144,9 @@ function thumbImg(thumbnailUrl, fallbackIcon, alt, regenerateUrl = null) {
const sized = /^https:\/\/openvibe\.media\/t\/[^?#\s]+$/.test(thumbnailUrl);
const src = sized ? `${thumbnailUrl}?w=480&fmt=webp` : thumbnailUrl;
const set = sized ? ` srcset="${esc([320, 480, 640].map((w) => `${thumbnailUrl}?w=${w}&fmt=webp ${w}w`).join(', '))}" sizes="(max-width: 600px) 100vw, 320px"` : '';
-- return `<img src="${esc(src)}"${set} alt="${esc(alt || '')}" loading="lazy" data-regenerate-url="${esc(regenerateUrl || '')}" onerror="handleThumbnailError(this)">
++ const colour = ovThumbColour(thumbnailUrl);
++ const bg = colour ? ` style="background-color:${colour}"` : '';
++ return `<img src="${esc(src)}"${set}${bg} alt="${esc(alt || '')}" loading="lazy" data-regenerate-url="${esc(regenerateUrl || '')}" onerror="handleThumbnailError(this)">
${iconHtml(' style="display:none"')}`;
}
if (regenerateUrl) {
ddiff --git a/public/js/content-feed.js b/public/js/content-feed.js
iindex b8f1b320..2e150d76 100644
---- a/public/js/content-feed.js
++++ b/public/js/content-feed.js
@@@ -120,6 +120,9 @@
// A picture paste is its original upload (one is 2460 px, drawn at 390): Media's 640 px copy, the original once if it
// fails, then the placeholder.
const img = el('img', { class: 'cf-thumb', src: typeof ovImgW === 'function' ? ovImgW(url, 640) : url, alt: '', loading: 'lazy', decoding: 'async' });
++ // The picture's own colour first (Media names a thumbnail with it), so it fills in over that colour.
++ const colour = typeof ovThumbColour === 'function' ? ovThumbColour(url) : '';
++ if (colour) img.style.backgroundColor = colour;
img.addEventListener('error', () => {
if (img.src !== url && !img.dataset.retried) { img.dataset.retried = '1'; img.src = url; return; }
img.replaceWith(placeholder(fallbackIcon, who));
ddiff --git a/test/thumb-srcset.test.js b/test/thumb-srcset.test.js
iindex b89114ac..78830fca 100644
---- a/test/thumb-srcset.test.js
++++ b/test/thumb-srcset.test.js
@@@ -1,7 +1,8 @@
'use strict';
// Card thumbnails (public/js/app.js thumbImg): an openvibe.media thumbnail loads a WebP near the card's width through
// Media's /t/:id?w=&fmt= variants (a 480 px src and a 320/480/640 srcset); any other URL is used as it is; and a
--// regenerated thumbnail drops the srcset, so the browser shows the new picture rather than the failed variants.
++// regenerated thumbnail drops the srcset, so the browser shows the new picture rather than the failed variants. A
++// thumbnail whose name carries its colour (Media #290) is drawn over that colour until the picture arrives.
const assert = require('assert');
const fs = require('fs');
const path = require('path');
@@@ -10,7 +11,7 @@ const vm = require('vm');
const js = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'app.js'), 'utf8');
const between = (startMark, endMark) => { const a = js.indexOf(startMark); const b = js.indexOf(endMark, a); assert.ok(a >= 0 && b > a, startMark); return js.slice(a, b); };
const ctx = vm.createContext({});
--vm.runInContext(`${between('function esc(str) {', '\n}\n')}\n}\n${between('function thumbImg(', '\nfunction formatDuration(')}\nthis.thumbImg = thumbImg;`, ctx);
++vm.runInContext(`${between('function esc(str) {', '\n}\n')}\n}\n${between('function ovThumbColour(', '\n}\n')}\n}\n${between('function thumbImg(', '\nfunction formatDuration(')}\nthis.thumbImg = thumbImg; this.ovThumbColour = ovThumbColour;`, ctx);
const media = ctx.thumbImg('https://openvibe.media/t/vod-1-2.jpg', 'fa-video', 'A stream', '/api/thumbnails/generate/vod/1');
assert.ok(media.includes('src="https://openvibe.media/t/vod-1-2.jpg?w=480&amp;fmt=webp"'), media);
@@@ -23,5 +24,12 @@ assert.ok(other.includes('src="https://example.org/x.jpg"') && !other.includes('
const queried = ctx.thumbImg('https://openvibe.media/t/vod-1-2.jpg?t=5', 'fa-video', 'x');
assert.ok(!queried.includes('srcset'), 'a URL that already has a query is used as it is');
++const coloured = ctx.thumbImg('https://openvibe.media/t/vod-1-1730000000000-3566ce.jpg', 'fa-video', 'x');
++assert.ok(coloured.includes('style="background-color:#3566ce"'), 'its own colour behind the picture');
++assert.ok(!media.includes('background-color'), 'an older name has no colour and none is invented');
++assert.strictEqual(ctx.ovThumbColour('https://openvibe.media/t/clip-9-1730000000000-ABCDEF.jpg?w=480&fmt=webp'), '#abcdef');
++assert.strictEqual(ctx.ovThumbColour('https://openvibe.media/t/stream-live-100234.jpg'), '', 'a stream\'s stable name: its id is not a colour');
++assert.strictEqual(ctx.ovThumbColour('https://openvibe.media/t/vod-1-2.jpg'), '');
++
assert.match(js, /img\.removeAttribute\('srcset'\);[^\n]*\n\s*img\.src = `\$\{data\.thumbnail_url\}/, 'a regenerated thumbnail drops the srcset before its new src');
console.log('thumb-srcset: all passed');