From 688803c9a49b6ae89fced8de057566a3378248fa Mon Sep 17 00:00:00 2001 From: Juliet Date: Wed, 11 Mar 2026 05:15:57 +0100 Subject: [PATCH] new view design --- web/src/App.tsx | 2 +- web/src/lib/ui.ts | 6 + web/src/lib/utils.ts | 22 ++- web/src/pages/Upload.tsx | 98 +++++------- web/src/pages/View.tsx | 316 +++++++++++++++++++++------------------ web/src/styles.css | 25 +++- 6 files changed, 250 insertions(+), 219 deletions(-) create mode 100644 web/src/lib/ui.ts diff --git a/web/src/App.tsx b/web/src/App.tsx index e9f9994..fcc9d1c 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -3,7 +3,7 @@ import type { ParentProps } from "solid-js"; export default function App(props: ParentProps) { return (
-
{props.children}
+
{props.children}
= { + mp4: "video/mp4", + webm: "video/webm", + ogv: "video/ogg", + mov: "video/mp4", +}; + +export const AUDIO_MIME: Record = { + mp3: "audio/mpeg", + wav: "audio/wav", + ogg: "audio/ogg", + flac: "audio/flac", + aac: "audio/aac", + m4a: "audio/mp4", +}; + export const IMAGE_MIME: Record = { png: "image/png", jpg: "image/jpeg", @@ -51,7 +70,6 @@ export const IMAGE_MIME: Record = { svg: "image/svg+xml", ico: "image/x-icon", bmp: "image/bmp", - tiff: "image/tiff", avif: "image/avif", }; diff --git a/web/src/pages/Upload.tsx b/web/src/pages/Upload.tsx index 84531b9..d8ed499 100644 --- a/web/src/pages/Upload.tsx +++ b/web/src/pages/Upload.tsx @@ -8,16 +8,11 @@ import { } from "solid-js"; import { generateKey } from "../lib/crypto"; +import { btnClass, btnStyle, fadeIn } from "../lib/ui"; import { formatBytes } from "../lib/utils"; -const viewClass = "flex flex-col items-center gap-2 sm:gap-3"; -const fadeIn = { animation: "fade-in 0.4s" }; - -const btnClass = - "bg-transparent hover:bg-accent/10 active:scale-95 border-accent text-accent rounded-md border px-4 py-1.5 font-medium transition"; -const btnStyle = { "font-size": "clamp(1rem, 3vw, 1.5rem)" }; const ghostClass = - "text-muted hover:text-accent-hover border-none bg-transparent p-0 text-[10px] sm:text-xs"; + "text-muted hover:text-accent-hover border-none bg-transparent min-w-16 py-1.5 -my-1.5 text-[10px] sm:text-xs"; const DURATION_UNITS: Record = { s: 1, @@ -56,7 +51,6 @@ export default function Upload() { const RADIUS = 130; const CENTER = RADIUS + 10; const SIZE = CENTER * 2; - const CIRCUMFERENCE = 2 * Math.PI * RADIUS; const WAVE_AMP = 3; const WAVE_LEN = RADIUS; // one full wave period @@ -64,7 +58,7 @@ export default function Upload() { const topY = CENTER - RADIUS + 2 * RADIUS * (1 - progress() / 100); const left = CENTER - RADIUS - WAVE_LEN; const right = CENTER + RADIUS + WAVE_LEN; - const bottom = CENTER + RADIUS; + const bottom = CENTER + RADIUS + 2; let d = `M ${left} ${bottom} V ${topY}`; for (let x = left; x < right; x += WAVE_LEN / 2) { const cx = x + WAVE_LEN / 4; @@ -77,21 +71,6 @@ export default function Upload() { return d; }; - let prevRatio = 0; - const sizeRatio = createMemo(() => { - const f = file(); - const max = maxFileSize(); - if (!f || !max) return 0; - return Math.min(f.size / max, 1); - }); - - const animDuration = createMemo(() => { - const cur = sizeRatio(); - const dur = Math.max(Math.max(cur, prevRatio) * 800, 200); - prevRatio = cur; - return dur; - }); - const tooLarge = createMemo(() => { const f = file(); const max = maxFileSize(); @@ -285,47 +264,36 @@ export default function Upload() { - - {/* Upload liquid fill */} - - - + + + + +
@@ -334,7 +302,7 @@ export default function Upload() { -
+
-
- - {progress()}% - +
+ + + {progress()}% + + {status() === "encrypting" ? "encrypting\u2026" @@ -383,7 +353,7 @@ export default function Upload() { -
+
-
+
("binary"); const [textContent, setTextContent] = createSignal(""); const [imageSrc, setImageSrc] = createSignal(""); - const [imageAlt, setImageAlt] = createSignal(""); + const [mediaSrc, setMediaSrc] = createSignal(""); const [fileName, setFileName] = createSignal(""); + const [copied, setCopied] = createSignal(false); let decryptedBlob: Blob | null = null; const worker = new Worker( @@ -47,6 +48,7 @@ export default function View() { type: "module", }, ); + onCleanup(() => worker.terminate()); onMount(async () => { @@ -78,22 +80,43 @@ export default function View() { setSize(info.size); setExpiresAt(info.expiresAt); setBurnAfterRead(info.burnAfterRead); - setStage("meta"); + + if (info.burnAfterRead) { + setStage("meta"); + } else { + handleView(); + } }); const handleView = async () => { - setLoadBtnDisabled(true); - setLoadBtnText("Decrypting\u2026"); + setStage("decrypting"); + setProgress(0); + setDecrypting(false); try { - const res = await fetch(`/api/file/${params.id}`); - if (!res.ok) { - setError("File not found or expired."); - setStage("error"); - return; - } + const xhr = new XMLHttpRequest(); + xhr.open("GET", `/api/file/${params.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 buf = await res.arrayBuffer(); const { fileName: name, fileData } = await new Promise<{ fileName: string; fileData: Uint8Array; @@ -111,18 +134,28 @@ export default function View() { [buf], ); }); + const ext = getExt(name); setFileName(name); if (burnAfterRead()) setBurned(true); - if (IMAGE_EXTS.has(ext)) { - const mime = IMAGE_MIME[ext] || "image/png"; + const mime = + IMAGE_MIME[ext] || VIDEO_MIME[ext] || AUDIO_MIME[ext] || undefined; + if (mime) { const blob = new Blob([fileData], { type: mime }); decryptedBlob = blob; - setImageSrc(URL.createObjectURL(blob)); - setImageAlt(name); - setContentType("image"); + 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"); @@ -132,153 +165,146 @@ export default function View() { } setStage("content"); - } catch { - setError("Failed to decrypt. The key may be wrong."); - setLoadBtnDisabled(false); - setLoadBtnText("Retry"); + } catch (e: any) { + setError(e.message || "Failed to decrypt."); + setStage("error"); } }; const copyText = () => { navigator.clipboard.writeText(textContent()); - }; - - const viewRaw = () => { - const blob = new Blob([textContent()], { type: "text/plain" }); - window.open(URL.createObjectURL(blob)); + setCopied(true); + setTimeout(() => setCopied(false), 1500); }; const saveFile = () => { - if (decryptedBlob) triggerDownload(decryptedBlob, fileName()); + if (contentType() === "text") { + const blob = new Blob([textContent()], { type: "text/plain" }); + triggerDownload(blob, fileName()); + } else if (decryptedBlob) { + triggerDownload(decryptedBlob, fileName()); + } }; return ( <> - -
-
Encrypted file
-
- {formatBytes(size())} - {formatExpiry(expiresAt())} -
- -
- - Burns after viewing - -
-
-
- -
+ {progress()}% + + downloading… + +
+
+ + +
+ + {formatBytes(size())} + + + + {formatExpiry(expiresAt())} · burns after viewing + +
+
+ + +
+ {error()}
- - -
-
- - -
-
- {textContent()} -
+
-
+
- -
+ +
{imageAlt()}
-
- + + +
+
- - - -
-

{fileName()}

-
-
- +
- -
- This file has been burned and can no longer be viewed. +
+ + {burned() ? "burned" : formatExpiry(expiresAt())} + + + new drop +
- - - - -
{error()}
+
); diff --git a/web/src/styles.css b/web/src/styles.css index 5285459..5071989 100644 --- a/web/src/styles.css +++ b/web/src/styles.css @@ -16,17 +16,28 @@ } :root { - --c-bg: #faf7f4; - --c-surface: #f0ebe4; - --c-border: #ddd3c8; + --c-bg: #fdf8f2; + --c-surface: #f5ede0; + --c-border: #e0d0c0; --c-text: #1c1410; - --c-muted: #8c7b6e; - --c-accent: #9b5e23; - --c-accent-hover: #7c4a1a; - --c-accent-subtle: #f5ece3; + --c-muted: #9a8070; + --c-accent: #b08855; + --c-accent-hover: #8f6e3f; + --c-accent-subtle: #f5e8d5; --c-danger: #b91c1c; } +@keyframes fade-in { + 0% { + opacity: 0; + transform: translateY(4px); + } + 100% { + opacity: 1; + transform: translateY(0); + } +} + @media (prefers-color-scheme: dark) { :root { --c-bg: #100c09; -- 2.51.2