import { useEffect, useState } from 'react' import { filesApi, type Correspondent, type FileItem, type Tag } from '../api/client' import { formatBytes } from '../pages/FileBrowser' import { useOverlayDismiss } from '../hooks/useOverlayDismiss' import TagChip from './TagChip' /** * „Properties" einer Datei – das, was Dateimanager unter Windows und Linux * im Rechtsklickmenü anbieten. * * Anlass war eine Lücke, die man erst beim Benutzen merkt: Bei einem Bild * stand nirgends, **wie groß** es ist. Nicht in Byte – in Pixeln. Die Kachel * zeigt eine Vorschau, die Liste zeigt Kilobyte, und ob das Foto 800 oder * 4000 Punkte breit ist, war der Anwendung schlicht nicht zu entlocken. * * Die Maße kommen **hier im Browser** zustande, nicht vom Server: Er * speichert sie nicht, und für eine Angabe in einem Dialog lohnt weder eine * Spalte in der Datenbank noch ein Durchlauf über alle bestehenden Dateien. * Der Browser kann jedes Bild, das er anzeigen kann, auch ausmessen. */ interface Props { file: FileItem tags: Tag[] correspondent?: Correspondent onClose: () => void } function formatDate(iso: string): string { return new Date(iso).toLocaleString(undefined, { day: '2-digit', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit', }) } /** Der Teil hinter dem letzten Punkt, in Großbuchstaben. Leer, wenn keiner. */ function extensionOf(name: string): string { const at = name.lastIndexOf('.') return at > 0 && at < name.length - 1 ? name.slice(at + 1).toUpperCase() : '' } function Row({ label, children }: { label: string; children: React.ReactNode }) { return (
{label}
{children}
) } export default function FileInfoDialog({ file, tags, correspondent, onClose }: Props) { const overlay = useOverlayDismiss(onClose) const [dims, setDims] = useState<{ w: number; h: number } | null>(null) const [copied, setCopied] = useState(false) const isImage = file.mime_type.startsWith('image/') useEffect(() => { if (!isImage) return let alive = true const img = new Image() // Dieselbe Adresse, die auch die Vorschau nimmt – das Bild liegt danach // ohnehin im Zwischenspeicher des Browsers. img.src = filesApi.inline(file.id) img.onload = () => { if (alive) setDims({ w: img.naturalWidth, h: img.naturalHeight }) } // Kein onerror-Zweig: Kann der Browser das Bild nicht lesen (etwa ein // Format, das er nicht kennt), bleibt die Zeile einfach weg. Eine // Fehlermeldung über ein Maß, das niemand angefordert hat, wäre nur Lärm. return () => { alive = false; img.src = '' } }, [file.id, isImage]) async function copyChecksum() { try { await navigator.clipboard.writeText(file.checksum) setCopied(true) setTimeout(() => setCopied(false), 1500) } catch { /* Ohne sicheren Kontext gibt es keine Zwischenablage – dann bleibt der Wert zum Markieren stehen, er ist ja vollständig zu sehen. */ } } return (

Properties

{file.name}

{extensionOf(file.name) && {extensionOf(file.name)} } {file.mime_type} {formatBytes(file.size)} {/* Die genaue Zahl daneben: Beim Vergleich zweier Fassungen sind „1,2 MB" und „1,2 MB" dasselbe, 1 261 «und» 1 264 Kilobyte nicht. */} {' '}({file.size.toLocaleString()} bytes) {isImage && ( {dims ? <>{dims.w} × {dims.h} px {' '}({(dims.w * dims.h / 1e6).toFixed(1)} MP) : measuring…} )} {file.document_date && {file.document_date}} {correspondent && ( {correspondent.icon ? `${correspondent.icon} ${correspondent.name}` : correspondent.name} )} {tags.length > 0 && ( {tags.map(t => )} )} {formatDate(file.created_at)} {formatDate(file.updated_at)} {file.version} {' '}({file.versioning_enabled ? 'history kept' : 'history off'}) {/* Die Prüfsumme steht schon in der Datenbank und wird bei jedem Download nachgerechnet. Sie hier zu zeigen kostet nichts und beantwortet die eine Frage, für die es sonst keinen Weg gibt: „Ist das dieselbe Datei wie die auf meiner Platte?" */}
) }