mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
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:
@@ -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">
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user