import { createSignal, Show, onMount, onCleanup } from "solid-js"; import { importKey } from "../lib/crypto"; import { btnClass, btnStyle, fadeIn } from "../lib/ui"; import { formatBytes, formatExpiry, getExt, triggerDownload, IMAGE_EXTS, TEXT_EXTS, VIDEO_EXTS, IMAGE_MIME, VIDEO_MIME, AUDIO_MIME, } from "../lib/utils"; const ghostClass = "text-muted hover:text-accent hover:border-accent border border-border bg-transparent rounded px-2 py-1 text-sm transition-colors"; type Stage = "loading" | "meta" | "decrypting" | "content" | "error"; type ContentType = "text" | "image" | "video" | "audio" | "binary"; export default function View() { const parts = location.pathname.split("/").filter(Boolean); const id = parts[0] === "p" ? parts[1] : parts[0]; const [stage, setStage] = createSignal("loading"); const [error, setError] = createSignal(""); const [size, setSize] = createSignal(0); const [expiresAt, setExpiresAt] = createSignal(0); const [burnAfterRead, setBurnAfterRead] = createSignal(false); const [burned, setBurned] = createSignal(false); const [progress, setProgress] = createSignal(0); const [decrypting, setDecrypting] = createSignal(false); const [contentType, setContentType] = createSignal("binary"); const [textContent, setTextContent] = createSignal(""); const [imageSrc, setImageSrc] = createSignal(""); const [mediaSrc, setMediaSrc] = createSignal(""); const [fileName, setFileName] = createSignal(""); const [copied, setCopied] = createSignal(false); let decryptedBlob: Blob | null = null; const worker = new Worker( new URL("../lib/crypto.worker.ts", import.meta.url), { type: "module", }, ); onCleanup(() => worker.terminate()); onMount(async () => { const keyEncoded = window.location.hash.slice(1); if (!id || !keyEncoded) { setError("Invalid URL."); setStage("error"); return; } try { await importKey(keyEncoded); } catch { setError("Invalid key."); setStage("error"); return; } const infoRes = await fetch(`/api/file/${id}/info`); if (!infoRes.ok) { setError("File not found or expired."); setStage("error"); return; } const info = await infoRes.json(); setSize(info.size); setExpiresAt(info.expiresAt); setBurnAfterRead(info.burnAfterRead); if (info.burnAfterRead) { setStage("meta"); } else { handleView(); } }); const handleView = async () => { setStage("decrypting"); setProgress(0); setDecrypting(false); try { const xhr = new XMLHttpRequest(); xhr.open("GET", `/api/file/${id}`); xhr.responseType = "arraybuffer"; const buf = await new Promise((resolve, reject) => { xhr.onprogress = (e) => { if (e.lengthComputable) { setProgress(Math.round((e.loaded / e.total) * 100)); } }; xhr.onload = () => { if (xhr.status >= 200 && xhr.status < 300) { setProgress(100); setDecrypting(true); resolve(xhr.response); } else { reject(new Error("File not found or expired.")); } }; xhr.onerror = () => reject(new Error("Download failed.")); xhr.send(); }); const { fileName: name, fileData } = await new Promise<{ fileName: string; fileData: Uint8Array; }>((resolve, reject) => { worker.onmessage = (e) => { if (e.data.error) reject(new Error(e.data.error)); else resolve(e.data); }; worker.postMessage( { type: "decrypt", ciphertext: buf, keyEncoded: location.hash.slice(1), }, [buf], ); }); const ext = getExt(name); setFileName(name); if (burnAfterRead()) setBurned(true); const mime = IMAGE_MIME[ext] || VIDEO_MIME[ext] || AUDIO_MIME[ext] || undefined; if (mime) { const blob = new Blob([fileData], { type: mime }); decryptedBlob = blob; const url = URL.createObjectURL(blob); if (IMAGE_EXTS.has(ext)) { setImageSrc(url); setContentType("image"); } else if (VIDEO_EXTS.has(ext)) { setMediaSrc(url); setContentType("video"); } else { setMediaSrc(url); setContentType("audio"); } } else if (TEXT_EXTS.has(ext)) { setTextContent(new TextDecoder().decode(fileData)); setContentType("text"); } else { decryptedBlob = new Blob([fileData]); setContentType("binary"); } setStage("content"); } catch (e: any) { setError(e.message || "Failed to decrypt."); setStage("error"); } }; const copyText = () => { navigator.clipboard.writeText(textContent()); setCopied(true); setTimeout(() => setCopied(false), 1500); }; const saveFile = () => { if (contentType() === "text") { const blob = new Blob([textContent()], { type: "text/plain" }); triggerDownload(blob, fileName()); } else if (decryptedBlob) { triggerDownload(decryptedBlob, fileName()); } }; return ( <>
loading…
decrypting…} > {progress()}% downloading…
{formatBytes(size())} {formatExpiry(expiresAt())} · burns after viewing
{error()}
{fileName()} {formatBytes(size())}
{fileName()}
{textContent()}
{burned() ? "burned" : formatExpiry(expiresAt())} new drop
); }