From e7979230cdddde45113a7bf282f00b4192d2ab0a Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Thu, 22 Oct 2026 10:07:34 -0700 Subject: Videos grid, "All shapes": justified rows (each tile its clip's own proportions, every row one height filling the width) instead of squares Co-Authored-By: Claude Opus 5.5 --- src/features/messages.js | 17 ++++++++++++----- src/style.css | 10 ++++++++-- 2 files changed, 20 insertions(+), 7 deletions(-) (limited to 'src') diff --git a/src/features/messages.js b/src/features/messages.js index 350ad7e..b209e35 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -13917,12 +13917,16 @@ export function messagesFeature(ctx) { const clipsAll = () => { try { return localStorage.getItem(CLIPS_FIT_KEY) === 'all'; } catch { return false; } }; const setClipsAll = (all) => { try { if (all) localStorage.setItem(CLIPS_FIT_KEY, 'all'); else localStorage.removeItem(CLIPS_FIT_KEY); } catch {} }; const screenTall = () => window.innerHeight >= window.innerWidth; - const clipShape = (url) => { + const clipSize = (url) => { const d = videoInfo.get(url) || {}; const k = d.w && d.h ? d : videoDims.get(url) || mediaReady.get(url); const w = k && (k.w || k.width), hh = k && (k.h || k.height); - return w && hh ? (hh > w ? 'tall' : hh < w ? 'wide' : 'square') : null; + return w && hh ? { w, h: hh } : null; }; + const clipShape = (url) => { const z = clipSize(url); return z ? (z.h > z.w ? 'tall' : z.h < z.w ? 'wide' : 'square') : null; }; + // width over height, kept between a phone's upright 9:16 and a cinema 2:1 + // (a stray 32:9 or 1:4 would make a tile a sliver); unknown: a square + const clipRatio = (url) => { const z = clipSize(url); return z ? Math.min(2, Math.max(0.5, z.w / z.h)) : 1; }; // true/false, or null while the clip's shape isn't known const clipFits = (url) => { if (clipsAll()) return true; const sh = clipShape(url); return sh === null ? null : sh === 'square' || sh === (screenTall() ? 'tall' : 'wide'); }; const clipsLabel = () => t(clipsAll() ? 'clipsAll' : screenTall() ? 'clipsTall' : 'clipsWide'); @@ -14207,9 +14211,11 @@ export function messagesFeature(ctx) { if (mediaMode === 'videos') for (const it of items.slice(0, 24)) preconnect(playSrc(it.url)); // The tiles take the clips' shape (Adam, 2026-10-22): wide clips only (a // wide screen) go one to a row, their posters whole, not slivers cut to - // portrait tiles; every shape ("All shapes") goes three across in - // squares; tall ones only (a phone held upright) keep the tall tiles. - const shape = mediaMode !== 'videos' ? '' : clipsAll() ? ' square' : screenTall() ? '' : ' wide'; + // portrait tiles; every shape ("All shapes") is laid out in justified + // rows, each tile its clip's own proportions and each row as tall as it + // takes to fill the width; tall ones only (a phone held upright) keep + // the tall tiles three across. + const shape = mediaMode !== 'videos' ? '' : clipsAll() ? ' justified' : screenTall() ? '' : ' wide'; return h('div', { class: 'media-grid' + shape, 'data-key': 'media-grid:' + mediaMode }, ...items.map((it) => { const k = mediaKey(it); @@ -14223,6 +14229,7 @@ export function messagesFeature(ctx) { const fromServer = mediaMode === 'videos' && poster && poster === serverThumb(it.url); return h('button', { class: 'media-tile' + (mediaMode === 'videos' ? ' video' : ''), type: 'button', 'data-key': 'tile:' + k, + style: shape === ' justified' ? '--ar:' + clipRatio(it.url).toFixed(3) : undefined, 'aria-label': displayName(it.ev.pubkey), onClick: () => openMediaPager(mediaMode, k), onPointerDown: mediaMode === 'videos' ? () => earlyPlayer(it.url) : undefined, }, diff --git a/src/style.css b/src/style.css index 0403a88..3dcbf17 100644 --- a/src/style.css +++ b/src/style.css @@ -1541,8 +1541,14 @@ body::after { /* only wide clips in it: one to a row, the poster whole */ .media-grid.wide { grid-template-columns: 1fr; gap: 6px; } .media-grid.wide .media-tile.video { aspect-ratio: 16 / 9; } -/* every shape in it: squares, none of them favoured */ -.media-grid.square .media-tile.video { aspect-ratio: 1; } +/* every shape in it: justified rows. A tile starts at its proportions at + the row height and grows in proportion to them, so a row's tiles share one + height and fill the width; the filler takes the last row's slack instead + of stretching its few tiles. */ +.media-grid.justified { display: flex; flex-wrap: wrap; --row-h: 140px; } +.media-grid.justified .media-tile.video { aspect-ratio: var(--ar); flex: var(--ar) 1 calc(var(--ar) * var(--row-h)); min-width: 0; } +.media-grid.justified::after { content: ''; flex: 1000 1 0; } +@media (max-width: 600px) { .media-grid.justified { --row-h: 100px; } } .media-tile img, .media-tile video { width: 100%; height: 100%; object-fit: cover; display: block; pointer-events: none; } .media-tile.broken { display: none; } .media-tile-play { -- cgit v1.2.3