"use strict"; const ICONS = { play: ``, pause: ``, next: ``, prev: ``, heart: ``, heartFill: ``, home: ``, search: ``, library: ``, grid: ``, radio: ``, shuffle: ``, repeat: ``, volume: ``, volumeX: ``, queue: ``, plus: ``, x: ``, trash: ``, pencil: ``, dots: ``, music: ``, clock: ``, sparkle: ``, check: ``, chevron: `` }; function icon(name, size) { const s = size || 20; const solid = ["play", "pause", "next", "prev", "heartFill", "grid", "sparkle"]; const svg = `${ICONS[name]}`; return svg; } function $(sel, root) { return (root || document).querySelector(sel); } function esc(s) { return String(s == null ? "" : s).replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); } function fmt(sec) { sec = Math.max(0, Math.floor(sec || 0)); const m = Math.floor(sec / 60); const s = sec % 60; return m + ":" + (s < 10 ? "0" : "") + s; } function fmtNum(n) { if (n >= 1e9) return (n / 1e9).toFixed(1).replace(/\.0$/, "") + "B"; if (n >= 1e6) return (n / 1e6).toFixed(1).replace(/\.0$/, "") + "M"; if (n >= 1e3) return (n / 1e3).toFixed(1).replace(/\.0$/, "") + "K"; return String(n); } function fmtDurTotal(sec) { const m = Math.round(sec / 60); const h = Math.floor(m / 60); return h ? h + " hr " + (m % 60) + " min" : m + " minutes"; } function eqIcon() { return ``; } const ctxMap = {}; let prevRoute = ""; Player.onState = function (playing) { updatePlayerUI(); updateRows(); }; Player.onTime = function (cur, dur) { const pct = dur > 0 ? (cur / dur) * 100 : 0; $("#pb-fill").style.width = pct + "%"; $("#pb-cur").textContent = fmt(cur); $("#pb-dur").textContent = fmt(dur || Player.current()?.dur || 0); $("#now-fill").style.width = pct + "%"; $("#now-cur").textContent = fmt(cur); $("#now-dur").textContent = fmt(dur || Player.current()?.dur || 0); }; Player.onTrack = function (song) { updatePlayerUI(); updateRows(); updateNow(); updateQueue(); document.title = song ? song.title + " — " + song.artist + " · Music" : "Music — Listen Now"; }; function updateRows() { const rows = document.querySelectorAll(".song-row"); rows.forEach((row) => { const s = Player.current(); const cur = s && row.dataset.id === s.id; row.classList.toggle("is-cur", !!cur); const idx = row.querySelector(".row-idx"); const rank = row.querySelector(".row-rank"); if (idx) idx.innerHTML = cur ? eqIcon() : esc(row.dataset.idxText || ""); if (rank) rank.innerHTML = cur ? eqIcon() : esc(row.dataset.idxText || ""); }); } function updatePlayerUI() { const cur = Player.current(); const playBtn = $("#pb-play"); const playBtnNow = $("#now-play"); const iconPlay = playBtn ? icon(Player.playing ? "pause" : "play", 20) : ""; const iconPlayNow = playBtnNow ? icon(Player.playing ? "pause" : "play", 44) : ""; if (playBtn) playBtn.innerHTML = iconPlay; if (playBtnNow) playBtnNow.innerHTML = iconPlayNow; if (cur) { $("#pb-art-img").src = Art.song(cur); $("#pb-title").textContent = cur.title; $("#pb-sub").textContent = cur.artist; $("#pb-art").classList.toggle("has-track", true); const heart = $("#pb-heart"); if (heart) { heart.classList.toggle("liked", Store.isLiked(cur.id)); heart.innerHTML = icon(Store.isLiked(cur.id) ? "heartFill" : "heart", 18); heart.dataset.id = cur.id; } } else { $("#pb-art").classList.remove("has-track"); $("#pb-title").textContent = "Nothing playing"; $("#pb-sub").textContent = "Pick a song to start"; $("#pb-art-img").src = Art.placeholder(); } $("#pb-shuffle").classList.toggle("on", Player.shuffle); $("#now-shuffle").classList.toggle("on", Player.shuffle); const rep = Player.repeat; $("#pb-repeat").classList.toggle("on", rep !== 0); $("#pb-repeat").classList.toggle("one", rep === 2); $("#pb-repeat").innerHTML = icon("repeat", 18) + (rep === 2 ? '1' : ""); $("#now-repeat").classList.toggle("on", rep !== 0); $("#now-repeat").classList.toggle("one", rep === 2); $("#now-repeat").innerHTML = icon("repeat", 20) + (rep === 2 ? '1' : ""); const muted = Player.audio.volume === 0; $("#pb-mute").innerHTML = icon(muted ? "volumeX" : "volume", 18); $("#now-mute").innerHTML = icon(muted ? "volumeX" : "volume", 20); $("#pb-vol-fill").style.width = Player.volume * 100 + "%"; $("#now-vol-fill").style.width = Player.volume * 100 + "%"; $("#pb-fill").style.width = (Player.dur > 0 ? (Player.cur / Player.dur) * 100 : 0) + "%"; $("#pb-cur").textContent = fmt(Player.cur); $("#pb-dur").textContent = fmt(Player.dur || (cur ? cur.dur : 0)); $("#now-fill").style.width = (Player.dur > 0 ? (Player.cur / Player.dur) * 100 : 0) + "%"; $("#now-cur").textContent = fmt(Player.cur); $("#now-dur").textContent = fmt(Player.dur || (cur ? cur.dur : 0)); } function navigate(hash) { if (location.hash === hash) { route(); } else { location.hash = hash; } window.scrollTo && $("#main").scrollTo({ top: 0 }); $("#main").scrollTop = 0; } function route() { closeMenu(); const raw = location.hash.replace(/^#\/?/, ""); const [path, query] = raw.split("?"); const parts = path.split("/").filter(Boolean); const qs = query ? new URLSearchParams(query) : null; const view = $("#view"); let html = ""; const root = parts[0] || "home"; const fresh = root !== "search" || prevRoute !== "search"; prevRoute = root; if (root === "home" || root === "") html = viewHome(); else if (root === "browse") html = viewBrowse(); else if (root === "radio") html = viewRadio(); else if (root === "search") html = viewSearch(qs ? qs.get("q") || "" : ""); else if (root === "liked") html = viewLiked(); else if (root === "album" && parts[1]) html = viewAlbum(parts[1]); else if (root === "artist" && parts[1]) html = viewArtist(parts[1]); else if (root === "playlist" && parts[1]) html = viewPlaylist(parts[1]); else if (root === "station" && parts[1]) html = viewStation(parts[1]); else if (root === "genre" && parts[1]) html = viewGenre(decodeURIComponent(parts[1])); else if (root === "library") html = viewLibrary(parts[1] || "playlists"); else html = viewHome(); view.innerHTML = html; setActiveNav(root, parts[1]); afterRender(fresh); } function setActiveNav(root, sub) { const key = root === "library" ? sub : root; document.querySelectorAll(".nav-item").forEach((el) => { el.classList.toggle("active", el.dataset.nav === key); }); } function afterRender(fresh) { const input = $("#search-input"); if (input && !input.dataset.bound) { input.dataset.bound = "1"; input.addEventListener("input", () => { clearTimeout(input._t); input._t = setTimeout(() => { navigate("#/search?q=" + encodeURIComponent(input.value.trim())); }, 180); }); input.addEventListener("keydown", (e) => { if (e.key === "Enter") { const q = input.value.trim(); if (!q) return; const res = D.search(q); const s = res.songs[0]; if (s) { const ctx = "search:" + q.toLowerCase(); ctxMap[ctx] = res.songs.map((x) => x.id); Player.playContext(ctxMap[ctx], 0); } } }); } if (fresh && input) { input.focus(); } } function renderSidebar() { const pl = Store.playlists; $("#sidebar").innerHTML = ` `; } function renderPlayer() { $("#player").innerHTML = `
${eqIcon()}
Nothing playing
Pick a song to start
0:00
0:00
`; } function renderNow() { $("#now").innerHTML = `
Nothing playing
0:00
0:00
`; } function updateNow() { const cur = Player.current(); if (!cur) { $("#now-title").textContent = "Nothing playing"; $("#now-artist").textContent = ""; $("#now-art").src = Art.placeholder(); $("#now-bg").src = Art.placeholder(); $("#now-art-inner").classList.remove("live"); return; } const art = Art.song(cur); $("#now-art").src = art; $("#now-bg").src = art; $("#now-title").textContent = cur.title; $("#now-artist").textContent = cur.artist + " — " + cur.album; $("#now-heart").classList.toggle("liked", Store.isLiked(cur.id)); $("#now-heart").innerHTML = icon(Store.isLiked(cur.id) ? "heartFill" : "heart", 24); $("#now-heart").dataset.id = cur.id; $("#now-art-inner").classList.toggle("live", Player.playing); } function renderQueue() { const q = Player.queue; const cur = Player.current(); $("#qpanel").innerHTML = `
${cur ? esc(cur.title) : "Nothing playing"}
${cur ? esc(cur.artist) : "—"}
Up Next · ${q.length} ${q.length === 1 ? "song" : "songs"}
${q.length ? q.map((s, i) => `
${i === Player.index ? eqIcon() : ""}${esc(s.title)}
${esc(s.artist)}
`).join("") : `
Queue is empty.
Play something to fill it.
`}
`; } function updateQueue() { renderQueue(); } function toggleQueue(open) { const force = open != null ? open : !$("#qpanel").classList.contains("open"); $("#qpanel").classList.toggle("open", force); if (force) renderQueue(); } function toggleNow(open) { const force = open != null ? open : !$("#now").classList.contains("open"); $("#now").classList.toggle("open", force); if (force) { Player.ensureGraph(); Player.resumeGraph(); updateNow(); requestAnimationFrame(vizLoop); } } const PALETTES = [ ["#fa233b", "#fb5c74", "#2d0a12"], ["#8338ec", "#ff4d6d", "#150a2e"], ["#06d6a0", "#073b4c", "#0f2e26"], ["#fca311", "#e5383b", "#241505"], ["#3a86ff", "#8338ec", "#0a1b3d"], ["#ffbe0b", "#ff006e", "#2e1208"] ]; function genrePalette(genre) { const h = hashStr(genre); return PALETTES[h % PALETTES.length]; } function heroBg(palette) { return `background-image: radial-gradient(120% 130% at 12% 8%, ${palette[0]}e6, transparent 55%), radial-gradient(120% 130% at 88% 95%, ${palette[2] || palette[0]}cc, transparent 55%), linear-gradient(150deg, #101014, ${palette[1]}44);`; } function songCard(s, ctx, index) { return `
${esc(s.title)}
${esc(s.artist)}
`; } function albumCard(a) { return `
${esc(a.title)}
${esc(D.artist(a.artistId).name)}
`; } function mixCard(m) { return `
${esc(m.name)}
${esc(m.desc)}
`; } function stationCard(st) { return `
${esc(st.name)}
${esc(st.desc)}
`; } function artistCard(a) { return `
${esc(a.name)}
${esc(a.genre)}
`; } function plCard(p) { const art = svgURI(artSVG([p.color, "#141417", p.color], hashStr(p.id), ["mesh", "orbital", "rings"][hashStr(p.id) % 3])); return `
${esc(p.name)}
${p.items.length} ${p.items.length === 1 ? "song" : "songs"}
`; } function songRow(s, ctx, index, mode) { const cur = Player.current(); const isCur = cur && cur.id === s.id; const like = Store.isLiked(s.id); if (mode === "num") { return `
${isCur ? eqIcon() : index + 1}
${esc(s.title)}
${esc(s.artist)}
${esc(s.album)}
${fmt(s.dur)}
`; } return `
${esc(s.title)}
${esc(s.artist)}
${esc(s.album)}
${fmt(s.dur)}
`; } function chartRow(s, rank, ctx, index) { const cur = Player.current(); const isCur = cur && cur.id === s.id; const like = Store.isLiked(s.id); return `
${isCur ? eqIcon() : rank}
${esc(s.title)}
${esc(s.artist)}
${fmtNum(s.plays)} plays
${fmt(s.dur)}
`; } function section(title, inner, seeAll) { return `

${title}

${seeAll ? `See All` : ""}
${inner}
`; } function viewHome() { const feat = D.featuredAlbum(); const featAlbum = D.album(feat.id); const ctxAlbum = "album:" + feat.id; ctxMap[ctxAlbum] = D.albumSongs(feat.id).map((s) => s.id); const recent = Store.recentSongs().slice(0, 10); const recCtx = "recent"; ctxMap[recCtx] = recent.map((s) => s.id); const charts = D.topCharts(); const chartCtx = "charts"; ctxMap[chartCtx] = charts.map((s) => s.id); const newRel = D.newReleases(); return `
Featured Album

${esc(feat.title)}

${esc(featAlbum.artist)} · ${feat.year} · ${esc(feat.genre)}
${fmtNum(D.albumPlays(feat))} plays · ${fmtDurTotal(D.albumSongs(feat.id).reduce((t, s) => t + s.dur, 0))}
${recent.length ? section("Recently Played", recent.map((s, i) => songCard(s, recCtx, i)).join("")) : ""} ${section("Made For You", D.mixes.map(mixCard).join(""))}

Top Charts

${charts.map((s, i) => chartRow(s, i + 1, chartCtx, i)).join("")}
${section("New Releases", newRel.map(albumCard).join(""), "#/browse")} `; } function viewBrowse() { const genres = Array.from(new Set(D.songs.map((s) => s.genre))).sort(); const charts = D.topCharts(); const ctx = "charts"; ctxMap[ctx] = charts.map((s) => s.id); return ` ${section("Genres", genres.map((g) => { const p = genrePalette(g); return `
${esc(g)}
`; }).join(""))}

Top Charts

${charts.map((s, i) => chartRow(s, i + 1, ctx, i)).join("")}
${section("All Albums", D.albums.slice().sort((a, b) => a.title.localeCompare(b.title)).map(albumCard).join(""))} `; } function viewRadio() { return `
${icon("radio", 22)}
Always On

Music Radio

Nonstop stations across every genre. Hit play and let it run.
${section("Stations", D.stations.map(stationCard).join(""))} `; } function viewSearch(q) { const res = D.search(q); const ctx = "search:" + q.toLowerCase(); ctxMap[ctx] = res.songs.map((s) => s.id); const top = res.songs[0]; return `
${!q ? `

Search for something

Try “Aurora”, “Neon”, “jazz”, or an album like “Velvet Hours”.

` : !res.songs.length && !res.albums.length && !res.artists.length ? `

No results for “${esc(q)}”

Check the spelling or try something else.

` : ` ${top ? `

Top Result

${esc(top.title)}
${esc(top.artist)} · ${esc(top.album)}
` : ""} ${res.songs.length ? `

Songs

${res.songs.map((s, i) => songRow(s, ctx, i, "art")).join("")}
` : ""} ${res.albums.length ? section("Albums", res.albums.map(albumCard).join("")) : ""} ${res.artists.length ? section("Artists", res.artists.map(artistCard).join("")) : ""} `} `; } function viewAlbum(id) { const album = D.album(id); if (!album) return ""; const artist = D.artist(album.artistId); const songs = D.albumSongs(id).map((s) => D.song(s.id)); const ctx = "album:" + id; ctxMap[ctx] = songs.map((s) => s.id); const totalDur = songs.reduce((t, s) => t + s.dur, 0); return `
Album

${esc(album.title)}

${esc(artist.name)} · ${album.year} · ${esc(album.genre)}
${fmtNum(D.albumPlays(album))} plays · ${fmtDurTotal(totalDur)}
${songs.map((s, i) => songRow(s, ctx, i, "num")).join("")}
${section("More by " + artist.name, D.artistAlbums(artist.id).filter((a) => a.id !== id).map(albumCard).join(""))} `; } function viewArtist(id) { const artist = D.artist(id); if (!artist) return ""; const songs = D.artistSongs(id).slice(0, 8).map((s) => D.song(s.id)); const ctx = "artist:" + id; ctxMap[ctx] = D.artistSongs(id).map((s) => s.id); return `
Artist

${esc(artist.name)}

${esc(artist.genre)}
${fmtNum(artist.followers)} monthly listeners
${esc(artist.bio)}
${songs.map((s, i) => songRow(s, ctx, i, "art")).join("")}
${section("Albums", D.artistAlbums(id).map(albumCard).join(""))} `; } function viewPlaylist(id) { const isMix = id.startsWith("mix-"); if (isMix) { const mix = D.mixes.find((m) => m.id === id); if (!mix) return ""; const songs = D.mixSongs(mix); const ctx = "mix:" + id; ctxMap[ctx] = songs.map((s) => s.id); return `
Made For You

${esc(mix.name)}

${esc(mix.desc)}
${songs.length} songs · ${fmtDurTotal(songs.reduce((t, s) => t + s.dur, 0))}
${songs.map((s, i) => songRow(s, ctx, i, "art")).join("")}
`; } const pl = Store.playlists.find((p) => p.id === id); if (!pl) return ""; const songs = Store.playlistSongs(id); const ctx = "playlist:" + id; ctxMap[ctx] = songs.map((s) => s.id); const art = svgURI(artSVG([pl.color, "#141417", pl.color], hashStr(pl.id), ["mesh", "orbital", "rings"][hashStr(pl.id) % 3])); return `
Playlist

${esc(pl.name)}

${songs.length} ${songs.length === 1 ? "song" : "songs"} · created ${new Date(pl.createdAt).toLocaleDateString(undefined, { month: "long", year: "numeric" })}
${songs.length ? `
${songs.map((s, i) => { const row = songRow(s, ctx, i, "art"); return row; }).join("")}
` : `
${icon("music", 30)}

This playlist is empty

Tap ${icon("dots", 14)} on any song and choose “Add to Playlist”.

`} `; } function viewStation(id) { const st = D.stations.find((s) => s.id === id); if (!st) return ""; const songs = D.stationSongs(st).sort((a, b) => b.plays - a.plays); const ctx = "station:" + id; ctxMap[ctx] = songs.map((s) => s.id); return `
Radio Station

${esc(st.name)}

${esc(st.desc)}
${songs.length} tracks in rotation
${songs.slice(0, 25).map((s, i) => songRow(s, ctx, i, "art")).join("")}
`; } function viewGenre(genre) { const albums = D.genreAlbums(genre); const songs = D.genreOf(genre).sort((a, b) => b.plays - a.plays); const ctx = "genre:" + genre; ctxMap[ctx] = songs.map((s) => s.id); const p = genrePalette(genre); return `
${icon("sparkle", 24)}
Genre

${esc(genre)}

${songs.length} songs · ${fmtNum(songs.reduce((t, s) => t + s.plays, 0))} total plays
${songs.slice(0, 15).map((s, i) => songRow(s, ctx, i, "art")).join("")}
${section("Albums", albums.map(albumCard).join(""))} `; } function viewLiked() { const songs = Store.likedSongs().sort((a, b) => a.title.localeCompare(b.title)); const ctx = "liked"; ctxMap[ctx] = songs.map((s) => s.id); return `
${icon("heartFill", 90)}
Playlist

Liked Songs

Every song you've hearted, in one place.
${songs.length} ${songs.length === 1 ? "song" : "songs"}
${songs.length ? `
${songs.map((s, i) => songRow(s, ctx, i, "art")).join("")}
` : `
${icon("heart", 30)}

No liked songs yet

Tap the heart on any song to save it here.

`} `; } function viewLibrary(tab) { const tabs = [ ["playlists", "Playlists"], ["artists", "Artists"], ["albums", "Albums"], ["songs", "Songs"] ]; let body = ""; if (tab === "playlists") { const pls = Store.playlists; body = ` ${pls.length ? `
${pls.map(plCard).join("")}
` : `
${icon("queue", 30)}

No playlists yet

Create one and start collecting songs.

`}`; } else if (tab === "artists") { body = `
${D.artists.map(artistCard).join("")}
`; } else if (tab === "albums") { body = `
${D.albums.slice().sort((a, b) => a.title.localeCompare(b.title)).map(albumCard).join("")}
`; } else { const songs = D.songs.slice().sort((a, b) => a.title.localeCompare(b.title)).map((s) => D.song(s.id)); const ctx = "libsongs"; ctxMap[ctx] = songs.map((s) => s.id); body = `
${songs.map((s, i) => songRow(s, ctx, i, "art")).join("")}
`; } return `

Library

${tabs.map(([t, label]) => ``).join("")}
${body} `; } function showToast(msg) { const t = $("#toast"); t.innerHTML = msg; t.classList.add("show"); clearTimeout(showToast._t); showToast._t = setTimeout(() => t.classList.remove("show"), 2200); } function openModal(html) { $("#modal-root").innerHTML = ` `; const input = $(".modal input"); if (input) input.focus(); } function closeModal() { $("#modal-root").innerHTML = ""; } function playlistForm(pl) { const colors = ["#fa233b", "#fb5c74", "#8338ec", "#3a86ff", "#06d6a0", "#fca311", "#ff006e", "#e09f3e"]; return `

${pl ? "Rename Playlist" : "New Playlist"}

${colors.map((c, i) => ``).join("")}
`; } function openMenu(x, y, items) { closeMenu(); const root = $("#menu-root"); root.innerHTML = ``; const menu = $(".menu", root); const mw = menu.offsetWidth, mh = menu.offsetHeight; const px = Math.min(x, window.innerWidth - mw - 12); const py = Math.min(y, window.innerHeight - mh - 110); menu.style.left = px + "px"; menu.style.top = py + "px"; menu._items = items; menu._menuEl = menu; root.style.pointerEvents = "auto"; } function closeMenu() { $("#menu-root").innerHTML = ""; } function bindBar(bar, cb) { bar.addEventListener("pointerdown", (e) => { if (e.button && e.button !== 0) return; e.preventDefault(); const move = (ev) => { const r = bar.getBoundingClientRect(); cb(Math.max(0, Math.min(1, (ev.clientX - r.left) / r.width))); }; move(e); const onMove = (ev) => move(ev); const onUp = () => { window.removeEventListener("pointermove", onMove); window.removeEventListener("pointerup", onUp); }; window.addEventListener("pointermove", onMove); window.addEventListener("pointerup", onUp); }); } let vizRaf = 0; function vizLoop() { const canvas = $("#now-viz"); if (!canvas || !$("#now").classList.contains("open")) return; vizRaf = requestAnimationFrame(vizLoop); const wrap = canvas.parentElement; const w = wrap.clientWidth, h = wrap.clientHeight; if (!w || !h) return; if (canvas.width !== w) canvas.width = w; if (canvas.height !== h) canvas.height = h; const g = canvas.getContext("2d"); g.clearRect(0, 0, w, h); let data = null; let sum = 0; if (Player.analyser) { Player.analyser.getByteFrequencyData(Player.freq); data = Player.freq; for (let i = 0; i < data.length; i++) sum += data[i]; } const t = performance.now() / 1000; const n = 48; const bw = w / n; for (let i = 0; i < n; i++) { let v = 0.06; if (data && sum > 0) { v = data[Math.floor((i / n) * data.length * 0.8)] / 255; } else if (Player.playing) { v = Math.max(0.08, 0.45 + 0.4 * Math.sin(t * 3 + i * 0.55) * Math.sin(t * 1.7 + i * 0.23)); } const hh = Math.max(3, v * h * 0.9); const x = i * bw + bw * 0.25; const grad = g.createLinearGradient(0, h, 0, 0); grad.addColorStop(0, "rgba(250,35,59,0.05)"); grad.addColorStop(1, "rgba(251,92,116,0.9)"); g.fillStyle = grad; g.beginPath(); g.roundRect(x, h - hh, bw * 0.5, hh, bw * 0.25); g.fill(); } } function initEvents() { document.addEventListener("click", (e) => { const menuBtn = e.target.closest("[data-midx]"); if (menuBtn) { const menu = $(".menu", $("#menu-root")); if (menu && menu._items) { menu._items[+menuBtn.dataset.midx].run(e); } closeMenu(); return; } const nav = e.target.closest("[data-go]"); if (nav) { e.preventDefault(); navigate(nav.dataset.go); return; } const actionEl = e.target.closest("[data-action]"); if (!actionEl) { if (!e.target.closest(".menu")) closeMenu(); return; } const act = actionEl.dataset.action; const id = actionEl.dataset.id; const ctx = actionEl.dataset.ctx; const index = actionEl.dataset.index; if (act === "play") { if (ctxMap[ctx]) Player.playContext(ctxMap[ctx], +index || 0); } else if (act === "play-ctx") { if (ctxMap[ctx]) Player.playContext(ctxMap[ctx], 0); } else if (act === "shuffle-ctx") { if (ctxMap[ctx]) { const i = Math.floor(Math.random() * ctxMap[ctx].length); Player.playContext(ctxMap[ctx], i); } } else if (act === "play-station") { const st = D.stations.find((s) => s.id === id); if (!st) return; const songs = D.stationSongs(st).map((s) => s.id); const i = Math.floor(Math.random() * songs.length); Player.playContext(songs, i); } else if (act === "play-pl") { const pl = Store.playlists.find((p) => p.id === id); if (pl && pl.items.length) { Player.playContext(pl.items, 0); } } else if (act === "toggle") { Player.togglePlay(); } else if (act === "next") { Player.next(); } else if (act === "prev") { Player.prev(); } else if (act === "shuffle") { Player.toggleShuffle(); updatePlayerUI(); renderQueue(); } else if (act === "repeat") { Player.cycleRepeat(); updatePlayerUI(); renderQueue(); } else if (act === "mute") { Player.toggleMute(); updatePlayerUI(); } else if (act === "like") { if (!id) return; const liked = Store.toggleLike(id); Store.save(); updatePlayerUI(); updateNow(); route(); showToast(liked ? "Added to Liked Songs" : "Removed from Liked Songs"); } else if (act === "more") { const s = D.song(id); if (!s) return; const inUserPlaylist = /^#\/playlist\/pl/.test(location.hash); const menuItems = [ { label: "Play Next", icon: "play", run: () => { const q = Player.queue; if (!q.length) { Player.playContext([s], 0); } else { q.splice(Player.index + 1, 0, s); renderQueue(); showToast("Added to Up Next"); } } }, { label: "Add to Playlist…", icon: "plus", run: () => { const pls = Store.playlists; const items = pls.map((p) => ({ label: p.name + (p.items.includes(s.id) ? " ✓" : ""), icon: "queue", run: () => { if (Store.addToPlaylist(p.id, s.id)) showToast('Added to "' + p.name + '"'); else showToast('Already in "' + p.name + '"'); } })); items.push({ sep: true }, { label: "New Playlist…", icon: "plus", run: () => openModal(playlistForm(null)) }); const mx = Math.min(e.clientX, window.innerWidth - 220); const my = Math.min(e.clientY, window.innerHeight - 260); openMenu(mx, my, items); } }, { sep: true }, { label: Store.isLiked(s.id) ? "Remove from Liked" : "Like", icon: Store.isLiked(s.id) ? "heart" : "heart", run: () => { Store.toggleLike(s.id); Store.save(); route(); } }, { label: "Go to Album", icon: "grid", run: () => navigate("#/album/" + s.albumId) }, { label: "Go to Artist", icon: "music", run: () => navigate("#/artist/" + s.artistId) } ]; if (inUserPlaylist) { menuItems.splice(1, 0, { sep: true }, { label: "Remove from Playlist", icon: "trash", danger: true, run: () => { const plId = location.hash.replace(/^#\/playlist\//, ""); Store.removeFromPlaylist(plId, s.id); route(); showToast("Removed from playlist"); } }); } openMenu(e.clientX, e.clientY, menuItems); } else if (act === "open-now") { toggleNow(true); } else if (act === "close-now") { toggleNow(false); } else if (act === "toggle-queue") { toggleQueue(); } else if (act === "close-queue") { toggleQueue(false); } else if (act === "queue-jump") { Player.index = +index; Player.loadCurrent(); } else if (act === "queue-remove") { Player.removeAt(+index); renderQueue(); updatePlayerUI(); } else if (act === "open-album") { navigate("#/album/" + id); } else if (act === "open-artist") { navigate("#/artist/" + id); } else if (act === "open-playlist") { navigate("#/playlist/" + id); } else if (act === "open-station") { navigate("#/station/" + id); } else if (act === "open-genre") { navigate("#/genre/" + encodeURIComponent(actionEl.dataset.genre)); } else if (act === "open-library") { navigate("#/library/" + actionEl.dataset.tab); } else if (act === "clear-search") { navigate("#/search"); } else if (act === "create-playlist") { openModal(playlistForm(null)); } else if (act === "rename-playlist") { const pl = Store.playlists.find((p) => p.id === id); if (pl) openModal(playlistForm(pl)); } else if (act === "clear-playlist") { Store.clearPlaylist(id); route(); showToast("Playlist cleared"); } else if (act === "modal-close") { closeModal(); } else if (act === "modal-create-save") { const name = $(".modal-input").value.trim(); const color = $(".swatch.sel").dataset.color; const pl = Store.createPlaylist(name, color); closeModal(); renderSidebar(); showToast('Playlist "' + pl.name + '" created'); navigate("#/playlist/" + pl.id); } else if (act === "modal-rename-save") { const name = $(".modal-input").value.trim(); Store.renamePlaylist(id, name); closeModal(); renderSidebar(); route(); showToast("Playlist renamed"); } }); document.addEventListener("click", (e) => { const sw = e.target.closest(".swatch"); if (sw) { $(".swatch.sel") && $(".swatch.sel").classList.remove("sel"); sw.classList.add("sel"); } }); const modalRoot = $("#modal-root"); modalRoot.addEventListener("click", (e) => { if (e.target.classList.contains("modal-backdrop")) closeModal(); }); bindBar($("#pb-bar"), (r) => Player.seek(r * Player.dur)); bindBar($("#pb-vol"), (r) => Player.setVolume(r)); bindBar($("#now-bar"), (r) => Player.seek(r * Player.dur)); bindBar($("#now-vol"), (r) => Player.setVolume(r)); document.addEventListener("keydown", (e) => { const typing = /^(INPUT|TEXTAREA)$/.test(document.activeElement.tagName); if (e.key === "Escape") { closeMenu(); closeModal(); toggleQueue(false); toggleNow(false); return; } if ((e.ctrlKey || e.metaKey) && (e.key === "k" || e.key === "K")) { e.preventDefault(); navigate("#/search"); return; } if (typing) return; if (e.code === "Space") { e.preventDefault(); Player.togglePlay(); } else if (e.key === "ArrowRight") { Player.seek(Player.cur + 10); } else if (e.key === "ArrowLeft") { Player.seek(Player.cur - 10); } else if (e.key === "ArrowUp") { Player.setVolume(Player.volume + 0.05); updatePlayerUI(); } else if (e.key === "ArrowDown") { Player.setVolume(Player.volume - 0.05); updatePlayerUI(); } else if (e.key === "n" || e.key === "N") { Player.next(); } else if (e.key === "p" || e.key === "P") { Player.prev(); } else if (e.key === "l" || e.key === "L") { const cur = Player.current(); if (cur) { Store.toggleLike(cur.id); Store.save(); updatePlayerUI(); updateNow(); updateRows(); showToast(Store.isLiked(cur.id) ? "Added to Liked Songs" : "Removed from Liked Songs"); } } else if (e.key === "m" || e.key === "M") { Player.toggleMute(); updatePlayerUI(); } else if (e.key === "q" || e.key === "Q") { toggleQueue(); } }); window.addEventListener("hashchange", route); } function init() { renderSidebar(); renderPlayer(); renderNow(); initEvents(); updatePlayerUI(); route(); } init();