Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

update: add track selector to play.mjs

Browse available tracks when visiting play with no params.
Tap or use arrow keys + enter to pick a track. TRACKS button
returns to the selector from the player view.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+105 -16
+105 -16
system/public/aesthetic.computer/disks/play.mjs
··· 1 1 // Play, 2025.4.05 2 - // Simple audio player. Usage: play [track-name|url] 2 + // Simple audio player with track selector. Usage: play [track-name|url] 3 3 4 4 import { createScrubber } from "./common/scrub.mjs"; 5 5 ··· 13 13 }, 14 14 }; 15 15 16 + const trackKeys = Object.keys(KNOWN_TRACKS); 17 + 18 + // State 19 + let mode = "select"; // "select" or "player" 20 + let selectedIndex = 0; 16 21 let trackTitle, audioUrl; 17 22 let isPlaying = false, isLoading = false, hasStarted = false; 18 23 let currentTime = 0, duration = 0, bufferedTime = 0; 19 24 let scrubber = createScrubber(); 20 25 let bars = new Array(32).fill(0); 21 26 let frequencyData = []; 22 - let playBtn, scrubBox; 23 - 24 - function boot({ params, send, ui, screen, hud }) { 25 - hud.label("play"); 26 - const param = params[0] || "sopra-reversed"; 27 + let playBtn, backBtn, scrubBox; 28 + let trackBtns = []; 27 29 28 - const track = KNOWN_TRACKS[param]; 30 + function loadTrack(key, send, ui, screen) { 31 + const track = KNOWN_TRACKS[key]; 29 32 if (track) { 30 33 audioUrl = track.audio; 31 34 trackTitle = track.title; 32 - } else if (param.startsWith("http")) { 33 - audioUrl = param; 34 - trackTitle = param.split("/").pop().replace(/\.[^.]+$/, ""); 35 + } else if (key.startsWith("http")) { 36 + audioUrl = key; 37 + trackTitle = key.split("/").pop().replace(/\.[^.]+$/, ""); 38 + } else { 39 + audioUrl = `${CDN}/audio/${key}.mp3`; 40 + trackTitle = key; 41 + } 42 + // Stop any current playback 43 + if (hasStarted) send({ type: "stream:stop", content: { id: STREAM_ID } }); 44 + isPlaying = false; isLoading = false; hasStarted = false; 45 + currentTime = 0; duration = 0; bufferedTime = 0; 46 + scrubber.reset(); 47 + bars.fill(0); 48 + frequencyData = []; 49 + playBtn = new ui.TextButton("PLAY", { center: "x", screen, y: 0 }); 50 + backBtn = new ui.TextButton("TRACKS", { x: 6, y: 0, screen }); 51 + mode = "player"; 52 + } 53 + 54 + function boot({ params, send, ui, screen, hud, jump }) { 55 + hud.label("play"); 56 + 57 + if (params[0]) { 58 + loadTrack(params[0], send, ui, screen); 35 59 } else { 36 - audioUrl = `${CDN}/audio/${param}.mp3`; 37 - trackTitle = param; 60 + // Build track selector buttons 61 + trackBtns = trackKeys.map( 62 + (key, i) => new ui.TextButton(KNOWN_TRACKS[key].title, { x: 6, y: 0, screen }), 63 + ); 64 + mode = "select"; 65 + } 66 + } 67 + 68 + function paint($) { 69 + if (mode === "select") return paintSelect($); 70 + return paintPlayer($); 71 + } 72 + 73 + function paintSelect({ wipe, ink, screen }) { 74 + wipe(20, 15, 30); 75 + const m = 6; 76 + let y = 20; 77 + 78 + ink(200, 180, 220).write("TRACKS", { x: m, y }); 79 + y += 18; 80 + 81 + for (let i = 0; i < trackKeys.length; i++) { 82 + const btn = trackBtns[i]; 83 + btn.reposition({ x: m, y }); 84 + const selected = i === selectedIndex; 85 + btn.paint({ ink }, selected 86 + ? [[0, 80, 70], [0, 200, 180], 255] 87 + : [[40, 30, 55], [120, 100, 160], [200, 180, 220]]); 88 + y += btn.height + 4; 38 89 } 39 90 40 - playBtn = new ui.TextButton("PLAY", { center: "x", screen, y: 0 }); 91 + y += 12; 92 + ink(100, 90, 120).write("or: play:https://url.mp3", { x: m, y }); 41 93 } 42 94 43 - function paint({ wipe, ink, screen, line }) { 95 + function paintPlayer({ wipe, ink, screen, line }) { 44 96 const floor = Math.floor; 45 97 wipe(20, 15, 30); 46 98 47 99 const m = 6; 48 - let y = 20; 100 + let y = 6; 101 + 102 + // Back button 103 + backBtn.reposition({ x: m, y }); 104 + backBtn.paint({ ink }, [[40, 30, 55], [80, 70, 100], [150, 130, 170]]); 105 + y += backBtn.height + 6; 49 106 50 107 // Title 51 108 ink(200, 180, 220).write(trackTitle, { x: m, y }); ··· 107 164 } 108 165 } 109 166 110 - function act({ event: e, screen, send }) { 167 + function act({ event: e, screen, send, ui, jump }) { 168 + if (mode === "select") { 169 + // Track buttons 170 + for (let i = 0; i < trackBtns.length; i++) { 171 + trackBtns[i].act(e, { 172 + push: () => { 173 + selectedIndex = i; 174 + loadTrack(trackKeys[i], send, ui, screen); 175 + }, 176 + }); 177 + } 178 + // Keyboard nav 179 + if (e.is("keyboard:down:arrowdown")) selectedIndex = Math.min(selectedIndex + 1, trackKeys.length - 1); 180 + if (e.is("keyboard:down:arrowup")) selectedIndex = Math.max(selectedIndex - 1, 0); 181 + if (e.is("keyboard:down:enter")) loadTrack(trackKeys[selectedIndex], send, ui, screen); 182 + return; 183 + } 184 + 185 + // Player mode 186 + backBtn?.act(e, { 187 + push: () => { 188 + if (hasStarted) send({ type: "stream:stop", content: { id: STREAM_ID } }); 189 + isPlaying = false; hasStarted = false; currentTime = 0; duration = 0; 190 + scrubber.reset(); bars.fill(0); 191 + mode = "select"; 192 + // Rebuild track buttons 193 + trackBtns = trackKeys.map( 194 + (key) => new ui.TextButton(KNOWN_TRACKS[key].title, { x: 6, y: 0, screen }), 195 + ); 196 + }, 197 + }); 198 + 111 199 playBtn?.act(e, { push: () => togglePlay(send) }); 112 200 113 201 if (scrubBox && e.is("touch") && duration > 0) { ··· 135 223 } 136 224 137 225 function sim({ send }) { 226 + if (mode !== "player") return; 138 227 send({ type: "stream:time", content: { id: STREAM_ID } }); 139 228 if (isPlaying) send({ type: "stream:frequencies", content: { id: STREAM_ID } }); 140 229