Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K6897PWcUEwHiqZMcVnNF6
47 lines
3.0 KiB
HTML
47 lines
3.0 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>g*ndora · winamp</title>
|
|
<link rel="icon" href="favicon.svg">
|
|
<style>
|
|
html,body{margin:0;height:100%;background:#0b0b0e;color:#ededf0;font:13px ui-monospace,Menlo,Consolas,monospace}
|
|
#note{position:fixed;left:50%;top:50%;transform:translate(-50%,-50%);text-align:center;color:#8a8a96;line-height:1.7;max-width:420px;padding:0 16px}
|
|
#note a{color:#ededf0}
|
|
#bar{position:fixed;left:0;right:0;bottom:0;display:flex;gap:14px;justify-content:center;padding:10px;font-size:11px;letter-spacing:.18em;color:#8a8a96}
|
|
#bar a{color:#8a8a96;text-decoration:none}#bar a:hover{color:#ededf0}
|
|
#app{position:fixed;inset:0}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="app"></div>
|
|
<div id="note">loading…</div>
|
|
<div id="bar"><a href="/">◂ G*NDORA</a><span id="count"></span></div>
|
|
<script type="module">
|
|
/* g*ndora's library in Winamp. The tracks come from this origin's IndexedDB. Milkdrop runs Butterchurn's presets on
|
|
the real audio. */
|
|
import Webamp from "https://unpkg.com/webamp@2.3.1/built/webamp.butterchurn-bundle.min.mjs";
|
|
const note = document.getElementById("note");
|
|
function open() { return new Promise((ok, no) => { const r = indexedDB.open("gandora", 3); r.onupgradeneeded = () => { const d = r.result; for (const s of ["tracks", "stations", "tapes"]) if (!d.objectStoreNames.contains(s)) d.createObjectStore(s, { keyPath: "id" }); }; r.onsuccess = () => ok(r.result); r.onerror = () => no(r.error); }); }
|
|
async function all(store) { const d = await open(); return new Promise((ok, no) => { const r = d.transaction(store).objectStore(store).getAll(); r.onsuccess = () => ok(r.result); r.onerror = () => no(r.error); }); }
|
|
try {
|
|
const [tracks, stations] = await Promise.all([all("tracks"), all("stations")]);
|
|
const byStation = Object.fromEntries(stations.map((s) => [s.id, s.name || s.seed]));
|
|
const rows = tracks.filter((t) => t.blob && t.spec && !t.spec.break).sort((a, b) => (b.kept - a.kept) || (b.made - a.made));
|
|
document.getElementById("count").textContent = rows.length ? `${rows.length} TRACKS · KEPT FIRST` : "";
|
|
if (!rows.length) { note.innerHTML = 'nothing in this library yet. <a href="/">play a station</a> first, then come back.'; }
|
|
else note.remove();
|
|
const initialTracks = rows.map((t) => ({ blob: t.blob, metaData: { title: t.spec.title, artist: byStation[t.spec.stationId] || t.spec.seed || "g*ndora" }, duration: t.spec.seconds || 120 }));
|
|
const webamp = new Webamp({
|
|
initialTracks,
|
|
windowLayout: { main: { position: { top: 0, left: 0 } }, equalizer: { position: { top: 116, left: 0 } }, playlist: { position: { top: 232, left: 0 }, size: { extraHeight: 4, extraWidth: 0 } }, milkdrop: { position: { top: 0, left: 275 }, size: { extraHeight: 12, extraWidth: 7 } } },
|
|
enableHotkeys: true,
|
|
});
|
|
await webamp.renderWhenReady(document.getElementById("app"));
|
|
webamp.onClose(() => { location.href = "/"; });
|
|
} catch (e) { note.textContent = "winamp could not start: " + e.message; }
|
|
</script>
|
|
</body>
|
|
</html>
|