diff --git a/components/files/file-preview-modal.tsx b/components/files/file-preview-modal.tsx index 848975f0..e1beedcd 100644 --- a/components/files/file-preview-modal.tsx +++ b/components/files/file-preview-modal.tsx @@ -1,10 +1,47 @@ "use client"; -import { useState, useEffect } from "react"; +import { useState, useEffect, useRef } from "react"; import { useTranslations } from "next-intl"; -import { X, Download, Loader2 } from "lucide-react"; +import { X, Download, Loader2, ExternalLink } from "lucide-react"; import { Button } from "@/components/ui/button"; import { getFilePreviewKind } from "@/lib/file-preview"; +import dynamic from "next/dynamic"; + +// pdf.js-based inline viewer for mobile (no native inline PDF viewer). Loaded +// only on the mobile PDF path so pdfjs-dist + its worker never reach the +// desktop bundle. +const PdfMobileViewer = dynamic( + () => import("@/components/files/pdf-mobile-viewer").then((m) => m.PdfMobileViewer), + { ssr: false }, +); + +// Map a few well-known extensions back to canonical MIME types. Used when the +// server returns application/octet-stream (or empty) for an attachment whose +// actual type is obvious from the filename. The blob.type drives how browsers +// render blob: URLs, so guessing wrong here means the inline preview silently +// downgrades to a download. +const EXT_TO_MIME: Record = { + pdf: "application/pdf", + png: "image/png", + jpg: "image/jpeg", + jpeg: "image/jpeg", + gif: "image/gif", + webp: "image/webp", + avif: "image/avif", + bmp: "image/bmp", + mp3: "audio/mpeg", + wav: "audio/wav", + ogg: "audio/ogg", + m4a: "audio/mp4", + mp4: "video/mp4", + webm: "video/webm", + ogv: "video/ogg", +}; + +function inferMimeFromName(name: string): string | undefined { + const ext = name.toLowerCase().split(".").pop(); + return ext ? EXT_TO_MIME[ext] : undefined; +} interface FilePreviewModalProps { name: string; @@ -111,6 +148,49 @@ export function FilePreviewModal({ name, onClose, onDownload, getFileContent }: const [loading, setLoading] = useState(true); const [error, setError] = useState(false); const [resolvedFileType, setResolvedFileType] = useState(() => getFilePreviewKind(name)); + const [pdfInlineSupported, setPdfInlineSupported] = useState(true); + + // Decide whether to render the PDF in a plain