From a78d6db949c6ff80573bd6cd306d148cd2fc1944 Mon Sep 17 00:00:00 2001 From: teahubxyz Date: Tue, 20 Oct 2026 20:34:00 +0000 Subject: [PATCH] gandora: the URL follows the station; SHARE links carry a track's sheet music so it opens and renders for anyone Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01K6897PWcUEwHiqZMcVnNF6 --- ventures/gandora/index.html | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/ventures/gandora/index.html b/ventures/gandora/index.html index 04116b3..4505d3f 100644 --- a/ventures/gandora/index.html +++ b/ventures/gandora/index.html @@ -198,7 +198,7 @@ input,textarea{font:inherit;color:inherit} -
+
@@ -492,13 +492,25 @@ async function listStations() { row.append(d); } } -async function openStation(seed, { id, name = "", taste, history, played } = {}) { +/* links: the URL follows the station (#seed); a share link carries the current track's sheet music (?t=) so the same + song opens and renders for whoever follows it, even with nothing stored anywhere */ +const b64u = (s) => btoa(unescape(encodeURIComponent(s))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); +const unb64u = (s) => decodeURIComponent(escape(atob(String(s).replace(/-/g, "+").replace(/_/g, "/") + "===".slice((s.length + 3) % 4)))); +function shareLink() { + const s = state.current && state.current.spec; if (!s) return ""; + const t = { seed: s.seed, station: state.station, title: s.title, tags: s.tags, lyrics: s.lyrics || "", bpm: s.bpm, key: s.key, energy: s.energy, line: s.line || "" }; + return `${location.origin}/?t=${b64u(JSON.stringify(t))}`; +} +function setUrl() { try { history.replaceState(null, "", state.seed ? "/#" + encodeURIComponent(state.seed).replace(/%20/g, "+") : "/"); } catch {} } +async function openStation(seed, { id, name = "", taste, history, played, first } = {}) { if (state.stationId !== id) { state.audio.pause(); state.playing = false; setPlayIcon(); state.current = null; state.nextJob = null; state.recent = []; state.pos = -1; } state.seed = seed; state.station = name; state.stationId = id || `s-${Date.now()}-${hash32(seed)}`; state.taste = taste || { like: [], avoid: [], notes: [] }; state.history = history || []; state.played = played || 0; $("#stationName").textContent = name || seed; $("#seed").classList.add("hidden"); $("#player").classList.remove("hidden"); + setUrl(); if (!state.current) { + if (first) { first.seed = seed; first.made = Date.now(); state.nextJob = makeJob(first); drawCover($("#cover canvas"), first, { lit: 0.6 }); $("#title").textContent = first.title; saveStation().catch(() => {}); advance(); return; } drawCover($("#cover canvas"), { seed, title: "", tags: "", bpm: 100, key: KEYS[hash32(seed) % 12], energy: 0.5 }, { lit: 0.6 }); $("#title").textContent = "tuning…"; $("#tags").textContent = ""; $("#lyric").textContent = ""; keepLabel(); saveStation().catch(() => {}); @@ -652,7 +664,14 @@ async function keepLabel() { if (!cur || !cur.id) { b.disabled = true; b.classList.remove("on"); b.textContent = "➕ KEEP"; return; } const row = (await db.all()).find((t) => t.id === cur.id); b.disabled = false; b.classList.toggle("on", !!(row && row.kept)); b.textContent = row && row.kept ? "✓ KEPT" : "➕ KEEP"; + $("#share").disabled = false; } +$("#share").addEventListener("click", async () => { + const url = shareLink(); if (!url) return; + const s = state.current.spec, text = `${s.title} · a gandora track from the ${state.station || s.seed} station`; + if (navigator.share) { try { await navigator.share({ title: s.title, text, url }); return; } catch {} } + try { await navigator.clipboard.writeText(url); toast("Link copied. Whoever opens it gets this song, freshly rendered."); } catch { prompt("Copy this link:", url); } +}); $("#keep").addEventListener("click", async () => { const cur = state.current && state.current.spec; if (!cur || !cur.id || !state.current.blob) return; const row = (await db.all()).find((t) => t.id === cur.id); @@ -807,8 +826,12 @@ $("#tasteBtn").addEventListener("click", () => openTaste().catch((e) => toast(e. $("#tasteClose").addEventListener("click", () => $("#taste").classList.add("hidden")); listStations().catch(() => {}); -// first paint of the seed mark, for a visitor who lands with a hash like #rainy+trains -if (location.hash.length > 1) { $("#seedIn").value = decodeURIComponent(location.hash.slice(1)).replace(/\+/g, " "); $("#seedIn").dispatchEvent(new Event("input")); } +// a shared track (?t=): open its station and render that song first; a hash (#rainy+trains) just fills the seed +const sharedT = new URLSearchParams(location.search).get("t"); +if (sharedT) { + try { const t = JSON.parse(unb64u(sharedT)); if (t && t.seed && t.title) { openStation(t.seed, { name: t.station || "", first: { title: t.title, tags: t.tags, lyrics: t.lyrics || "", bpm: t.bpm, key: t.key, energy: t.energy, line: t.line || "", station: t.station || "" } }); } } + catch { toast("That link didn't carry a track."); } +} else if (location.hash.length > 1) { $("#seedIn").value = decodeURIComponent(location.hash.slice(1)).replace(/\+/g, " "); $("#seedIn").dispatchEvent(new Event("input")); }