diff --git a/CHANGELOG.md b/CHANGELOG.md index d1257ca..6309ba1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and ### Changed +- Source Vault player: the dock now matches the site's player (borderless ▶ key, « » skips, title and chapter lines, the black bar), and the playing track's real waveform is the seek bar, with the times under it. The right-hand slot shows the tempo, key and Camelot code and opens a queue of all 20 files with their BPM; on phones it folds up like the site's dock. - End credits: the GitHub mark beside VIEW PROJECT → GITHUB now carries the magenta → cyan gradient, matching the GitHub mark in the footer bar. - README: new catalogue screenshot showing the Hyperpop · Metalcore · Glitchcore tags. diff --git a/src/vault/main.js b/src/vault/main.js index 32cc15e..7fff43b 100644 --- a/src/vault/main.js +++ b/src/vault/main.js @@ -615,10 +615,7 @@ function play(slug, at) { au.readyState >= 1 ? go() : au.addEventListener("loadedmetadata", go, { once: true }); } au.play().catch(() => {}); - $("#dockT").textContent = `${t.n} — ${t.title}`; - $("#dockS").textContent = `CHAPTER 0${t.ch} · ${t.chapter}`; - $("#dockChips").innerHTML = - `≈${t.bpm} BPM · ${t.key} ${t.camelot}`; + dockShow(t); const row = $(`.row[data-slug="${slug}"]`); if (row) buildSecs(row, t); } @@ -638,21 +635,33 @@ function syncPlaying() { r.classList.toggle("playing", me && (on || au.currentTime > 0)); $("[data-play]", r).innerHTML = me && on ? '' : ''; }); - $("#dockPlay").innerHTML = on ? '' : ''; + if (!dockT) return; + const k = $("#dockPlay"); + k.classList.toggle("on", on); + k.innerHTML = on + ? '' + : ''; + k.setAttribute("aria-label", `${on ? "Pause" : "Play"} ${dockT.title}`); + $("#dock").classList.toggle("playing", on); + $$("#dockQl .e-trk").forEach((b) => { + const me = b.dataset.go === dockT.slug; + b.setAttribute("aria-pressed", me && on); + $(".e-dock-n", b).textContent = me && on ? "\u25B6\uFE0E" : b.dataset.n; + }); } au.addEventListener("play", syncPlaying); au.addEventListener("pause", syncPlaying); -au.addEventListener("ended", () => { - const i = TRACKS.findIndex((t) => t.slug === cur); - play(TRACKS[(i + 1) % TRACKS.length].slug, 0); -}); +const step = (d) => { + const i = TRACKS.findIndex((t) => t.slug === dockT.slug); + play(TRACKS[(i + d + TRACKS.length) % TRACKS.length].slug, 0); +}; +au.addEventListener("ended", () => step(1)); au.addEventListener("timeupdate", () => { const t = TRACKS.find((x) => x.slug === cur); if (!t) return; const d = au.duration || t.duration, p = au.currentTime / d; - $("#dockProg").style.width = p * 100 + "%"; - $("#dockTm").textContent = `${fmt(au.currentTime)} / ${fmt(d)}`; + dockProgress(au.currentTime, d); const row = $(`.row[data-slug="${cur}"]`); if (!row) return; if (!$("[data-secs] button", row)) buildSecs(row, t); @@ -670,10 +679,96 @@ au.addEventListener("timeupdate", () => { `${esc(secs[si].name)} · FROM ≈${fmt(secs[si].start)}${secs[si].lines.length ? secs[si].lines.slice(0, 4).map(esc).join("
") : "(instrumental)"}`; } }); + +/* the dock: the site's player bar, with the shown track's real waveform as the seek bar */ +let dockT = null; +const DOCK_BARS = 150; +function dockShow(t) { + if (dockT === t) return; + dockT = t; + $("#dockT").textContent = `${t.n} — ${t.title}`; + $("#dockS").textContent = `CHAPTER 0${t.ch} · ${t.chapter}`; + $("#dockDur").textContent = t.durationText; + $("#dockBpm").textContent = `≈${t.bpm} BPM`; + $("#dockKey").textContent = t.key.toUpperCase(); + const c = $("#dockCam"); + c.textContent = t.camelot; + c.style.background = camColour(t.camelot); + $("#dockQbtn").setAttribute("aria-label", `Queue, ${t.n} / 20 · ≈${t.bpm} BPM · ${t.key} (${t.camelot})`); + $("#dockQpos").textContent = `${t.n} / 20`; + const w = $("#dockWave"); + w.setAttribute("aria-label", `Seek ${t.title}`); + w.innerHTML = + Array.from( + { length: DOCK_BARS }, + (_, i) => + ``, + ).join("") + ''; + $$("#dockQl .e-trk").forEach((b) => b.classList.toggle("cur", b.dataset.go === t.slug)); + dockProgress(0, t.duration); + syncPlaying(); +} +function dockProgress(at, d) { + const p = Math.min(at / d, 1) || 0; + $("#dockLine").style.width = p * 100 + "%"; + $("#dockCur").textContent = fmt(at); + $("#dockDur").textContent = fmt(d); + const w = $("#dockWave"); + w.setAttribute("aria-valuenow", Math.round(p * 100)); + w.setAttribute("aria-valuetext", `${fmt(at)} of ${fmt(d)}`); + $$("i", w).forEach((b, i) => b.classList.toggle("p", i / DOCK_BARS < p)); + $(".ph", w).style.left = p * 100 + "%"; +} +// first visit: the dock waits on GOSPEL_OUT, like the site's player +function dockInit() { + let ch = 0, + html = ""; + for (const t of TRACKS) { + if (t.ch !== ch) { + ch = t.ch; + html += `
0${ch} ${esc(t.chapter)}${TRACKS.filter((x) => x.ch === ch).length} FILES
`; + } + html += ``; + } + $("#dockQl").innerHTML = html; + dockShow(TRACKS.find((t) => t.slug === "gospel_out") || TRACKS[0]); +} +function dockQueue(open) { + const q = $("#dockQ"); + $("#dock").classList.toggle("q-open", open); + $("#dockQbtn").setAttribute("aria-expanded", open); + q.setAttribute("aria-hidden", !open); + q.inert = !open; + if (open) { + const list = $("#dockQl"), + c = $(".e-trk.cur", list); + if (c) list.scrollTop = c.offsetTop - list.clientHeight / 2 + c.clientHeight / 2; + } +} $("#dockPlay").onclick = () => { - if (!cur) return play("gospel_out"); + if (cur !== dockT.slug) return play(dockT.slug); au.paused ? au.play() : au.pause(); }; +$("#dockNext").onclick = () => step(1); +$("#dockPrev").onclick = () => (cur && au.currentTime > 3 ? (au.currentTime = 0) : step(-1)); +$("#dockQbtn").onclick = () => dockQueue(!$("#dock").classList.contains("q-open")); +$("#dockQx").onclick = () => dockQueue(false); +$("#dockQl").onclick = (e) => { + const b = e.target.closest("[data-go]"); + if (!b) return; + if (cur === b.dataset.go) au.paused ? au.play() : au.pause(); + else play(b.dataset.go, 0); +}; +$("#dockWave").onclick = (e) => { + const r = e.currentTarget.getBoundingClientRect(); + play(dockT.slug, Math.max(0, Math.min(1, (e.clientX - r.left) / r.width))); +}; +$("#dockWave").onkeydown = (e) => { + if (cur !== dockT.slug || !au.duration) return; + if (e.key === "ArrowRight") au.currentTime = Math.min(au.duration, au.currentTime + 5); + if (e.key === "ArrowLeft") au.currentTime = Math.max(0, au.currentTime - 5); +}; +addEventListener("keydown", (e) => e.key === "Escape" && dockQueue(false)); /* ---------- I · download moment ---------- */ // The vault asks the server for a short-lived signed link to the zip on the private release, @@ -1131,6 +1226,7 @@ function applyData(d) { `
${icon(p.name)}${p.name.toUpperCase()}${esc(p.text)}
`, ).join(""); render(); + dockInit(); drawWheel(); side(); loadSizes(); diff --git a/src/vault/vault.css b/src/vault/vault.css index 6761b04..8870539 100644 --- a/src/vault/vault.css +++ b/src/vault/vault.css @@ -1781,81 +1781,96 @@ details.fold[open] summary::after { width: min(78vw, 600px); } } -.dock-spacer { - height: calc(64px + env(safe-area-inset-bottom)); -} - -/* dock */ -.dock { - position: fixed; - left: 0; - right: 0; - bottom: 0; +/* player dock: the site's .e-dock (overrides.css) with the vault's parts. The seek bar is the + playing track's real waveform, and the right-hand slot shows its tempo, key and Camelot code + (and opens the queue). Phones keep the site's layout: key, title, skip, the Camelot chip and + the glowing line on top. */ +.vdock { z-index: 50; - background: #000; - padding-bottom: env(safe-area-inset-bottom); } -.dock .in { - max-width: var(--maxw); - margin: 0 auto; - height: 64px; +.vdock .e-playbtn { display: flex; align-items: center; - gap: 14px; - padding: 0 28px; + justify-content: center; + flex: none; } -.dock .prog { +.vwseek { + position: relative; + height: 28px; + display: flex; + align-items: flex-end; + gap: 2px; + cursor: pointer; +} +.vwseek:focus-visible { + outline: 1px solid rgba(255, 0, 229, 0.6); + outline-offset: 4px; +} +.vwseek i { + flex: 1; + min-width: 1px; + border-radius: 1px; + background: rgba(244, 244, 248, 0.16); + pointer-events: none; +} +.vwseek i.p { + background: linear-gradient(180deg, var(--mag), var(--cyan)); +} +.vwseek .ph { position: absolute; + top: -3px; + bottom: -3px; left: 0; - right: 0; - top: 0; - height: 2px; - background: rgba(244, 244, 248, 0.08); + width: 1px; + background: var(--ghost); + box-shadow: 0 0 6px var(--mag); + pointer-events: none; } -.dock .prog i { - display: block; - height: 100%; - width: 0; - background: var(--grad); - box-shadow: 0 0 10px rgba(255, 0, 229, 0.6); +.vtele { + gap: 10px; } -.dock .pk { - border: 0; +.vtele .vsep { + width: 1px; + height: 14px; + background: var(--hair); +} +.vdock .e-dock-bpm { + flex: none; + width: 44px; + text-align: right; + font-size: 9px; + color: rgba(244, 244, 248, 0.4); +} +.vdock .e-dock-q .e-trk { + width: 100%; + display: flex; + align-items: center; + gap: 12px; + text-align: left; + font-size: 11px; + letter-spacing: 0.06em; + color: var(--t2); } -.dock .tt { +.vdock .e-dock-q .e-trk:hover, +.vdock .e-dock-q .e-trk.cur { + color: var(--ghost); +} +.vdock .e-trk-t { flex: 1; min-width: 0; - font-family: var(--mono); -} -.dock .tt b { - display: block; - font-weight: 400; - font-size: 12px; - letter-spacing: 0.14em; - white-space: nowrap; overflow: hidden; text-overflow: ellipsis; -} -.dock .tt span { - font-size: 9px; - letter-spacing: 0.24em; - color: var(--t3); -} -.dock .tm, -.dock .dch { - font-family: var(--mono); - font-size: 10px; - letter-spacing: 0.12em; - color: var(--t3); white-space: nowrap; } -@media (max-width: 700px) { - .dock .dch, - .dock .tm { +@media (max-width: 1023px) { + .vtele .vt, + .vtele .vsep { display: none; } - .dock .in { - padding: 0 12px; +} +@media (max-width: 767px) { + .vtele .e-dock-led { + display: none; } } .toast { diff --git a/vault/index.html b/vault/index.html index c187306..204d38b 100644 --- a/vault/index.html +++ b/vault/index.html @@ -311,7 +311,7 @@

THE SOURCE VAULT.

- +
@@ -331,15 +331,99 @@

THE SOURCE VAULT.

RECOVERED
-