"use client"; import { useState, useCallback, useEffect } from "react"; import { motion, AnimatePresence, type PanInfo } from "framer-motion"; import { Info, ChevronUp, X } from "lucide-react"; import PreviewToolbar from "./PreviewToolbar"; import PreviewNavigation from "./PreviewNavigation"; import MetadataEditor from "./editors/MetadataEditor"; import VersionHistory from "./VersionHistory"; import { getPlugin } from "./plugins"; import type { FileData } from "@/types/files"; import toast from "react-hot-toast"; interface PreviewEngineProps { file: FileData | null; files?: FileData[]; onClose: () => void; onNavigate?: (file: FileData) => void; /** Appelé après une modification (favori, version restaurée, sauvegarde) pour rafraîchir la liste parente */ onChanged?: () => void; } function useCoarsePointer() { const [coarse, setCoarse] = useState(false); useEffect(() => { const mq = window.matchMedia("(pointer: coarse)"); const upd = () => setCoarse(mq.matches); upd(); mq.addEventListener("change", upd); return () => mq.removeEventListener("change", upd); }, []); return coarse; } export default function PreviewEngine({ file, files = [], onClose, onNavigate, onChanged }: PreviewEngineProps) { const [fullscreen, setFullscreen] = useState(false); const [isEditing, setIsEditing] = useState(false); const [showInfo, setShowInfo] = useState(false); const [showMobileInfo, setShowMobileInfo] = useState(false); const [isFavorite, setIsFavorite] = useState(!!file?.metadata?.isFavorite); const [direction, setDirection] = useState(0); const coarse = useCoarsePointer(); useEffect(() => { setIsFavorite(!!file?.metadata?.isFavorite); }, [file]); // Préchargement des images voisines useEffect(() => { if (!file || files.length < 2) return; const idx = files.findIndex((f) => f.id === file.id); if (idx === -1) return; [files[idx - 1], files[idx + 1]] .filter((f): f is FileData => !!f && f.mimeType.startsWith("image/")) .forEach((f) => { const img = new Image(); img.src = `/api/files/${f.id}/preview?w=2048`; }); }, [file, files]); // Échap : quitter l'édition / le plein écran / fermer useEffect(() => { const onKey = (e: KeyboardEvent) => { if (e.key !== "Escape") return; const t = e.target as HTMLElement | null; if (t && (t.tagName === "INPUT" || t.tagName === "TEXTAREA" || t.isContentEditable)) return; if (isEditing) return; if (fullscreen) { setFullscreen(false); return; } onClose(); }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [isEditing, fullscreen, onClose]); // Bloquer le défilement du fond en plein écran useEffect(() => { if (!fullscreen) return; const prev = document.body.style.overflow; document.body.style.overflow = "hidden"; return () => { document.body.style.overflow = prev; }; }, [fullscreen]); const handleSave = useCallback( async (data: Blob | string, asNewVersion?: boolean) => { if (!file) return; try { if (typeof data === "string") { const res = await fetch(`/api/files/${file.id}/content`, { method: "PUT", headers: { "Content-Type": "text/plain" }, body: data }); if (!res.ok) throw new Error("Save failed"); toast.success("Fichier sauvegardé"); } else { const formData = new FormData(); formData.append("file", data, file.name); if (asNewVersion) formData.append("asNewVersion", "true"); const res = await fetch(`/api/files/${file.id}/save-edit`, { method: "POST", body: formData }); if (!res.ok) throw new Error("Save failed"); toast.success(asNewVersion ? "Nouvelle version créée" : "Fichier sauvegardé"); } onChanged?.(); } catch { toast.error("Erreur lors de la sauvegarde"); } }, [file, onChanged] ); const handleToggleFavorite = useCallback(async () => { if (!file) return; try { const res = await fetch(`/api/files/${file.id}/favorite`, { method: "POST" }); if (res.ok) { const meta = await res.json().catch(() => null); const fav = typeof meta?.isFavorite === "boolean" ? meta.isFavorite : !isFavorite; setIsFavorite(fav); toast.success(fav ? "Ajouté aux favoris" : "Retiré des favoris"); onChanged?.(); } } catch { toast.error("Erreur"); } }, [file, isFavorite, onChanged]); if (!file) return null; const plugin = getPlugin(file.mimeType); const PluginComponent = plugin.component; const idx = files.findIndex((f) => f.id === file.id); const canSwipe = coarse && files.length > 1 && !!onNavigate && !isEditing; const navigateTo = (f: FileData, dir: number) => { setDirection(dir); setIsEditing(false); onNavigate?.(f); }; const onSwipeEnd = (_: unknown, info: PanInfo) => { if (!canSwipe) return; const threshold = 70; if ((info.offset.x < -threshold || info.velocity.x < -500) && idx < files.length - 1) navigateTo(files[idx + 1], 1); else if ((info.offset.x > threshold || info.velocity.x > 500) && idx > 0) navigateTo(files[idx - 1], -1); }; const infoPanel = ( <> ); return ( setFullscreen(!fullscreen)} onToggleEdit={() => setIsEditing(!isEditing)} onToggleFavorite={handleToggleFavorite} isFavorite={isFavorite} onToggleInfo={fullscreen ? () => setShowInfo(!showInfo) : undefined} infoOpen={showInfo} position={files.length > 1 && idx >= 0 ? { index: idx + 1, total: files.length } : undefined} />
{/* Zone principale */}
{files.length > 1 && onNavigate && !isEditing && ( navigateTo(f, files.indexOf(f) > idx ? 1 : -1)} /> )} {/* Bouton fermer flottant (mobile plein écran, si la barre est masquée par un plugin) */} {fullscreen && ( )}
{/* Panneau infos desktop en plein écran */} {fullscreen && showInfo && (
{infoPanel}
)}
{/* Mobile : bascule infos */} {!fullscreen && (
{showMobileInfo && (
{infoPanel}
)}
)} {/* Desktop, panneau latéral (non plein écran) : infos sous l'aperçu */} {!fullscreen && (
{infoPanel}
)}
); }