music video renderer web app deftoku.digi.rip
audio visualizer webapp
1

Configure Feed

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

ui: add file menu sections and import clear buttons

- redid file dropdown into labeled audio/render sections
- replace standalone remove buttons with inline × for parameter images
and file menu
- show imported filename on the import/upload button with truncation
- derive export filename from imported audio name
- add disabled mp4 placeholder button for future format support

digi.rip (Jun 21, 2026, 11:03 PM EDT) 5d6ffc45 841006ef

+101 -29
+15 -2
index.html
··· 30 30 <div class="menu-group"> 31 31 <button class="menu-trigger" data-menu="file">file</button> 32 32 <div class="menu-panel hidden" id="panel-file"> 33 - <button class="menu-item" id="import">import audio</button> 34 - <button class="menu-item" id="render">render video</button> 33 + <label class="menu-label">audio</label> 34 + <div class="import-row"> 35 + <button class="menu-item" id="import">import</button> 36 + <button 37 + class="menu-item clear-btn hidden" 38 + id="clear-audio" 39 + > 40 + × 41 + </button> 42 + </div> 43 + <label class="menu-label">render</label> 44 + <button class="menu-item" id="render-webm">webm</button> 45 + <button class="menu-item" id="render-mp4" disabled> 46 + mp4 47 + </button> 35 48 </div> 36 49 </div> 37 50
+17 -2
src/input.ts
··· 1 1 export let audioBuffer: AudioBuffer | null = null; 2 + export let audioFileName: string | null = null; 2 3 3 4 // import audio from frontend 4 5 const fileInput = document.createElement("input"); ··· 12 13 ctx.close(); 13 14 }); 14 15 15 - const importButton = document.querySelector<HTMLButtonElement>("#import"); 16 - importButton?.addEventListener("click", () => fileInput.click()); 16 + const importButton = document.querySelector<HTMLButtonElement>("#import")!; 17 + const clearBtn = document.querySelector<HTMLButtonElement>("#clear-audio")!; 18 + 19 + importButton.addEventListener("click", () => fileInput.click()); 20 + 21 + clearBtn.addEventListener("click", () => { 22 + audioBuffer = null; 23 + audioFileName = null; 24 + fileInput.value = ""; 25 + importButton.textContent = "import"; 26 + clearBtn.classList.add("hidden"); 27 + }); 28 + 17 29 fileInput.addEventListener("change", () => { 18 30 const file = fileInput.files?.[0]; 19 31 if (!file) return; 20 32 decode(file).then((buffer) => { 21 33 audioBuffer = buffer; 34 + audioFileName = file.name; 35 + importButton.textContent = file.name; 36 + clearBtn.classList.remove("hidden"); 22 37 console.log("decoded", buffer.duration, "seconds"); 23 38 }); 24 39 });
+1 -1
src/main.ts
··· 43 43 currentOptions, 44 44 )); 45 45 46 - const renderBtn = document.querySelector<HTMLButtonElement>("#render"); 46 + const renderBtn = document.querySelector<HTMLButtonElement>("#render-webm"); 47 47 renderBtn!.addEventListener( 48 48 "click", 49 49 () =>
+12 -6
src/ui/params.ts
··· 113 113 uploadBtn.textContent = "upload"; 114 114 uploadBtn.addEventListener("click", () => fileInput.click()); 115 115 116 - const removeBtn = document.createElement("button"); 117 - removeBtn.className = "menu-item"; 118 - removeBtn.textContent = "remove"; 119 - removeBtn.addEventListener("click", () => { 116 + const clearBtn = document.createElement("button"); 117 + clearBtn.className = "menu-item clear-btn hidden"; 118 + clearBtn.textContent = "×"; 119 + clearBtn.addEventListener("click", () => { 120 120 imageCache.delete(p.key); 121 121 delete cachedOpts[p.key]; 122 122 uploadBtn.textContent = "upload"; 123 + clearBtn.classList.add("hidden"); 123 124 fileInput.value = ""; 124 125 }); 125 126 ··· 127 128 const file = fileInput.files?.[0]; 128 129 if (!file) return; 129 130 uploadBtn.textContent = file.name; 131 + clearBtn.classList.remove("hidden"); 130 132 const reader = new FileReader(); 131 133 reader.onload = () => { 132 134 const img = new Image(); ··· 139 141 reader.readAsDataURL(file); 140 142 }); 141 143 144 + const row = document.createElement("div"); 145 + row.className = "import-row"; 146 + row.appendChild(uploadBtn); 147 + row.appendChild(clearBtn); 148 + 142 149 imgPanel.appendChild(rowLabel); 143 - imgPanel.appendChild(uploadBtn); 144 - imgPanel.appendChild(removeBtn); 150 + imgPanel.appendChild(row); 145 151 imgPanel.appendChild(fileInput); 146 152 } 147 153 } else {
+12 -5
src/ui/render.ts
··· 1 - import { audioBuffer } from "../input.ts"; 1 + import { audioBuffer, audioFileName } from "../input.ts"; 2 2 import { output } from "../output.ts"; 3 3 import { 4 4 VisualizerName, ··· 20 20 "high": QUALITY_HIGH, 21 21 }; 22 22 23 - const filename = "output.webm"; 23 + function getOutputFilename(): string { 24 + if (audioFileName) { 25 + const dot = audioFileName.lastIndexOf("."); 26 + const base = dot > 0 ? audioFileName.slice(0, dot) : audioFileName; 27 + return `${base}.webm`; 28 + } 29 + return "output.webm"; 30 + } 24 31 25 32 type ShowSaveFilePicker = ( 26 33 options?: { ··· 52 59 return; 53 60 } 54 61 55 - const renderBtn = document.querySelector<HTMLButtonElement>("#render")!; 62 + const renderBtn = document.querySelector<HTMLButtonElement>("#render-webm")!; 56 63 renderBtn.disabled = true; 57 64 const overlay = createOverlay(); 58 65 ··· 63 70 handle = await ( 64 71 globalThis as unknown as { showSaveFilePicker: ShowSaveFilePicker } 65 72 ).showSaveFilePicker({ 66 - suggestedName: filename, 73 + suggestedName: getOutputFilename(), 67 74 types: [ 68 75 { description: "WebM Video", accept: { "video/webm": [".webm"] } }, 69 76 ], ··· 112 119 const url = URL.createObjectURL(blob); 113 120 const a = document.createElement("a"); 114 121 a.href = url; 115 - a.download = filename; 122 + a.download = getOutputFilename(); 116 123 a.click(); 117 124 URL.revokeObjectURL(url); 118 125 console.log("render done");
+44 -13
style.css
··· 1 - /* ===== CSS Custom Properties ===== */ 2 1 :root { 3 - /* colors */ 4 2 --color-bg: #111; 5 3 --color-fg: #eee; 6 4 --color-muted: #888; ··· 8 6 --color-text-dim: #ddd; 9 7 --color-panel-bg: rgba(30, 30, 30, 0.95); 10 8 11 - /* borders */ 12 9 --border-subtle: rgba(255, 255, 255, 0.08); 13 10 --border-light: rgba(255, 255, 255, 0.1); 14 11 --border-medium: rgba(255, 255, 255, 0.12); 15 12 --border-strong: rgba(255, 255, 255, 0.18); 16 13 17 - /* radii */ 18 14 --radius-sm: 4px; 19 15 --radius-md: 8px; 20 16 21 - /* spacing */ 22 17 --space-xs: 2px; 23 18 --space-sm: 4px; 24 19 --space-md: 8px; 25 20 --space-lg: 10px; 26 21 --space-xl: 14px; 27 22 28 - /* typography */ 29 23 --font-family: 30 24 "Maple Mono", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", 31 25 "Noto Color Emoji", sans-serif; 32 26 } 33 27 34 - /* ===== Font ===== */ 35 28 @font-face { 36 29 font-family: "Maple Mono"; 37 30 font-style: normal; ··· 44 37 format("woff"); 45 38 } 46 39 47 - /* ===== Base ===== */ 48 40 body { 49 41 box-sizing: border-box; 50 42 display: flex; ··· 62 54 font-family: var(--font-family); 63 55 } 64 56 65 - /* ===== Layout ===== */ 57 + /* layout */ 66 58 .main-content { 67 59 position: relative; 68 60 display: flex; ··· 71 63 justify-content: center; 72 64 } 73 65 74 - /* ===== Render Overlay ===== */ 66 + /* render overlay */ 75 67 .render-overlay { 76 68 position: absolute; 77 69 inset: 0; ··· 91 83 will-change: transform; 92 84 } 93 85 94 - /* ===== Menu Bar ===== */ 86 + /* menu bar */ 95 87 .menubar { 96 88 position: fixed; 97 89 top: 0; ··· 140 132 color: var(--color-text-dim); 141 133 } 142 134 143 - /* ===== Menu Dropdowns ===== */ 135 + /* menu dropdowns */ 144 136 .menu-group { 145 137 position: relative; 146 138 } ··· 196 188 background: rgba(255, 255, 255, 0.12); 197 189 } 198 190 191 + .menu-item:disabled { 192 + opacity: 0.35; 193 + cursor: not-allowed; 194 + } 195 + 199 196 .menu-label { 200 197 margin: 0 var(--space-md); 201 198 padding: 6px var(--space-md) var(--space-xs); ··· 213 210 border-radius: var(--radius-sm); 214 211 } 215 212 216 - /* ===== Visualizer Controls ===== */ 213 + /* import */ 214 + .import-row { 215 + display: grid; 216 + grid-template-columns: 1fr auto; 217 + gap: 0; 218 + margin: 0 var(--space-md); 219 + } 220 + 221 + .import-row .menu-item { 222 + margin: 0; 223 + width: auto; 224 + } 225 + 226 + .import-row .menu-item:not(.clear-btn) { 227 + overflow: hidden; 228 + text-overflow: ellipsis; 229 + white-space: nowrap; 230 + } 231 + 232 + .import-row #import { 233 + border-top-right-radius: 0; 234 + border-bottom-right-radius: 0; 235 + } 236 + 237 + .clear-btn { 238 + width: auto !important; 239 + padding: var(--space-sm) var(--space-md) !important; 240 + margin-left: -1px !important; 241 + border-top-left-radius: 0 !important; 242 + border-bottom-left-radius: 0 !important; 243 + font-size: 0.72em !important; 244 + text-align: center !important; 245 + } 246 + 247 + /* visualizer controls */ 217 248 #vis-group { 218 249 display: flex; 219 250 flex-direction: row;