feat: add repeat functionality to player UI and enhance song metadata display

- Introduce repeat button with active state styling and toggle logic in player.js
- Update background grid animation to use faster, distance-scaled flyby passes for more dynamic visuals
- Extend song view model (`songs.js`) to include uploader's display name
- Improve user verification flow (`users.js`) to ensure fresh state after async password check
- Add uploader info line in public song page view
This commit is contained in:
Brian Fertig 2026-04-10 22:01:40 -06:00
parent 041f0f2ea1
commit 08cc6b2b03
7 changed files with 34 additions and 7 deletions

View File

@ -535,7 +535,7 @@ button.linklike.danger:hover { color: #fff; text-shadow: 0 0 6px var(--danger);
#player-audio { flex: 1; min-width: 160px; filter: hue-rotate(200deg) saturate(1.2); }
.player-queue { display: flex; align-items: center; gap: 0.6rem; }
#player-queue-info { font-family: 'Orbitron', monospace; font-size: 0.72rem; }
#player-next {
#player-next, #player-repeat {
background: transparent;
color: var(--neon-cyan);
border: 1px solid var(--neon-cyan);
@ -545,7 +545,12 @@ button.linklike.danger:hover { color: #fff; text-shadow: 0 0 6px var(--danger);
box-shadow: var(--glow-cyan);
transition: all .15s;
}
#player-next:hover { background: var(--neon-cyan); color: #05060d; }
#player-next:hover, #player-repeat:hover { background: var(--neon-cyan); color: #05060d; }
#player-repeat.active {
background: var(--neon-cyan);
color: #05060d;
box-shadow: 0 0 12px var(--neon-cyan), 0 0 24px rgba(0,229,255,0.4);
}
/* ---------- Boot sequence overlay ---------- */
#boot-overlay {

View File

@ -305,7 +305,7 @@ if (canvas) {
const flyby = {
active: null,
xVel: 0,
nextAt: performance.now() + rnd(15000, 30000),
nextAt: performance.now() + rnd(5000, 12000),
colorIdx: 0,
};
@ -313,11 +313,15 @@ if (canvas) {
const entry = flybyPool[Math.floor(Math.random() * flybyPool.length)];
const goLeft = Math.random() < 0.5;
entry.mat.color.setHex(FLYBY_COLORS[flyby.colorIdx++ % FLYBY_COLORS.length]);
entry.group.position.set(goLeft ? 65 : -65, rnd(7, 12), rnd(-30, -45));
const z = rnd(-6, -45);
// Closer passes move faster so they feel properly scaled in speed.
const dist = Math.abs(camera.position.z - z);
const speed = rnd(7, 14) * (55 / dist);
entry.group.position.set(goLeft ? 65 : -65, rnd(5, 13), z);
entry.group.rotation.set(0, 0, 0);
entry.group.visible = true;
flyby.active = entry;
flyby.xVel = goLeft ? -20 : 20;
flyby.xVel = goLeft ? -speed : speed;
}
// ---------- Beat / bass reactivity surface ----------
@ -395,7 +399,7 @@ if (canvas) {
if (Math.abs(flyby.active.group.position.x) > 72) {
flyby.active.group.visible = false;
flyby.active = null;
flyby.nextAt = now + rnd(15000, 30000);
flyby.nextAt = now + rnd(5000, 12000);
}
}

View File

@ -5,6 +5,8 @@
const coverEl = document.getElementById('player-cover');
const queueInfoEl = document.getElementById('player-queue-info');
const nextBtn = document.getElementById('player-next');
const repeatBtn = document.getElementById('player-repeat');
let repeatOn = false;
const state = {
queue: [], // array of {id, title, artist, cover}
@ -63,11 +65,20 @@
if (state.index + 1 < state.queue.length) {
state.index += 1;
playCurrent();
} else if (repeatOn && state.queue.length > 0) {
state.index = 0;
playCurrent();
}
}
audio.addEventListener('ended', next);
nextBtn.addEventListener('click', next);
repeatBtn.addEventListener('click', () => {
repeatOn = !repeatOn;
repeatBtn.classList.toggle('active', repeatOn);
repeatBtn.setAttribute('aria-pressed', repeatOn);
});
window.addEventListener('briTunes:navigate', () => {
if (window.briTunesNowPlaying) highlightNowPlaying(window.briTunesNowPlaying.id);
});

View File

@ -34,6 +34,7 @@ const listForManagementStmt = db.prepare(`
`);
const recentGuestStmt = db.prepare(`SELECT * FROM songs WHERE visibility = 'public' ORDER BY created_at DESC LIMIT ?`);
const recentUserStmt = db.prepare(`SELECT * FROM songs WHERE visibility IN ('public', 'logged_in') ORDER BY created_at DESC LIMIT ?`);
const uploaderNameStmt = db.prepare('SELECT display_name FROM users WHERE id = ?');
function toView(row) {
if (!row) return null;
@ -49,6 +50,7 @@ function toView(row) {
coverPath: row.cover_path,
visibility: row.visibility || 'logged_in',
uploadedBy: row.uploaded_by,
uploadedByName: row.uploaded_by ? (uploaderNameStmt.get(row.uploaded_by)?.display_name || null) : null,
createdAt: row.created_at,
};
}

View File

@ -57,7 +57,10 @@ async function verifyCredentials(email, password) {
if (!row) return null;
if (row.disabled) return { disabled: true };
const ok = await argon2.verify(row.password_hash, password);
return ok ? publicView(row) : null;
if (!ok) return null;
// Re-fetch after the async op so email_verified/disabled reflect the current DB state.
const fresh = findByIdStmt.get(row.id);
return fresh ? publicView(fresh) : null;
}
function findByEmail(email) {

View File

@ -10,6 +10,7 @@
<audio id="player-audio" preload="metadata" controls crossorigin="anonymous"></audio>
<div class="player-queue">
<span id="player-queue-info" class="muted"></span>
<button id="player-repeat" type="button" title="Repeat" aria-pressed="false">🔁</button>
<button id="player-next" type="button" title="Next">⏭</button>
</div>
</div>

View File

@ -10,6 +10,7 @@
<% if (song.visibility === 'private') { %><span class="badge badge-private">Private</span><% } %>
</h1>
<p class="muted"><%= song.artist %><% if (song.album) { %> · <%= song.album %><% } %></p>
<% if (song.uploadedByName) { %><p class="card-creator">Added by: <%= song.uploadedByName %></p><% } %>
<p class="muted small">
<% if (song.genre) { %><%= song.genre %> · <% } %>
<% if (song.year) { %><%= song.year %> · <% } %>