From 89314b1b6ed32ed39a9130fec346ebd279b7b84e Mon Sep 17 00:00:00 2001 From: khannurien Date: Tue, 11 Aug 2026 20:04:09 +0000 Subject: [PATCH] v3: journal now shows the generated still for video dumps, like the hot/new feeds already did MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The hot/new feeds go through FilePreview, which asks for GET /api/thumbnails/:dumpId unconditionally on a video mime and lets VideoThumb fall back to an icon if it 404s. The journal instead only recognised image files and dump.thumbnailMime — and that field maps to custom_thumbnail_mime, i.e. a thumbnail somebody uploaded by hand. Nothing on the Dump object advertises the ffmpeg still the route generates on demand, so hasThumbnail() returned false, mode degraded to text, and the card rendered a 🎬. hasThumbnail() and JournalCard's thumbnail resolution now both treat a video file as art, resolving to the same route. Since hasThumbnail() also gates grid footprints, video dumps can now claim feature/tall/wide slots — the mosaic has noticeably more image cards as a result, which is the point. ThumbnailPlaceholder gains `glyph` and `seed`: a host without ffmpeg gets a 404 and lands on the placeholder, where the mime emoji says more about the dump than an initial taken from its filename, and the hue is seeded from the filename since there's no hostname to hash. Co-Authored-By: Claude Opus 5 --- src/components/JournalCard.tsx | 29 ++++++++++++++++--------- src/components/ThumbnailPlaceholder.tsx | 10 ++++++--- src/utils/journalLayout.ts | 7 +++++- src/utils/thumbnailTint.ts | 11 +++++++--- 4 files changed, 40 insertions(+), 17 deletions(-) diff --git a/src/components/JournalCard.tsx b/src/components/JournalCard.tsx index 30daa10..8805210 100644 --- a/src/components/JournalCard.tsx +++ b/src/components/JournalCard.tsx @@ -41,12 +41,17 @@ export function JournalCard( navigate(dumpUrl(dump)); } - const thumbnailUrl = - dump.kind === "file" && dump.fileMime?.startsWith("image/") + // Mirrors FilePreview (the hot/new feeds) so a video shows its generated + // still here too, rather than degrading to a text card with a 🎬. + const thumbnailUrl = dump.thumbnailMime + ? dumpThumbnailUrl(dump, token) + : dump.kind === "file" + ? (dump.fileMime?.startsWith("image/") ? dumpFileUrl(dump, token) - : dump.thumbnailMime + : dump.fileMime?.startsWith("video/") ? dumpThumbnailUrl(dump, token) - : (dump.richContent?.thumbnailUrl ?? null); + : null) + : (dump.richContent?.thumbnailUrl ?? null); // Content mode is independent of grid footprint: a thumbnailed dump reads as // an image card, a thumbnail-less dump with a note becomes a pull-quote, and @@ -151,12 +156,16 @@ export function JournalCard(
{embedUrl && (
diff --git a/src/utils/journalLayout.ts b/src/utils/journalLayout.ts index 2fd239d..05a6914 100644 --- a/src/utils/journalLayout.ts +++ b/src/utils/journalLayout.ts @@ -17,7 +17,12 @@ export interface JournalEntry { /** A dump that can carry a real preview image (file image or rich thumbnail). */ export function hasThumbnail(dump: Dump): boolean { - if (dump.kind === "file" && dump.fileMime?.startsWith("image/")) return true; + if (dump.kind === "file") { + const mime = dump.fileMime ?? ""; + // Videos count: GET /api/thumbnails/:dumpId grabs a still with ffmpeg on + // first request and caches it, so there's art to show without any upload. + if (mime.startsWith("image/") || mime.startsWith("video/")) return true; + } if (dump.thumbnailMime) return true; return !!dump.richContent?.thumbnailUrl; } diff --git a/src/utils/thumbnailTint.ts b/src/utils/thumbnailTint.ts index 70d5932..d82ac76 100644 --- a/src/utils/thumbnailTint.ts +++ b/src/utils/thumbnailTint.ts @@ -43,13 +43,18 @@ function hostnameOf(url: string | undefined): string | undefined { /** * The page's own brand color when it declared a usable one, otherwise a hue * derived from its hostname — so every card gets a tint, including rows saved - * before accent extraction existed. + * before accent extraction existed. File dumps have no hostname to hash and + * pass `seed` (their filename) instead. */ export function tintFor( - { accentColor, url }: { accentColor?: string; url?: string }, + { accentColor, url, seed }: { + accentColor?: string; + url?: string; + seed?: string; + }, ): string { if (isSafeHex(accentColor)) return accentColor; - return hueFromString(hostnameOf(url) ?? url ?? ""); + return hueFromString(seed ?? hostnameOf(url) ?? url ?? ""); } /** The letter drawn when there's no favicon to show. */