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/components/ThumbnailPlaceholder.tsx b/src/components/ThumbnailPlaceholder.tsx
index 3b0cc58..7b0c41b 100644
--- a/src/components/ThumbnailPlaceholder.tsx
+++ b/src/components/ThumbnailPlaceholder.tsx
@@ -5,12 +5,16 @@ 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;
}
@@ -23,7 +27,7 @@ interface ThumbnailPlaceholderProps {
* compact thumbnail, the 128×72 feed preview and a 2×2 mosaic tile.
*/
export default function ThumbnailPlaceholder(
- { url, accentColor, faviconUrl, siteName, className }:
+ { url, seed, accentColor, faviconUrl, siteName, glyph, className }:
ThumbnailPlaceholderProps,
) {
const [iconFailed, setIconFailed] = useState(false);
@@ -36,7 +40,7 @@ export default function ThumbnailPlaceholder(
setIconFailed(false);
}
- const style = { "--thumb-tint": tintFor({ accentColor, url }) } as
+ const style = { "--thumb-tint": tintFor({ accentColor, url, seed }) } as
React.CSSProperties;
return (
@@ -58,7 +62,7 @@ export default function ThumbnailPlaceholder(
)
: (
- {initialsFor(siteName, url)}
+ {glyph ?? initialsFor(siteName, url)}
)}
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. */