import {useEffect, useState} from 'react' import {AlertTriangle, Download, Loader2, X} from 'lucide-react' import type {ObjectPreviewKind} from '@/lib/format' interface ObjectPreviewModalProps { kind: ObjectPreviewKind name: string src: string onClose: () => void } /** Text previews are fetched inline, so cap how much we pull and render. */ const TEXT_PREVIEW_LIMIT = 200_000 /** * Lightbox/preview drawer for storage objects. Reuses the existing object * download URL as the media source — no new backend endpoint, per the * "never invent a custom protocol" rule in CONTRIBUTING.md. Image/video/audio * are rendered via native HTML media elements; text is fetched and shown as * plain text, since there is no inline "view" mode on the download endpoint. */ export function ImagePreviewModal({kind, name, src, onClose}: ObjectPreviewModalProps) { useEffect(() => { function onKeyDown(event: KeyboardEvent) { if (event.key === 'Escape') onClose() } window.addEventListener('keydown', onKeyDown) return () => window.removeEventListener('keydown', onKeyDown) }, [onClose]) return (
{state.text}
{state.truncated && '\n\n… truncated'}
)
}
/**
* Fetches only as much of the object as needed for the preview. Stops
* reading (and cancels the underlying connection) once TEXT_PREVIEW_LIMIT
* characters have been decoded, instead of buffering the full response —
* important for large text-like objects, since res.text() would otherwise
* transfer and hold the entire body in memory just to show the first slice.
*/
async function readTextPreview(src: string, signal: AbortSignal): Promise<{text: string; truncated: boolean}> {
const res = await fetch(src, {signal})
if (!res.ok || !res.body) throw new Error(`HTTP ${res.status}`)
const reader = res.body.getReader()
const decoder = new TextDecoder()
let text = ''
let truncated: boolean
try {
while (text.length < TEXT_PREVIEW_LIMIT) {
const {done, value} = await reader.read()
if (done) break
text += decoder.decode(value, {stream: true})
}
if (text.length > TEXT_PREVIEW_LIMIT) {
text = text.slice(0, TEXT_PREVIEW_LIMIT)
// A code unit landing exactly on the cut could be the high half of a
// surrogate pair (e.g. an emoji) with its low half past the boundary —
// slice() doesn't know that, and leaves a lone surrogate that renders
// as a replacement glyph. Drop it so the preview only ever ends on a
// complete character.
const lastCode = text.charCodeAt(text.length - 1)
if (lastCode >= 0xd800 && lastCode <= 0xdbff) text = text.slice(0, -1)
truncated = true
} else {
// Confirm whether more data was actually available before claiming
// completeness — the loop can exit exactly at the limit.
const {done} = await reader.read()
truncated = !done
}
} finally {
void reader.cancel().catch(() => {})
}
return {text, truncated}
}
function PreviewStatus({label, isError}: {label: string; isError?: boolean}) {
return (