nickel
This commit is contained in:
parent
a9f9f70f84
commit
a087070a2e
@ -1,31 +1,63 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||||
import { LoaderCircle, FileText, Download, RefreshCcw } from 'lucide-react'
|
import { LoaderCircle, FileText, Download, RefreshCcw, Maximize2, Minimize2 } from 'lucide-react'
|
||||||
import { fmtSize } from '@/lib/drive-format'
|
import { fmtSize } from '@/lib/drive-format'
|
||||||
|
|
||||||
function Preview({item,url,onError}) {
|
function Preview({ item, url, onError, fullscreen }) {
|
||||||
if (!url) return <LoaderCircle className="size-6 animate-spin" />
|
if (!url) return <LoaderCircle className="size-6 animate-spin" />
|
||||||
if (url === 'unsupported') return <div className="p-8 text-center"><FileText className="mx-auto mb-2 size-12 text-muted-foreground"/><p className="text-sm">Aperçu indisponible pour ce format. Tu peux le télécharger.</p></div>
|
if (url === 'unsupported') return <div className="p-8 text-center"><FileText className="mx-auto mb-2 size-12 text-muted-foreground"/><p className="text-sm">Impossible de charger l'aperçu. Tu peux réessayer ou télécharger le fichier.</p></div>
|
||||||
|
|
||||||
const name = item?.name?.toLowerCase() || ''
|
const name = item?.name?.toLowerCase() || ''
|
||||||
const mime = (item?.mime_type || '').toLowerCase()
|
const mime = (item?.mime_type || '').toLowerCase()
|
||||||
if ((mime.startsWith('image/') && mime !== 'image/svg+xml') || /\.(jpe?g|png|gif|webp|avif|bmp)$/.test(name)) return <img src={url} onError={onError} alt={item.name} className="max-h-[70dvh] max-w-full object-contain" />
|
const mediaHeight = fullscreen ? 'max-h-full' : 'max-h-[70dvh]'
|
||||||
if (mime === 'application/pdf' || name.endsWith('.pdf')) return <iframe title={item.name} src={url} className="h-[70dvh] w-full rounded-md bg-white" />
|
const frameHeight = fullscreen ? 'h-full min-h-0' : 'h-[70dvh]'
|
||||||
if (mime.startsWith('video/') || /\.(mp4|webm|ogv|mov|m4v)$/.test(name)) return <video key={url} onError={onError} controls playsInline preload="metadata" className="max-h-[70dvh] w-full" src={url}>Vidéo non supportée par le navigateur.</video>
|
const textHeight = fullscreen ? 'h-full min-h-0' : 'h-[65dvh]'
|
||||||
|
|
||||||
|
if ((mime.startsWith('image/') && mime !== 'image/svg+xml') || /\.(jpe?g|png|gif|webp|avif|bmp)$/.test(name)) return <img src={url} onError={onError} alt={item.name} className={`${mediaHeight} max-w-full object-contain`} />
|
||||||
|
if (mime === 'application/pdf' || name.endsWith('.pdf')) return <iframe title={item.name} src={url} onError={onError} className={`${frameHeight} w-full rounded-md bg-white`} />
|
||||||
|
if (mime.startsWith('video/') || /\.(mp4|webm|ogv|mov|m4v)$/.test(name)) return <video key={url} onError={onError} controls playsInline preload="metadata" className={`${mediaHeight} w-full`} src={url}>Vidéo non supportée par le navigateur.</video>
|
||||||
if (mime.startsWith('audio/') || /\.(mp3|wav|ogg|flac|m4a|aac)$/.test(name)) return <audio key={url} onError={onError} controls preload="metadata" className="mx-5 w-full" src={url}>Audio non supporté.</audio>
|
if (mime.startsWith('audio/') || /\.(mp3|wav|ogg|flac|m4a|aac)$/.test(name)) return <audio key={url} onError={onError} controls preload="metadata" className="mx-5 w-full" src={url}>Audio non supporté.</audio>
|
||||||
if (mime.startsWith('text/') || mime==='application/json' || mime==='application/xml' || /\.(txt|md|csv|json|xml|log|py|js|jsx|ts|tsx|css|html|svg)$/.test(name)) return <iframe title={item.name} src={url} className="h-[65dvh] w-full rounded-md bg-white" sandbox="" />
|
if (mime.startsWith('text/') || mime === 'application/json' || mime === 'application/xml' || /\.(txt|md|csv|json|xml|log|py|js|jsx|ts|tsx|css|html|svg)$/.test(name)) return <iframe title={item.name} src={url} onError={onError} className={`${textHeight} w-full rounded-md bg-white`} sandbox="" />
|
||||||
return <p className="p-8 text-sm text-muted-foreground">Aperçu non disponible pour ce type de fichier.</p>
|
return <p className="p-8 text-sm text-muted-foreground">Aperçu non disponible pour ce type de fichier.</p>
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PreviewDialog({ controller: c }) {
|
export default function PreviewDialog({ controller: c }) {
|
||||||
const { preview, setPreview, previewUrl, setPreviewUrl, download, openEntry } = c
|
const { preview, setPreview, previewUrl, setPreviewUrl, download, openEntry } = c
|
||||||
const close=()=>{setPreview(null);setPreviewUrl('')}
|
const [fullscreen, setFullscreen] = useState(false)
|
||||||
|
const close = () => {
|
||||||
|
setFullscreen(false)
|
||||||
|
setPreview(null)
|
||||||
|
setPreviewUrl('')
|
||||||
|
}
|
||||||
|
// A failed URL/media is the only case where retrying is relevant.
|
||||||
|
const previewFailed = previewUrl === 'unsupported'
|
||||||
|
const handleMediaError = () => {
|
||||||
|
setPreviewUrl(current => current === previewUrl ? 'unsupported' : current)
|
||||||
|
}
|
||||||
|
|
||||||
return <Dialog open={!!preview} onOpenChange={value => { if (!value) close() }}>
|
return <Dialog open={!!preview} onOpenChange={value => { if (!value) close() }}>
|
||||||
<DialogContent className="w-[calc(100vw-1.5rem)] max-w-[min(96vw,1100px)]">
|
<DialogContent
|
||||||
<DialogHeader><DialogTitle className="max-w-[90%] break-all text-base">{preview?.name}</DialogTitle><DialogDescription>{preview&&!preview.is_folder ? fmtSize(preview.size) : ''}</DialogDescription></DialogHeader>
|
className={fullscreen
|
||||||
<div className="flex min-h-40 max-h-[72dvh] items-center justify-center overflow-auto rounded-xl bg-muted/60">
|
? 'grid h-dvh w-screen max-w-none grid-rows-[auto_minmax(0,1fr)_auto] gap-3 overflow-hidden rounded-none p-4'
|
||||||
{preview && <Preview item={preview} url={previewUrl} onError={()=>setPreviewUrl('unsupported')}/>}
|
: 'w-[calc(100vw-1.5rem)] max-w-[min(96vw,1100px)]'}
|
||||||
|
style={fullscreen ? { top: 0, left: 0, inset: 0, transform: 'none', width: '100vw', maxWidth: 'none', height: '100dvh', borderRadius: 0 } : undefined}
|
||||||
|
>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="max-w-[90%] break-all text-base">{preview?.name}</DialogTitle>
|
||||||
|
<DialogDescription>{preview && !preview.is_folder ? fmtSize(preview.size) : ''}</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className={fullscreen
|
||||||
|
? 'flex min-h-0 h-full items-center justify-center overflow-auto rounded-xl bg-muted/60'
|
||||||
|
: 'flex min-h-40 max-h-[72dvh] items-center justify-center overflow-auto rounded-xl bg-muted/60'}>
|
||||||
|
{preview && <Preview item={preview} url={previewUrl} fullscreen={fullscreen} onError={handleMediaError}/>}
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>{preview && <Button variant="outline" onClick={()=>openEntry(preview)}><RefreshCcw/> Réessayer</Button>}<Button variant="outline" onClick={close}>Fermer</Button>{preview?.permissions?.includes('download') && <Button onClick={()=>download(preview)}><Download/> Télécharger</Button>}</DialogFooter>
|
<DialogFooter>
|
||||||
|
{previewFailed && preview && <Button variant="outline" onClick={() => openEntry(preview)}><RefreshCcw/> Réessayer</Button>}
|
||||||
|
{preview && <Button variant="outline" onClick={() => setFullscreen(value => !value)} aria-label={fullscreen ? 'Quitter le plein écran' : 'Afficher en plein écran'}>{fullscreen ? <Minimize2/> : <Maximize2/>}{fullscreen ? 'Réduire' : 'Plein écran'}</Button>}
|
||||||
|
<Button variant="outline" onClick={close}>Fermer</Button>
|
||||||
|
{preview?.permissions?.includes('download') && <Button onClick={() => download(preview)}><Download/> Télécharger</Button>}
|
||||||
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
import { ArrowLeft, Download, File, Folder, FolderOpen, LoaderCircle, Eye, X, Image, FileText, ShieldCheck } from 'lucide-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 { Button } from '@/components/ui/button'
|
||||||
import { drive, message, publicFileUrl } from '@/api/client'
|
import { drive, message, publicFileUrl } from '@/api/client'
|
||||||
import { fmtSize } from '@/lib/drive-format'
|
import { fmtSize } from '@/lib/drive-format'
|
||||||
@ -13,14 +13,16 @@ function typeOf(item) {
|
|||||||
if(m.startsWith('text/') || /\.(txt|md|json|csv|xml|log|py|js|css)$/.test(n))return 'text'
|
if(m.startsWith('text/') || /\.(txt|md|json|csv|xml|log|py|js|css)$/.test(n))return 'text'
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
function Viewer({item,token}) {
|
function Viewer({item,token,fullscreen}) {
|
||||||
const kind=typeOf(item)
|
const kind=typeOf(item)
|
||||||
const src=publicFileUrl(token,item.id,true)
|
const src=publicFileUrl(token,item.id,true)
|
||||||
if(kind==='image')return <img className="mx-auto max-h-[72vh] max-w-full rounded-lg object-contain" alt={item.name} src={src}/>
|
const mediaHeight=fullscreen?'max-h-[calc(100dvh-5rem)]':'max-h-[72vh]'
|
||||||
if(kind==='pdf')return <iframe title={item.name} src={src} className="h-[70vh] w-full rounded-xl bg-white"/>
|
const frameHeight=fullscreen?'h-[calc(100dvh-5rem)]':'h-[70vh]'
|
||||||
if(kind==='video')return <video controls playsInline preload="metadata" src={src} className="max-h-[70vh] w-full rounded-xl"/>
|
if(kind==='image')return <img className={`mx-auto ${mediaHeight} max-w-full rounded-lg object-contain`} alt={item.name} src={src}/>
|
||||||
|
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==='audio')return <audio controls src={src} className="w-full"/>
|
||||||
if(kind==='text')return <iframe sandbox="" title={item.name} src={src} className="h-[65vh] w-full rounded-xl bg-white"/>
|
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>
|
return <div className="p-10 text-center text-muted-foreground">Aperçu indisponible pour ce format.</div>
|
||||||
}
|
}
|
||||||
export default function PublicSharePage({token}) {
|
export default function PublicSharePage({token}) {
|
||||||
@ -29,6 +31,23 @@ export default function PublicSharePage({token}) {
|
|||||||
const [selected,setSelected]=useState(null)
|
const [selected,setSelected]=useState(null)
|
||||||
const [error,setError]=useState('')
|
const [error,setError]=useState('')
|
||||||
const [loading,setLoading]=useState(true)
|
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 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()=>{
|
const load=useCallback(async()=>{
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
try { setData(await drive.publicBrowse(token,parent));setError('') }
|
try { setData(await drive.publicBrowse(token,parent));setError('') }
|
||||||
@ -68,9 +87,9 @@ export default function PublicSharePage({token}) {
|
|||||||
</>}
|
</>}
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
{selected && <div className="fixed inset-0 z-50 flex flex-col bg-black/85 p-3 text-white sm:p-6" role="dialog" aria-modal="true" aria-label={`Visualiser ${selected.name}`}>
|
{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 gap-2"><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={()=>setSelected(null)}><X className="size-5"/></Button></div></div>
|
<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 && <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={()=>setSelected(null)}><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}/></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>}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user