zero-knowledge file sharing
13
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix record format detection

Juliet ec69b4c9 6e6701da

+50 -22
+4 -4
bun.lock
··· 5 5 "": { 6 6 "name": "drop", 7 7 "dependencies": { 8 - "hono": "latest", 8 + "hono": "^4.12.12", 9 9 }, 10 10 "devDependencies": { 11 - "@types/bun": "latest", 12 - "oxfmt": "latest", 11 + "@types/bun": "^1.3.12", 12 + "oxfmt": "^0.44.0", 13 13 }, 14 14 "peerDependencies": { 15 - "typescript": "latest", 15 + "typescript": "^6.0.2", 16 16 }, 17 17 }, 18 18 },
+2 -2
web/bun.lock
··· 11 11 "@tailwindcss/vite": "^4.2.2", 12 12 "tailwindcss": "^4.2.2", 13 13 "typescript": "^6.0.2", 14 - "vite": "^8.0.2", 15 - "vite-plugin-solid": "^2.11.11", 14 + "vite": "^8.0.8", 15 + "vite-plugin-solid": "^2.11.12", 16 16 }, 17 17 }, 18 18 },
+19 -15
web/src/pages/Upload.tsx
··· 23 23 type Status = "idle" | "encrypting" | "uploading"; 24 24 type View = "result" | "uploading" | "file" | "empty" | "recording"; 25 25 26 - const REC_MIMES: { mime: string; ext: string }[] = [ 27 - { mime: "audio/webm;codecs=opus", ext: "webm" }, 28 - { mime: "audio/webm", ext: "webm" }, 29 - { mime: "audio/mp4", ext: "mp4" }, 30 - { mime: "audio/ogg;codecs=opus", ext: "ogg" }, 31 - ]; 32 - function pickRecMime() { 26 + const REC_MIMES = ["audio/webm;codecs=opus", "audio/webm", "audio/mp4", "audio/ogg;codecs=opus"]; 27 + function pickRecMime(): string | null { 33 28 const MR = (window as any).MediaRecorder; 34 29 if (!MR) return null; 35 30 for (const m of REC_MIMES) { 36 - if (MR.isTypeSupported?.(m.mime)) return m; 31 + if (MR.isTypeSupported?.(m)) return m; 37 32 } 38 - return { mime: "", ext: "webm" }; 33 + return ""; 34 + } 35 + function extForAudio(mimeType: string): string { 36 + const base = mimeType.split(";")[0].toLowerCase(); 37 + if (base.includes("mp4") || base.includes("aac") || base.includes("mpeg")) return "m4a"; 38 + if (base.includes("ogg")) return "ogg"; 39 + if (base.includes("wav")) return "wav"; 40 + return "webm"; 39 41 } 40 42 41 43 function formatTime(s: number) { ··· 159 161 }; 160 162 161 163 const startRecording = async () => { 162 - const picked = pickRecMime(); 163 - if (!picked) { 164 + const pickedMime = pickRecMime(); 165 + if (pickedMime === null) { 164 166 setError("recording not supported in this browser"); 165 167 return; 166 168 } ··· 198 200 } catch {} 199 201 200 202 const chunks: BlobPart[] = []; 201 - const mr = new MediaRecorder(mediaStream, picked.mime ? { mimeType: picked.mime } : undefined); 203 + const mr = new MediaRecorder(mediaStream, pickedMime ? { mimeType: pickedMime } : undefined); 202 204 mediaRecorder = mr; 203 205 mr.ondataavailable = (e) => { 204 206 if (e.data.size > 0) chunks.push(e.data); 205 207 }; 206 208 mr.onstop = () => { 207 - const type = picked.mime.split(";")[0] || "audio/webm"; 208 - const blob = new Blob(chunks, { type }); 209 + const actualMime = mr.mimeType || pickedMime || "audio/webm"; 210 + const baseType = actualMime.split(";")[0] || "audio/webm"; 211 + const ext = extForAudio(actualMime); 212 + const blob = new Blob(chunks, { type: baseType }); 209 213 const ts = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19); 210 - const f = new File([blob], `recording-${ts}.${picked.ext}`, { type }); 214 + const f = new File([blob], `recording-${ts}.${ext}`, { type: baseType }); 211 215 setFile(f); 212 216 stopRecStream(); 213 217 setRecording(false);
+25 -1
web/src/pages/View.tsx
··· 18 18 const ghostClass = 19 19 "text-muted hover:text-accent hover:border-accent border border-border bg-transparent rounded px-2 py-1 text-sm transition-colors"; 20 20 21 + function isAudioOnlyWebm(url: string): Promise<boolean> { 22 + return new Promise((resolve) => { 23 + const v = document.createElement("video"); 24 + v.preload = "metadata"; 25 + v.muted = true; 26 + let done = false; 27 + const finish = (result: boolean) => { 28 + if (done) return; 29 + done = true; 30 + v.removeAttribute("src"); 31 + v.load(); 32 + resolve(result); 33 + }; 34 + v.onloadedmetadata = () => finish(v.videoWidth === 0 && v.videoHeight === 0); 35 + v.onerror = () => finish(false); 36 + setTimeout(() => finish(false), 2000); 37 + v.src = url; 38 + }); 39 + } 40 + 21 41 type Stage = "loading" | "meta" | "decrypting" | "content" | "error"; 22 42 type ContentType = "text" | "image" | "video" | "audio" | "binary"; 23 43 ··· 146 166 setContentType("image"); 147 167 } else if (VIDEO_EXTS.has(ext)) { 148 168 setMediaSrc(url); 149 - setContentType("video"); 169 + if (ext === "webm" && (await isAudioOnlyWebm(url))) { 170 + setContentType("audio"); 171 + } else { 172 + setContentType("video"); 173 + } 150 174 } else { 151 175 setMediaSrc(url); 152 176 setContentType("audio");