fix: fetch library thumbnails with auth headers to show image previews

The <img src> 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.
This commit is contained in:
SnapOtter
2026-05-16 15:00:32 +08:00
parent 2fc5e15fff
commit 9b249a8662
2 changed files with 96 additions and 5 deletions
@@ -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<string | null>(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 (
<div className={cn("flex items-center justify-center bg-muted/50", className)}>
<ImageIcon className="h-8 w-8 text-muted-foreground/50" />
</div>
);
}
if (!blobUrl) {
return (
<div className={cn("flex items-center justify-center bg-muted/30", className)}>
<div className="h-4 w-4 border-2 border-muted-foreground/30 border-t-transparent rounded-full animate-spin" />
</div>
);
}
return <img src={blobUrl} alt={alt} className={className} loading="lazy" />;
}
interface FileLibraryModalProps {
open: boolean;
onClose: () => void;
@@ -170,11 +216,10 @@ export function FileLibraryModal({ open, onClose, onImport }: FileLibraryModalPr
: "border-border hover:border-primary/50",
)}
>
<img
<AuthImage
src={getFileThumbnailUrl(file.id)}
alt={file.originalName}
className="w-full h-full object-cover"
loading="lazy"
/>
{checked && (
<div className="absolute top-1 right-1 w-5 h-5 rounded-full bg-primary text-primary-foreground flex items-center justify-center">
+48 -2
View File
@@ -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<string | null>(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 (
<div className={cn("flex items-center justify-center bg-muted/50", className)}>
<ImageIcon className="h-8 w-8 text-muted-foreground/50" />
</div>
);
}
if (!blobUrl) {
return (
<div className={cn("flex items-center justify-center bg-muted/30", className)}>
<div className="h-4 w-4 border-2 border-muted-foreground/30 border-t-transparent rounded-full animate-spin" />
</div>
);
}
return <img src={blobUrl} alt={alt} className={className} />;
}
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 */}
<div className={cn("border-b border-border", mobile ? "" : "pb-4")}>
<img
<AuthImage
src={getFileThumbnailUrl(details.id)}
alt={details.originalName}
className="w-full rounded-lg object-contain max-h-48 bg-muted"