import { TOOLS } from "@snapotter/shared"; import { FileImage, FileText, ImageIcon, Music, Play, Video } from "lucide-react"; import { lazy, Suspense, useCallback, useEffect, useState } from "react"; import { useLocation, useNavigate } from "react-router-dom"; import { useTranslation } from "@/contexts/i18n-context"; import { apiGetFileDetails, formatHeaders, getFileDownloadUrl, getFilePreviewUrl, getFileThumbnailUrl, type UserFileDetail, } from "@/lib/api"; import { cn } from "@/lib/utils"; import { useFileStore } from "@/stores/file-store"; import { useFilesPageStore } from "@/stores/files-page-store"; const WaveformPlayer = lazy(() => import("@/components/common/waveform-player").then((m) => ({ default: m.WaveformPlayer })), ); 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}; } const NATIVE_VIDEO = new Set(["mp4", "webm", "ogg", "ogv", "m4v"]); const NATIVE_AUDIO = new Set(["mp3", "wav", "ogg", "oga", "opus", "aac", "m4a", "flac", "webm"]); const OFFICE_MIMES = new Set([ "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "application/vnd.openxmlformats-officedocument.presentationml.presentation", "application/vnd.oasis.opendocument.text", "application/vnd.oasis.opendocument.spreadsheet", "application/vnd.oasis.opendocument.presentation", "application/msword", "application/vnd.ms-excel", "application/vnd.ms-powerpoint", ]); function isNativePlayable(mimeType: string, filename: string): boolean { const ext = filename.split(".").pop()?.toLowerCase() ?? ""; if (mimeType.startsWith("video/")) return NATIVE_VIDEO.has(ext); if (mimeType.startsWith("audio/")) return NATIVE_AUDIO.has(ext); return false; } function FilePreview({ fileId, mimeType, name, }: { fileId: string; mimeType: string; name: string; }) { const [mediaSrc, setMediaSrc] = useState(null); const [previewSrc, setPreviewSrc] = useState(null); const [previewLoading, setPreviewLoading] = useState(false); const [previewError, setPreviewError] = useState(false); const isMedia = mimeType.startsWith("video/") || mimeType.startsWith("audio/"); const nativePlayable = isMedia && isNativePlayable(mimeType, name); const isPdf = mimeType === "application/pdf"; const isOfficeDoc = OFFICE_MIMES.has(mimeType); // Fetch native-playable media or PDF directly from the download endpoint. // Also resets server-side preview state when the file changes. useEffect(() => { // Reset server-side preview state on every file change setPreviewSrc((prev) => { if (prev) URL.revokeObjectURL(prev); return null; }); setPreviewLoading(false); setPreviewError(false); if (isPdf) { let revoked = false; fetch(getFileDownloadUrl(fileId), { headers: formatHeaders() }) .then((res) => res.blob()) .then((blob) => { if (!revoked) setMediaSrc(URL.createObjectURL(blob)); }) .catch(() => {}); return () => { revoked = true; setMediaSrc((prev) => { if (prev) URL.revokeObjectURL(prev); return null; }); }; } if (!isMedia || !nativePlayable) return; let revoked = false; const url = getFileDownloadUrl(fileId); fetch(url, { headers: formatHeaders() }) .then((res) => res.blob()) .then((blob) => { if (!revoked) setMediaSrc(URL.createObjectURL(blob)); }) .catch(() => {}); return () => { revoked = true; setMediaSrc((prev) => { if (prev) URL.revokeObjectURL(prev); return null; }); }; }, [isMedia, nativePlayable, isPdf, fileId]); const handleGeneratePreview = useCallback(() => { setPreviewLoading(true); setPreviewError(false); fetch(getFilePreviewUrl(fileId), { headers: formatHeaders() }) .then((res) => { if (!res.ok) throw new Error("Preview generation failed"); return res.blob(); }) .then((blob) => { setPreviewSrc(URL.createObjectURL(blob)); }) .catch(() => { setPreviewError(true); }) .finally(() => { setPreviewLoading(false); }); }, [fileId]); // PDF files -- render inline in iframe if (isPdf) { return mediaSrc ? (