Files
gerbeur/src/components/ThumbnailPlaceholder.tsx
khannurien 89314b1b6e
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 46s
v3: journal now shows the generated still for video dumps, like the hot/new feeds already did
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 <noreply@anthropic.com>
2026-08-11 20:04:09 +00:00

71 lines
2.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState } from "react";
import { proxyImageUrl } from "../utils/proxyImage.ts";
import { initialsFor, tintFor } from "../utils/thumbnailTint.ts";
interface ThumbnailPlaceholderProps {
/** The dumped URL — seeds the fallback hue and the initial. */
url?: string;
/** Explicit hue seed for dumps with no URL to hash (file dumps). */
seed?: string;
/** The target page's declared brand color, if it had one. */
accentColor?: string;
/** The target page's icon, drawn contained over the tint. */
faviconUrl?: string;
/** Preferred source for the initial when there's no favicon. */
siteName?: string;
/** Emoji shown instead of an initial — file dumps say more with 🎬 than "T". */
glyph?: string;
className?: string;
}
/**
* Stand-in artwork for pages that offer no preview image: the site's own color
* washed over the theme surface, with its favicon centered on top — or its
* initial when the favicon is missing or fails to load.
*
* Fills whatever box it's placed in, so the same component serves the 36px
* compact thumbnail, the 128×72 feed preview and a 2×2 mosaic tile.
*/
export default function ThumbnailPlaceholder(
{ url, seed, accentColor, faviconUrl, siteName, glyph, className }:
ThumbnailPlaceholderProps,
) {
const [iconFailed, setIconFailed] = useState(false);
// A different icon is a fresh thing to try — clear the prior failure during
// render rather than in an effect (same shape as Avatar.tsx).
const [prevIcon, setPrevIcon] = useState(faviconUrl);
if (prevIcon !== faviconUrl) {
setPrevIcon(faviconUrl);
setIconFailed(false);
}
const style = { "--thumb-tint": tintFor({ accentColor, url, seed }) } as
React.CSSProperties;
return (
<div
className={`thumb-placeholder${className ? ` ${className}` : ""}`}
style={style}
aria-hidden="true"
>
{faviconUrl && !iconFailed
? (
<img
src={proxyImageUrl(faviconUrl)}
alt=""
loading="lazy"
referrerPolicy="no-referrer"
className="thumb-placeholder-icon"
onError={() => setIconFailed(true)}
/>
)
: (
<span className="thumb-placeholder-initials">
{glyph ?? initialsFor(siteName, url)}
</span>
)}
</div>
);
}