102 lines
8.3 KiB
JavaScript
102 lines
8.3 KiB
JavaScript
import { useCallback, useEffect, useRef, useState } from 'react'
|
||
import { ArrowLeft, Download, File, Folder, FolderOpen, LoaderCircle, Eye, X, Image, FileText, ShieldCheck, Maximize2, Minimize2 } from 'lucide-react'
|
||
import { Button } from '@/components/ui/button'
|
||
import { drive, message, publicFileUrl } from '@/api/client'
|
||
import { fmtSize } from '@/lib/drive-format'
|
||
|
||
function typeOf(item) {
|
||
const n = (item.name || '').toLowerCase(), m=(item.mime_type || '').toLowerCase()
|
||
if(m.startsWith('image/') && m!=='image/svg+xml' || /\.(png|jpe?g|gif|webp|bmp|avif)$/.test(n)) return 'image'
|
||
if(n.endsWith('.docx'))return 'word'
|
||
if(m==='application/pdf' || n.endsWith('.pdf'))return 'pdf'
|
||
if(m.startsWith('video/') || /\.(mp4|webm|mov|m4v|ogv)$/.test(n))return 'video'
|
||
if(m.startsWith('audio/') || /\.(mp3|ogg|wav|m4a|flac|aac)$/.test(n))return 'audio'
|
||
if(m.startsWith('text/') || /\.(txt|md|json|csv|xml|log|py|js|css)$/.test(n))return 'text'
|
||
return null
|
||
}
|
||
function Viewer({item,token,fullscreen}) {
|
||
const kind=typeOf(item)
|
||
const src=publicFileUrl(token,item.id,true)
|
||
const mediaHeight=fullscreen?'max-h-[calc(100dvh-5rem)]':'max-h-[72vh]'
|
||
const frameHeight=fullscreen?'h-[calc(100dvh-5rem)]':'h-[70vh]'
|
||
if(kind==='image')return <img className={`mx-auto ${mediaHeight} max-w-full rounded-lg object-contain`} alt={item.name} src={src}/>
|
||
if(kind==='word')return <iframe title={item.name} src={src} sandbox="" className={`${frameHeight} w-full rounded-xl bg-white`}/>
|
||
if(kind==='pdf')return <iframe title={item.name} src={src} className={`${frameHeight} w-full rounded-xl bg-white`}/>
|
||
if(kind==='video')return <video controls playsInline preload="metadata" src={src} className={`${mediaHeight} w-full rounded-xl`}/>
|
||
if(kind==='audio')return <audio controls src={src} className="w-full"/>
|
||
if(kind==='text')return <iframe sandbox="" title={item.name} src={src} className={`${frameHeight} w-full rounded-xl bg-white`}/>
|
||
return <div className="p-10 text-center text-muted-foreground">Aperçu indisponible pour ce format.</div>
|
||
}
|
||
export default function PublicSharePage({token}) {
|
||
const [parent,setParent]=useState(null)
|
||
const [data,setData]=useState(null)
|
||
const [selected,setSelected]=useState(null)
|
||
const [error,setError]=useState('')
|
||
const [loading,setLoading]=useState(true)
|
||
const viewerRef=useRef(null)
|
||
const [fullscreen,setFullscreen]=useState(false)
|
||
const [fullscreenSupported,setFullscreenSupported]=useState(false)
|
||
useEffect(()=>{
|
||
setFullscreenSupported(Boolean(document.fullscreenEnabled))
|
||
const update=()=>setFullscreen(document.fullscreenElement===viewerRef.current)
|
||
document.addEventListener('fullscreenchange', update)
|
||
return ()=>document.removeEventListener('fullscreenchange', update)
|
||
},[])
|
||
const closeViewer=()=>{
|
||
if(document.fullscreenElement===viewerRef.current) document.exitFullscreen?.().catch(()=>{})
|
||
setSelected(null)
|
||
}
|
||
const toggleFullscreen=async()=>{
|
||
try {
|
||
if(document.fullscreenElement===viewerRef.current) await document.exitFullscreen()
|
||
else await viewerRef.current?.requestFullscreen()
|
||
} catch {
|
||
// Browsers may deny fullscreen; leave the normal viewer usable.
|
||
}
|
||
}
|
||
const load=useCallback(async()=>{
|
||
setLoading(true)
|
||
try { setData(await drive.publicBrowse(token,parent));setError('') }
|
||
catch(e){setError(e?.response?.status===404?'Ce lien a expiré ou n’est plus disponible.':message(e))}
|
||
finally{setLoading(false)}
|
||
},[token,parent])
|
||
useEffect(()=>{load()},[load])
|
||
// A public file link first shows its details; preview starts only on "Ouvrir".
|
||
useEffect(()=>{setSelected(null)},[token])
|
||
const previous=()=>setParent(data?.breadcrumb?.length>1?data.breadcrumb.at(-2).id:null)
|
||
const items=data?.items || []
|
||
return <div className="min-h-dvh bg-muted/25 px-4 py-5 sm:py-12">
|
||
<div className="mx-auto max-w-5xl space-y-5">
|
||
<header className="flex items-center gap-3 px-1"><img src="/icon.png" alt="GoodDrive" className="size-12 rounded-xl"/><div><h1 className="text-xl font-bold tracking-tight">GoodDrive</h1><p className="text-sm text-muted-foreground">Un partage, tout simplement.</p></div></header>
|
||
<main className="overflow-hidden rounded-3xl border bg-card shadow-sm">
|
||
{loading ? <div className="flex min-h-72 items-center justify-center gap-3"><LoaderCircle className="size-5 animate-spin"/> Chargement…</div>
|
||
: error ? <div className="p-10 text-center text-destructive" role="alert">{error}</div>
|
||
: data && <>
|
||
<div className="border-b bg-muted/20 px-5 py-6 sm:px-8">
|
||
<div className="mb-3 flex items-center gap-2 text-xs font-medium text-muted-foreground"><ShieldCheck className="size-4"/> Partage public en lecture seule</div>
|
||
<h2 className="break-words text-2xl font-semibold tracking-tight">{data.root.name}</h2>
|
||
{data.root.is_folder ? <p className="mt-2 text-sm text-muted-foreground">{parent ? data.breadcrumb?.map(i=>i.name).join(' / ') : `${items.length} élément${items.length>1?'s':''}`}</p> : <p className="mt-2 text-sm text-muted-foreground">{fmtSize(data.root.size)}</p>}
|
||
</div>
|
||
{data.root.is_folder && <div className="px-3 py-3 sm:px-5 sm:py-5">
|
||
{parent && <Button variant="ghost" size="sm" className="mb-3" onClick={previous}><ArrowLeft className="size-4"/> Retour</Button>}
|
||
<div className="space-y-1.5">{items.length ? items.map(item=><div key={item.id} className="group flex items-center gap-3 rounded-xl border border-transparent p-3 transition-colors hover:border-border hover:bg-muted/45">
|
||
<div className="grid size-11 shrink-0 place-items-center rounded-xl bg-primary/10 text-primary">{item.is_folder?<Folder className="size-6"/>:typeOf(item)==='image'?<Image className="size-5"/>:typeOf(item)==='pdf'?<FileText className="size-5"/>:<File className="size-5"/>}</div>
|
||
<button type="button" onClick={()=>item.is_folder?setParent(item.id):setSelected(item)} className="min-w-0 flex-1 text-left"><span className="block truncate text-sm font-medium">{item.name}</span><span className="text-xs text-muted-foreground">{item.is_folder?'Dossier':fmtSize(item.size)}</span></button>
|
||
<Button variant="outline" size="sm" onClick={()=>item.is_folder?setParent(item.id):setSelected(item)}>{item.is_folder?<FolderOpen className="size-4"/>:<Eye className="size-4"/>} Ouvrir</Button>
|
||
{!item.is_folder && <a className="grid size-8 shrink-0 place-items-center rounded-lg hover:bg-muted" aria-label={`Télécharger ${item.name}`} href={publicFileUrl(token,item.id)}><Download className="size-4"/></a>}
|
||
</div>):<p className="py-12 text-center text-sm text-muted-foreground">Ce dossier est vide.</p>}</div>
|
||
</div>}
|
||
{!data.root.is_folder && <div className="flex flex-wrap items-center justify-end gap-3 px-5 py-5 sm:px-8">
|
||
<Button onClick={()=>setSelected(data.root)} className="gap-2"><Eye className="size-4"/> Ouvrir</Button>
|
||
<a className="inline-flex h-10 items-center gap-2 rounded-xl border bg-background px-4 text-sm font-medium hover:bg-muted" href={publicFileUrl(token,data.root.id)}><Download className="size-4"/> Télécharger</a>
|
||
</div>}
|
||
</>}
|
||
</main>
|
||
</div>
|
||
{selected && <div ref={viewerRef} className="fixed inset-0 z-50 flex flex-col bg-black/95 p-3 text-white sm:p-6" role="dialog" aria-modal="true" aria-label={`Visualiser ${selected.name}`}>
|
||
<div className="mb-4 flex items-center justify-between gap-2"><p className="min-w-0 truncate font-medium">{selected.name}</p><div className="flex items-center gap-2">{fullscreenSupported && !['video', 'audio'].includes(typeOf(selected)) && <Button variant="secondary" size="icon" aria-label={fullscreen ? "Quitter le plein écran" : "Afficher en plein écran"} title={fullscreen ? "Réduire" : "Plein écran"} onClick={toggleFullscreen}>{fullscreen ? <Minimize2 className="size-5"/> : <Maximize2 className="size-5"/>}</Button>}<a className="inline-flex items-center gap-2 rounded-xl bg-white/15 px-3 py-2 text-sm hover:bg-white/25" href={publicFileUrl(token,selected.id)}><Download className="size-4"/> Télécharger</a><Button variant="secondary" size="icon" aria-label="Fermer l’aperçu" onClick={closeViewer}><X className="size-5"/></Button></div></div>
|
||
<div className="flex min-h-0 flex-1 items-center justify-center overflow-auto"><Viewer item={selected} token={token} fullscreen={fullscreen}/></div>
|
||
</div>}
|
||
</div>
|
||
}
|