From 9b249a86621998bc079346be58f73c4a32fb9176 Mon Sep 17 00:00:00 2001 From: SnapOtter Date: Sat, 16 May 2026 15:00:32 +0800 Subject: [PATCH] fix: fetch library thumbnails with auth headers to show image previews The tag cannot send Authorization headers, so thumbnail requests returned 401 and browsers displayed the alt text (filename) instead of the image. Replaced with AuthImage component that fetches via fetch() with proper auth headers and renders blob URLs. --- .../components/common/file-library-modal.tsx | 51 +++++++++++++++++-- .../web/src/components/files/file-details.tsx | 50 +++++++++++++++++- 2 files changed, 96 insertions(+), 5 deletions(-) diff --git a/apps/web/src/components/common/file-library-modal.tsx b/apps/web/src/components/common/file-library-modal.tsx index e68fa680..a951cdd6 100644 --- a/apps/web/src/components/common/file-library-modal.tsx +++ b/apps/web/src/components/common/file-library-modal.tsx @@ -1,4 +1,4 @@ -import { Check, FolderOpen, Loader2, Search, X } from "lucide-react"; +import { Check, FolderOpen, ImageIcon, Loader2, Search, X } from "lucide-react"; import { useCallback, useEffect, useRef, useState } from "react"; import { apiListFiles, @@ -9,6 +9,52 @@ import { } from "@/lib/api"; import { cn } from "@/lib/utils"; +function AuthImage({ src, alt, className }: { src: string; alt: string; className?: string }) { + const [blobUrl, setBlobUrl] = useState(null); + const [failed, setFailed] = useState(false); + + useEffect(() => { + let revoked = false; + fetch(src, { headers: formatHeaders() }) + .then((res) => { + if (!res.ok) throw new Error(); + return res.blob(); + }) + .then((blob) => { + if (revoked) return; + setBlobUrl(URL.createObjectURL(blob)); + }) + .catch(() => { + if (!revoked) setFailed(true); + }); + return () => { + revoked = true; + setBlobUrl((prev) => { + if (prev) URL.revokeObjectURL(prev); + return null; + }); + }; + }, [src]); + + if (failed) { + return ( +
+ +
+ ); + } + + if (!blobUrl) { + return ( +
+
+
+ ); + } + + return {alt}; +} + interface FileLibraryModalProps { open: boolean; onClose: () => void; @@ -170,11 +216,10 @@ export function FileLibraryModal({ open, onClose, onImport }: FileLibraryModalPr : "border-border hover:border-primary/50", )} > - {file.originalName} {checked && (
diff --git a/apps/web/src/components/files/file-details.tsx b/apps/web/src/components/files/file-details.tsx index cd4d6dd6..0cd50fd3 100644 --- a/apps/web/src/components/files/file-details.tsx +++ b/apps/web/src/components/files/file-details.tsx @@ -1,5 +1,5 @@ import { TOOLS } from "@snapotter/shared"; -import { FileImage, Workflow } from "lucide-react"; +import { FileImage, ImageIcon, Workflow } from "lucide-react"; import { useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; import { @@ -13,6 +13,52 @@ import { cn } from "@/lib/utils"; import { useFileStore } from "@/stores/file-store"; import { useFilesPageStore } from "@/stores/files-page-store"; +function AuthImage({ src, alt, className }: { src: string; alt: string; className?: string }) { + const [blobUrl, setBlobUrl] = useState(null); + const [failed, setFailed] = useState(false); + + useEffect(() => { + let revoked = false; + fetch(src, { headers: formatHeaders() }) + .then((res) => { + if (!res.ok) throw new Error(); + return res.blob(); + }) + .then((blob) => { + if (revoked) return; + setBlobUrl(URL.createObjectURL(blob)); + }) + .catch(() => { + if (!revoked) setFailed(true); + }); + return () => { + revoked = true; + setBlobUrl((prev) => { + if (prev) URL.revokeObjectURL(prev); + return null; + }); + }; + }, [src]); + + if (failed) { + return ( +
+ +
+ ); + } + + if (!blobUrl) { + return ( +
+
+
+ ); + } + + return {alt}; +} + function toolName(toolId: string): string { return TOOLS.find((t) => t.id === toolId)?.name ?? toolId; } @@ -130,7 +176,7 @@ export function FileDetails({ mobile = false }: FileDetailsProps) { > {/* Thumbnail */}
-