"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 (
)
}
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)
}}
/>
>
)
}