"use client" import dynamic from "next/dynamic" import { useRouter } from "next/navigation" import { useEffect, useMemo, useRef, useState, type ReactNode } from "react" import { ChevronLeft, ChevronRight, Copy, Download, ExternalLink, FolderInput, HardDrive, Loader2, Star, Trash2, UserPlus, X, } from "lucide-react" import Link from "next/link" import { toast } from "sonner" import { Dialog, DialogClose, DialogContent, DialogTitle, } from "@/components/ui/dialog" import { Button } from "@/components/ui/button" import { DriveMoveDialog, type DriveFolderPickerMode } from "@/components/drive/drive-move-dialog" import { downloadDriveFile, fetchDrivePreviewBlob } from "@/lib/api/drive-download" import { fetchPublicShareBlob } from "@/lib/api/public-share" import { downloadPublicShareFile } from "@/lib/drive/open-public-share-item" import { useDriveMutations } from "@/lib/api/hooks/use-drive-queries" import { ApiRequestError } from "@/lib/api/client" import { displayFileName } from "@/lib/drive/display-file-name" import { drivePreviewKind, isSvgFile, previewTargetToFileInfo, type DrivePreviewKind, } from "@/lib/drive/drive-preview" import { SvgPreviewViewer } from "@/components/drive/svg-preview-viewer" import { useDriveUIStore } from "@/lib/stores/drive-ui-store" import { apiClient } from "@/lib/api/client" import { MailDriveFolderPicker } from "@/components/mail/mail-drive-folder-picker" import { useSaveAttachmentToDrive } from "@/lib/api/hooks/use-mail-drive-save" import { mailDriveFileHref, mailDriveFolderHref, mailDriveFolderLabel, mailDriveSaveErrorMessage, mailDriveSaveSuccessMessage, } from "@/lib/mail/mail-drive" import { cn } from "@/lib/utils" const TEXT_PREVIEW_MAX_BYTES = 2 * 1024 * 1024 const PdfPreviewViewer = dynamic( () => import("@/components/drive/pdf-preview-viewer").then((m) => m.PdfPreviewViewer), { ssr: false, loading: () => (
Ouverture du PDF…
), }, ) const PREVIEW_ACTION_BTN = "cursor-pointer text-zinc-300 hover:bg-white/10 hover:text-white disabled:pointer-events-none disabled:opacity-40" function PreviewActionButton({ label, onClick, disabled, className, children, }: { label: string onClick: () => void disabled?: boolean className?: string children: ReactNode }) { return ( ) } function PreviewBody({ kind, blobUrl, name, textContent, svgMarkup, onImageError, }: { kind: DrivePreviewKind blobUrl: string name: string textContent?: string | null svgMarkup?: string | null onImageError?: () => void }) { if (kind === "text") { return (
        {textContent ?? ""}
      
) } if (svgMarkup) { return } if (kind === "image") { return ( {displayFileName(name)} ) } if (kind === "video") { return ( ) } if (kind === "audio") { return ( ) } return } export function FilePreviewDialog() { const previewFiles = useDriveUIStore((s) => s.previewFiles) const previewIndex = useDriveUIStore((s) => s.previewIndex) const previewContext = useDriveUIStore((s) => s.previewContext) const closePreview = useDriveUIStore((s) => s.closePreview) const stepPreview = useDriveUIStore((s) => s.stepPreview) const setSharePath = useDriveUIStore((s) => s.setSharePath) const updatePreviewFavorite = useDriveUIStore((s) => s.updatePreviewFavorite) const removePreviewFile = useDriveUIStore((s) => s.removePreviewFile) const mutations = useDriveMutations() const [folderPickerMode, setFolderPickerMode] = useState(null) const [mailSavePickerOpen, setMailSavePickerOpen] = useState(false) const file = previewIndex >= 0 ? (previewFiles[previewIndex] ?? null) : null const fileInfo = useMemo(() => (file ? previewTargetToFileInfo(file) : null), [file]) const allowShare = previewContext?.allowShare ?? true const isTrash = previewContext?.isTrash ?? false const publicShare = previewContext?.publicShare const mailSource = previewContext?.mailSource ?? false const mailMessageId = previewContext?.mailMessageId ?? file?.mailMessageId ?? "" const router = useRouter() const saveToDrive = useSaveAttachmentToDrive(mailMessageId) const isMailAttachment = mailSource && Boolean(file?.mailAttachmentId) const showWriteActions = !isTrash && !publicShare && !isMailAttachment const mailDrivePath = isMailAttachment && file?.path.startsWith("/") ? file.path : undefined const [blobUrl, setBlobUrl] = useState(null) const [textContent, setTextContent] = useState(null) const [svgMarkup, setSvgMarkup] = useState(null) const [loading, setLoading] = useState(false) const [error, setError] = useState(null) const [imgFailed, setImgFailed] = useState(false) const blobUrlRef = useRef(null) const kind = file ? drivePreviewKind(file) : null const isSvg = file ? isSvgFile(file) : false const hasPrev = previewIndex > 0 const hasNext = previewIndex >= 0 && previewIndex < previewFiles.length - 1 const positionLabel = previewFiles.length > 1 ? `${previewIndex + 1} / ${previewFiles.length}` : null const revokeBlobUrl = () => { if (blobUrlRef.current) { URL.revokeObjectURL(blobUrlRef.current) blobUrlRef.current = null } setBlobUrl(null) } useEffect(() => { if (!file || !kind) { revokeBlobUrl() setLoading(false) setError(null) setImgFailed(false) setTextContent(null) setSvgMarkup(null) return } let cancelled = false setLoading(true) setError(null) setImgFailed(false) setTextContent(null) setSvgMarkup(null) ;(async () => { try { const blob = file.mailAttachmentId ? await apiClient.getBlob(`/mail/attachments/${file.mailAttachmentId}`) : publicShare ? await fetchPublicShareBlob( publicShare.token, { path: file.path, name: file.name, mime_type: file.mime_type }, publicShare.password ) : await fetchDrivePreviewBlob(file) if (cancelled) return if (kind === "text") { if (blobUrlRef.current) { URL.revokeObjectURL(blobUrlRef.current) blobUrlRef.current = null } setBlobUrl(null) setSvgMarkup(null) if (blob.size > TEXT_PREVIEW_MAX_BYTES) { setError("Fichier trop volumineux pour l’aperçu texte. Téléchargez-le.") return } setTextContent(await blob.text()) return } if (isSvg) { if (blobUrlRef.current) { URL.revokeObjectURL(blobUrlRef.current) blobUrlRef.current = null } setBlobUrl(null) setSvgMarkup(await blob.text()) return } const url = URL.createObjectURL(blob) const previous = blobUrlRef.current blobUrlRef.current = url setBlobUrl(url) if (previous && previous !== url) { URL.revokeObjectURL(previous) } } catch (err) { if (!cancelled) { const msg = err instanceof ApiRequestError ? err.message : "Impossible de charger l’aperçu." setError(msg) } } finally { if (!cancelled) setLoading(false) } })() return () => { cancelled = true } }, [ file?.path, file?.mime_type, file?.mailAttachmentId, kind, isSvg, publicShare?.token, publicShare?.password, ]) const previewReady = kind === "text" ? textContent !== null : isSvg ? svgMarkup !== null : Boolean(blobUrl) useEffect(() => { if (previewFiles.length === 0) return const onKeyDown = (e: KeyboardEvent) => { if (e.key === "ArrowLeft") { e.preventDefault() stepPreview(-1) } else if (e.key === "ArrowRight") { e.preventDefault() stepPreview(1) } } window.addEventListener("keydown", onKeyDown) return () => window.removeEventListener("keydown", onKeyDown) }, [previewFiles.length, stepPreview]) useEffect(() => { return () => revokeBlobUrl() }, []) const title = file ? displayFileName(file.name) : "" const open = Boolean(file && kind) const onShare = () => { if (!file) return setSharePath(file.path, "file") } const onToggleFavorite = async () => { if (!file) return const next = !file.is_favorite try { await mutations.favorite.mutateAsync({ path: file.path, favorite: next }) updatePreviewFavorite(file.path, next) toast.success(next ? "Ajouté aux favoris" : "Retiré des favoris") } catch { toast.error("Impossible de modifier les favoris") } } const onDelete = async () => { if (!file) return try { await mutations.deleteFile.mutateAsync(file.path) removePreviewFile(file.path) toast.success("Supprimé") } catch { toast.error("Impossible de supprimer") } } const favoriteLabel = file?.is_favorite ? "Retirer des favoris" : "Ajouter aux favoris" const onMailDownload = () => { if (!file?.mailAttachmentId) return void apiClient.getBlob(`/mail/attachments/${file.mailAttachmentId}`).then((blob) => { const url = URL.createObjectURL(blob) const a = document.createElement("a") a.href = url a.download = displayFileName(file.name) a.click() URL.revokeObjectURL(url) }) } const onMailSaveToDrive = async (folderPath: string) => { if (!file?.mailAttachmentId || !mailMessageId) return try { const drivePath = await saveToDrive.mutateAsync({ attachmentId: file.mailAttachmentId, folderPath, }) useDriveUIStore.setState((state) => ({ previewFiles: state.previewFiles.map((f) => f.mailAttachmentId === file.mailAttachmentId ? { ...f, path: drivePath } : f ), })) setMailSavePickerOpen(false) toast.success(mailDriveSaveSuccessMessage(folderPath), { action: { label: "Ouvrir le dossier", onClick: () => router.push(mailDriveFolderHref(folderPath)), }, }) } catch (err) { toast.error(mailDriveSaveErrorMessage(err)) } } return ( <> { if (!nextOpen) setFolderPickerMode(null) }} mode={folderPickerMode ?? "move"} sources={fileInfo ? [fileInfo] : []} onMoved={() => { if (folderPickerMode === "move" && file) removePreviewFile(file.path) setFolderPickerMode(null) }} /> { if (!nextOpen) closePreview() }} >
{title} {positionLabel ? ( {positionLabel} ) : null} {file ? (
{showWriteActions && allowShare ? ( ) : null} {showWriteActions ? ( void onToggleFavorite()} disabled={mutations.favorite.isPending} className={cn( file.is_favorite && "text-amber-400 hover:bg-amber-400/10 hover:text-amber-300" )} > ) : null} {showWriteActions ? ( <> setFolderPickerMode("move")} > setFolderPickerMode("copy")} className="max-sm:hidden" > ) : null} {isMailAttachment && !mailDrivePath ? ( setMailSavePickerOpen(true)} disabled={saveToDrive.isPending} > ) : null} {isMailAttachment && mailDrivePath ? ( {}} disabled className="!w-auto max-w-[min(40vw,16rem)] px-2 opacity-100" > e.stopPropagation()} > {mailDriveFolderLabel(mailDrivePath)} ) : null} void (isMailAttachment ? onMailDownload() : publicShare ? downloadPublicShareFile( publicShare.token, { path: file.path, name: file.name, mime_type: file.mime_type, type: "file", size: 0, last_modified: "", is_favorite: false, is_shared: false }, publicShare.password ) : downloadDriveFile(file.path, file.name, file.name)) } > {showWriteActions ? ( void onDelete()} disabled={mutations.deleteFile.isPending} className="hover:bg-red-500/10 hover:text-red-400" > ) : null}
) : null}
{hasPrev ? ( ) : null} {hasNext ? ( ) : null}
{loading ? (
Chargement…
) : null} {!loading && error ? (

{error}

) : null} {!loading && !error && imgFailed ? (

Aperçu non pris en charge par le navigateur (ex. HEIC). Téléchargez le fichier.

) : null} {!loading && !error && !imgFailed && kind && file && previewReady ? ( setImgFailed(true)} /> ) : null}
) }