1import { siteName } from './site.mjs'; 2import { codeHash, endReplay, fetchPerformance, PERFORMANCE_PARAM } from './performance.mjs'; 3import { LISTENER_PARAM, listenerParam } from './listeners.mjs'; 4import { armSandboxReplay, playInSandbox, setOwnCode, showForeign } from './sandbox.mjs'; 5 6// The browser side of the song index: index.astro embeds loadSongs() as 7// JSON in #jev-songs, and the REPL reads it back from the page. 8 9// The song the home page opens on: selected on the jev panel, loaded into 10// the editor, and played once audio is allowed (see useReplContext). 11export const featuredSongId = 'jev/lightning-in-a-bottle'; 12 13let songsCache; 14export function readSongs() { 15 if (songsCache === undefined) { 16 const el = typeof document !== 'undefined' && document.getElementById('jev-songs'); 17 songsCache = el ? JSON.parse(el.textContent) : []; 18 } 19 return songsCache; 20} 21 22// The song this page opens on: a song's own page (/songs/<theme>/<song>/) 23// names it in a jev-song meta tag (ReplPage.astro); the home page opens on 24// the featured song. 25export function songPageId() { 26 const meta = typeof document !== 'undefined' && document.querySelector('meta[name="jev-song"]'); 27 return meta?.content || null; 28} 29export function startSongId() { 30 return songPageId() ?? featuredSongId; 31} 32 33// A listener's song link (/?listener=<id>) opens on that song instead, 34// fetched from the Worker when the editor starts (useReplContext). 35export function startSong() { 36 if (!songPageId() && listenerParam()) return undefined; 37 const id = startSongId(); 38 return readSongs().find((s) => s.id === id && s.code); 39} 40 41// The song in the editor, as the song browser, the radio and "is it art?" 42// see it: set when a song is loaded, and read against the editor's code, so 43// an edited song still counts as that song. 44let playingId = null; 45const playingListeners = new Set(); 46export const nowPlaying = { 47 subscribe(listener) { 48 playingListeners.add(listener); 49 return () => playingListeners.delete(listener); 50 }, 51 get: () => playingId, 52 set(id) { 53 playingId = id; 54 for (const listener of playingListeners) listener(); 55 }, 56}; 57 58// Songs are arranged with masks that count cycles from 0, so they must start 59// from stopped: the scheduler's stop() resets the cycle count, pause() does not. 60// A song picked here plays live: a performance being replayed ends 61// (performance.mjs). Only the site's own songs: a listener's has no `code` 62// (listeners.mjs) and plays in the sandbox (sandbox.mjs, playListenerSong). 63export async function playSong(editor, song) { 64 if (!editor || !song.code) return; 65 endReplay(); 66 showSong(song); 67 nowPlaying.set(song.id); 68 await editor.stop(); 69 setOwnCode(editor, song.code); 70 await editor.evaluate(); 71} 72 73// A listener's song (listeners.mjs): its code into the editor as foreign, 74// so it is evaluated only in the sandbox (sandbox.mjs), and, with `play`, 75// played there. With `replay`, a recorded performance's id: the sandbox's 76// jev()s play that take and ask Jev nothing, still in the sandbox (the 77// frame arms it; sandbox.mjs's armSandboxReplay). Resolves to { replaying: 78// { changed } } when it replays, or { error } when that take could not be 79// loaded (Jev then plays the song live), else {}. 80export async function loadListenerSong(editor, song, { play = false, replay = null } = {}) { 81 if (!editor || !song.foreignCode) return {}; 82 endReplay(); 83 let replaying = null; 84 let error; 85 if (replay) { 86 try { 87 const performance = await fetchPerformance(replay); 88 if (performance.song !== song.id) throw new Error(`that take is of ${performance.song}`); 89 replaying = { jevs: performance.jevs, changed: performance.codeHash !== (await codeHash(song.foreignCode)) }; 90 } catch (e) { 91 error = e.message; 92 } 93 } 94 showSong(song); 95 nowPlaying.set(song.id); 96 await editor.stop(); 97 // `code`: the song as written, so the recorder knows an edited one (JevPerformance.jsx) 98 showForeign(editor, song.foreignCode, { song: { title: song.title, author: song.author, id: song.id, code: song.foreignCode } }); 99 armSandboxReplay(song.id, replaying); 100 if (play) await playInSandbox(song.foreignCode); 101 return replaying ? { replaying: { changed: replaying.changed } } : error ? { error } : {}; 102} 103 104// A song's shareable address. Picking a song puts it in the address bar 105// (keeping the query, e.g. ?session=, but not a replayed ?performance=, 106// which that song does not play) and the tab title, so the link to copy is 107// always the song that is playing. A listener's song has no page of its 108// own (the site is built before it exists): its address is the home page 109// with ?listener=<id>. 110export const songPath = (song) => `/songs/${song.id}/`; 111export function showSong(song) { 112 document.title = `${song.title} - ${siteName}`; 113 try { 114 const query = new URLSearchParams(location.search); 115 query.delete(PERFORMANCE_PARAM); 116 query.delete(LISTENER_PARAM); 117 if (song.listenerId) query.set(LISTENER_PARAM, song.listenerId); 118 const search = query.size ? `?${query}` : ''; 119 history.replaceState(history.state, '', (song.listenerId ? '/' : songPath(song)) + search); 120 } catch { 121 // an embedded or sandboxed page may refuse; the song still plays 122 } 123} 124 125// Whether the song the page opens on plays by itself. Never on a reload: 126// the PWA's "update" reloads the page, and a song the visitor had stopped 127// must not start again on their next click. A song's own page is a link to 128// that song, so it plays whenever it is opened; the home page's featured 129// song plays once per tab (and the tools mark it played to keep it quiet). 130const AUTOPLAYED = 'jevstrudel.autoplayed'; 131const reloaded = () => performance.getEntriesByType?.('navigation')[0]?.type === 'reload'; 132export function takeAutoplay() { 133 if (reloaded()) return false; 134 if (songPageId()) return true; 135 try { 136 if (sessionStorage.getItem(AUTOPLAYED)) return false; 137 sessionStorage.setItem(AUTOPLAYED, '1'); 138 } catch { 139 // no storage: play, as a first visit would 140 } 141 return true; 142}