All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 46s
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>
71 lines
2.3 KiB
TypeScript
71 lines
2.3 KiB
TypeScript
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>
|
||
);
|
||
}
|