Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
120 changes: 108 additions & 12 deletions src/vault/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -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} <span class="cam" style="background:${camColour(t.camelot)}">${t.camelot}</span>`;
dockShow(t);
const row = $(`.row[data-slug="${slug}"]`);
if (row) buildSecs(row, t);
}
Expand All @@ -638,21 +635,33 @@ function syncPlaying() {
r.classList.toggle("playing", me && (on || au.currentTime > 0));
$("[data-play]", r).innerHTML = me && on ? '<span class="bars"></span>' : '<span class="tri"></span>';
});
$("#dockPlay").innerHTML = on ? '<span class="bars"></span>' : '<span class="tri"></span>';
if (!dockT) return;
const k = $("#dockPlay");
k.classList.toggle("on", on);
k.innerHTML = on
? '<span class="e-pp" aria-hidden="true"></span>'
: '<span class="e-pi" aria-hidden="true"></span>';
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);
Expand All @@ -670,10 +679,96 @@ au.addEventListener("timeupdate", () => {
`<span class="h">${esc(secs[si].name)} · FROM ≈${fmt(secs[si].start)}</span>${secs[si].lines.length ? secs[si].lines.slice(0, 4).map(esc).join("<br />") : "<span style='color:var(--t4)'>(instrumental)</span>"}`;
}
});

/* 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) =>
`<i style="height:${Math.max(8, t.peaks[Math.floor((i / DOCK_BARS) * t.peaks.length)] * 100)}%"></i>`,
).join("") + '<span class="ph"></span>';
$$("#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 += `<div class="e-dock-ch"><span><b>0${ch}</b> ${esc(t.chapter)}</span><span>${TRACKS.filter((x) => x.ch === ch).length} FILES</span></div>`;
}
html += `<button type="button" class="e-trk" data-go="${t.slug}" data-n="${t.n}" aria-label="Play track ${t.n}: ${esc(t.title)}, ${t.durationText}"><span class="e-dock-n">${t.n}</span><span class="e-trk-t">${esc(t.title)}</span><span class="e-dock-bpm">≈${t.bpm}</span><span class="e-dock-d">${t.durationText}</span></button>`;
}
$("#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,
Expand Down Expand Up @@ -1131,6 +1226,7 @@ function applyData(d) {
`<div class="pal" style="--c:${SW[p.name][0]}">${icon(p.name)}<b>${p.name.toUpperCase()}</b><span>${esc(p.text)}</span></div>`,
).join("");
render();
dockInit();
drawWheel();
side();
loadSizes();
Expand Down
129 changes: 72 additions & 57 deletions src/vault/vault.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading
Loading