feat: Medien im Fenster ansehen, und eine Auffrischung, die nichts wegzieht
ANSEHEN OHNE NEUEN TAB. Klick auf eine Kachel oeffnet den Betrachter: Bilder, Videos, Ton und PDF direkt, alles andere mit ehrlichem Hinweis und Herunterladen-Knopf -- ein Betrachter, der bei einer .zip ein schwarzes Feld zeigt, sieht aus wie ein kaputter Betrachter. Pfeiltasten blaettern, Escape schliesst; Adresse kopieren, neuer Tab und Loeschen sitzen in der Kopfzeile. Gezeigt wird IMMER die Vollfassung, nie die Vorschau: 320 Pixel reichen fuer eine Kachel, aber nicht dafuer, ein Bild zu beurteilen -- und genau dazu klickt man darauf. Damit musste die Auswahl umziehen: der Klick auf die Kachel oeffnet jetzt, das Kaestchen oben links waehlt aus. Es erscheint beim Darueberfahren und bleibt sichtbar, solange etwas gewaehlt ist. AUTOMATISCH AUFFRISCHEN, aber nicht stumpf. Ein billiger Fuehler (/api/dash/stand: drei Zahlen, GEMESSEN 6 ms je Abfrage ueber TLS und Proxy) wird alle fuenf Sekunden gefragt; erst wenn er sich aendert, wird richtig nachgeladen. Ein Tab im Hintergrund fragt gar nicht. Die dritte Zahl ist kein Zierrat: nach einem LOESCHEN gehen Anzahl und neuester Zeitstempel auf die alten Werte zurueck -- nur die Ereignis-ID zaehlt weiter. Ohne sie bliebe eine geloeschte Datei in der Galerie stehen. Nachgemessen. UND ES ZIEHT NICHTS WEG: wer etwas ausgewaehlt hat, ein Bild offen hat oder geblaettert ist, bekommt keinen Neuaufbau unter den Haenden, sondern einen Knopf "Neues da -- laden". Eine Liste, die sich beim Klicken selbst neu sortiert, ist der Grund, warum Leute Auto-Refresh abschalten. Der Schalter merkt sich seinen Zustand. Kein SSE, und das mit Absicht: dafuer braeuchte NPM zusaetzlich `proxy_buffering off` -- ein weiterer Handgriff im Proxy, der vergessen werden kann und dann zu einer Verbindung fuehrt, die scheinbar haengt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
import { useEffect } from 'react'
|
||||
import {
|
||||
Check,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Copy,
|
||||
Download,
|
||||
ExternalLink,
|
||||
FileQuestion,
|
||||
Trash2,
|
||||
X,
|
||||
} from 'lucide-react'
|
||||
import type { MediaItem } from '../api'
|
||||
import { useStore } from '../store'
|
||||
import { bytes, zeit } from '../format'
|
||||
import { useKopieren } from './Bausteine'
|
||||
|
||||
/** Eine Datei ansehen, ohne die Seite zu verlassen.
|
||||
*
|
||||
* Bilder, Videos, Ton und PDF werden HIER gezeigt; alles andere bekommt einen
|
||||
* ehrlichen Hinweis samt Herunterladen-Knopf. Ein Betrachter, der bei einer
|
||||
* .zip ein leeres schwarzes Feld zeigt, sieht aus wie ein kaputter
|
||||
* Betrachter.
|
||||
*
|
||||
* Gezeigt wird IMMER die Vollfassung und nie die Vorschau: 320 Pixel breit
|
||||
* reichen für eine Kachel, aber nicht dafür, ein Bild zu beurteilen — und
|
||||
* genau dazu klickt man darauf. */
|
||||
export function Ansehen({
|
||||
item,
|
||||
hatVorher,
|
||||
hatNachher,
|
||||
onVorher,
|
||||
onNachher,
|
||||
onSchliessen,
|
||||
onLoeschen,
|
||||
}: {
|
||||
item: MediaItem
|
||||
hatVorher: boolean
|
||||
hatNachher: boolean
|
||||
onVorher: () => void
|
||||
onNachher: () => void
|
||||
onSchliessen: () => void
|
||||
onLoeschen: () => void
|
||||
}) {
|
||||
const note = useStore((s) => s.note)
|
||||
const { kopiert, kopieren } = useKopieren()
|
||||
|
||||
// Tastatur: Escape zu, Pfeile weiter. Wer 900 Bilder durchsieht, will
|
||||
// nicht 900 Mal zielen.
|
||||
useEffect(() => {
|
||||
const taste = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') onSchliessen()
|
||||
else if (e.key === 'ArrowLeft' && hatVorher) onVorher()
|
||||
else if (e.key === 'ArrowRight' && hatNachher) onNachher()
|
||||
}
|
||||
window.addEventListener('keydown', taste)
|
||||
return () => window.removeEventListener('keydown', taste)
|
||||
}, [hatVorher, hatNachher, onVorher, onNachher, onSchliessen])
|
||||
|
||||
const art = item.mime.split('/')[0]
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex flex-col bg-black/85 backdrop-blur-sm"
|
||||
onClick={onSchliessen}
|
||||
>
|
||||
{/* Kopfzeile */}
|
||||
<header
|
||||
className="flex shrink-0 items-center gap-3 border-b border-white/10 px-4 py-2.5"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate font-mono text-sm text-schrift" title={item.path}>
|
||||
{item.path}
|
||||
</p>
|
||||
<p className="text-xs text-schrift-leise">
|
||||
{bytes(item.size)} · {item.mime} · {zeit(item.updated_at)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={async () => {
|
||||
if (!(await kopieren(item.url, 'url')))
|
||||
note('fehler', 'Die Zwischenablage ist gesperrt.')
|
||||
}}
|
||||
className="flex items-center gap-1.5 rounded border border-white/15 px-2.5 py-1.5
|
||||
text-xs text-schrift-leise hover:bg-white/10 hover:text-schrift"
|
||||
>
|
||||
{kopiert === 'url' ? (
|
||||
<>
|
||||
<Check size={13} className="text-gut" /> kopiert
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Copy size={13} /> Adresse
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<a
|
||||
href={item.url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="rounded border border-white/15 px-2.5 py-1.5 text-schrift-leise
|
||||
hover:bg-white/10 hover:text-schrift"
|
||||
aria-label="In neuem Tab öffnen"
|
||||
>
|
||||
<ExternalLink size={13} />
|
||||
</a>
|
||||
|
||||
<button
|
||||
onClick={onLoeschen}
|
||||
className="rounded border border-white/15 px-2.5 py-1.5 text-schrift-leise
|
||||
hover:bg-schlecht/20 hover:text-schlecht"
|
||||
aria-label="Löschen"
|
||||
>
|
||||
<Trash2 size={13} />
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={onSchliessen}
|
||||
className="rounded border border-white/15 px-2.5 py-1.5 text-schrift-leise
|
||||
hover:bg-white/10 hover:text-schrift"
|
||||
aria-label="Schließen (Esc)"
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
</header>
|
||||
|
||||
{/* Der Inhalt */}
|
||||
<div className="relative flex min-h-0 flex-1 items-center justify-center p-4">
|
||||
{hatVorher && (
|
||||
<Pfeil links onClick={onVorher} />
|
||||
)}
|
||||
{hatNachher && <Pfeil onClick={onNachher} />}
|
||||
|
||||
<div
|
||||
className="flex max-h-full max-w-full items-center justify-center"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{art === 'image' ? (
|
||||
<img
|
||||
src={item.url}
|
||||
alt={item.path}
|
||||
className="max-h-[calc(100vh-8rem)] max-w-full object-contain"
|
||||
/>
|
||||
) : art === 'video' ? (
|
||||
<video
|
||||
src={item.url}
|
||||
controls
|
||||
autoPlay
|
||||
className="max-h-[calc(100vh-8rem)] max-w-full"
|
||||
/>
|
||||
) : art === 'audio' ? (
|
||||
<div className="w-full max-w-lg rounded-lg border border-white/15 bg-flaeche p-6">
|
||||
<audio src={item.url} controls className="w-full" />
|
||||
</div>
|
||||
) : item.mime === 'application/pdf' ? (
|
||||
<iframe
|
||||
src={item.url}
|
||||
title={item.path}
|
||||
className="h-[calc(100vh-8rem)] w-full max-w-4xl rounded border border-white/15 bg-white"
|
||||
/>
|
||||
) : (
|
||||
// Der ehrliche Fall: wir können es nicht zeigen, und
|
||||
// sagen das, statt ein leeres Feld hinzustellen.
|
||||
<div className="flex flex-col items-center gap-3 rounded-lg border border-white/15 bg-flaeche px-10 py-8 text-center">
|
||||
<FileQuestion size={32} className="text-schrift-leise" />
|
||||
<p className="text-sm text-schrift-leise">
|
||||
Diese Art lässt sich hier nicht anzeigen
|
||||
<br />
|
||||
<code className="font-mono text-xs">{item.mime}</code>
|
||||
</p>
|
||||
<a
|
||||
href={item.url}
|
||||
download
|
||||
className="flex items-center gap-2 rounded-md border border-rand px-3 py-1.5
|
||||
text-sm text-schrift hover:bg-flaeche-hoch"
|
||||
>
|
||||
<Download size={15} />
|
||||
Herunterladen
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Pfeil({ links, onClick }: { links?: boolean; onClick: () => void }) {
|
||||
return (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onClick()
|
||||
}}
|
||||
className={`absolute top-1/2 z-10 -translate-y-1/2 rounded-full border border-white/15
|
||||
bg-black/50 p-2 text-schrift-leise hover:bg-black/80 hover:text-schrift
|
||||
${links ? 'left-4' : 'right-4'}`}
|
||||
aria-label={links ? 'Vorheriges' : 'Nächstes'}
|
||||
>
|
||||
{links ? <ChevronLeft size={20} /> : <ChevronRight size={20} />}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user