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:
parent
041f0f2ea1
commit
08cc6b2b03
|
|
@ -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-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 { display: flex; align-items: center; gap: 0.6rem; }
|
||||||
#player-queue-info { font-family: 'Orbitron', monospace; font-size: 0.72rem; }
|
#player-queue-info { font-family: 'Orbitron', monospace; font-size: 0.72rem; }
|
||||||
#player-next {
|
#player-next, #player-repeat {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--neon-cyan);
|
color: var(--neon-cyan);
|
||||||
border: 1px solid 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);
|
box-shadow: var(--glow-cyan);
|
||||||
transition: all .15s;
|
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 sequence overlay ---------- */
|
||||||
#boot-overlay {
|
#boot-overlay {
|
||||||
|
|
|
||||||
|
|
@ -305,7 +305,7 @@ if (canvas) {
|
||||||
const flyby = {
|
const flyby = {
|
||||||
active: null,
|
active: null,
|
||||||
xVel: 0,
|
xVel: 0,
|
||||||
nextAt: performance.now() + rnd(15000, 30000),
|
nextAt: performance.now() + rnd(5000, 12000),
|
||||||
colorIdx: 0,
|
colorIdx: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -313,11 +313,15 @@ if (canvas) {
|
||||||
const entry = flybyPool[Math.floor(Math.random() * flybyPool.length)];
|
const entry = flybyPool[Math.floor(Math.random() * flybyPool.length)];
|
||||||
const goLeft = Math.random() < 0.5;
|
const goLeft = Math.random() < 0.5;
|
||||||
entry.mat.color.setHex(FLYBY_COLORS[flyby.colorIdx++ % FLYBY_COLORS.length]);
|
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.rotation.set(0, 0, 0);
|
||||||
entry.group.visible = true;
|
entry.group.visible = true;
|
||||||
flyby.active = entry;
|
flyby.active = entry;
|
||||||
flyby.xVel = goLeft ? -20 : 20;
|
flyby.xVel = goLeft ? -speed : speed;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- Beat / bass reactivity surface ----------
|
// ---------- Beat / bass reactivity surface ----------
|
||||||
|
|
@ -395,7 +399,7 @@ if (canvas) {
|
||||||
if (Math.abs(flyby.active.group.position.x) > 72) {
|
if (Math.abs(flyby.active.group.position.x) > 72) {
|
||||||
flyby.active.group.visible = false;
|
flyby.active.group.visible = false;
|
||||||
flyby.active = null;
|
flyby.active = null;
|
||||||
flyby.nextAt = now + rnd(15000, 30000);
|
flyby.nextAt = now + rnd(5000, 12000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@
|
||||||
const coverEl = document.getElementById('player-cover');
|
const coverEl = document.getElementById('player-cover');
|
||||||
const queueInfoEl = document.getElementById('player-queue-info');
|
const queueInfoEl = document.getElementById('player-queue-info');
|
||||||
const nextBtn = document.getElementById('player-next');
|
const nextBtn = document.getElementById('player-next');
|
||||||
|
const repeatBtn = document.getElementById('player-repeat');
|
||||||
|
let repeatOn = false;
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
queue: [], // array of {id, title, artist, cover}
|
queue: [], // array of {id, title, artist, cover}
|
||||||
|
|
@ -63,11 +65,20 @@
|
||||||
if (state.index + 1 < state.queue.length) {
|
if (state.index + 1 < state.queue.length) {
|
||||||
state.index += 1;
|
state.index += 1;
|
||||||
playCurrent();
|
playCurrent();
|
||||||
|
} else if (repeatOn && state.queue.length > 0) {
|
||||||
|
state.index = 0;
|
||||||
|
playCurrent();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
audio.addEventListener('ended', next);
|
audio.addEventListener('ended', next);
|
||||||
nextBtn.addEventListener('click', next);
|
nextBtn.addEventListener('click', next);
|
||||||
|
|
||||||
|
repeatBtn.addEventListener('click', () => {
|
||||||
|
repeatOn = !repeatOn;
|
||||||
|
repeatBtn.classList.toggle('active', repeatOn);
|
||||||
|
repeatBtn.setAttribute('aria-pressed', repeatOn);
|
||||||
|
});
|
||||||
window.addEventListener('briTunes:navigate', () => {
|
window.addEventListener('briTunes:navigate', () => {
|
||||||
if (window.briTunesNowPlaying) highlightNowPlaying(window.briTunesNowPlaying.id);
|
if (window.briTunesNowPlaying) highlightNowPlaying(window.briTunesNowPlaying.id);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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 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 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) {
|
function toView(row) {
|
||||||
if (!row) return null;
|
if (!row) return null;
|
||||||
|
|
@ -49,6 +50,7 @@ function toView(row) {
|
||||||
coverPath: row.cover_path,
|
coverPath: row.cover_path,
|
||||||
visibility: row.visibility || 'logged_in',
|
visibility: row.visibility || 'logged_in',
|
||||||
uploadedBy: row.uploaded_by,
|
uploadedBy: row.uploaded_by,
|
||||||
|
uploadedByName: row.uploaded_by ? (uploaderNameStmt.get(row.uploaded_by)?.display_name || null) : null,
|
||||||
createdAt: row.created_at,
|
createdAt: row.created_at,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,10 @@ async function verifyCredentials(email, password) {
|
||||||
if (!row) return null;
|
if (!row) return null;
|
||||||
if (row.disabled) return { disabled: true };
|
if (row.disabled) return { disabled: true };
|
||||||
const ok = await argon2.verify(row.password_hash, password);
|
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) {
|
function findByEmail(email) {
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
<audio id="player-audio" preload="metadata" controls crossorigin="anonymous"></audio>
|
<audio id="player-audio" preload="metadata" controls crossorigin="anonymous"></audio>
|
||||||
<div class="player-queue">
|
<div class="player-queue">
|
||||||
<span id="player-queue-info" class="muted"></span>
|
<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>
|
<button id="player-next" type="button" title="Next">⏭</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
<% if (song.visibility === 'private') { %><span class="badge badge-private">Private</span><% } %>
|
<% if (song.visibility === 'private') { %><span class="badge badge-private">Private</span><% } %>
|
||||||
</h1>
|
</h1>
|
||||||
<p class="muted"><%= song.artist %><% if (song.album) { %> · <%= song.album %><% } %></p>
|
<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">
|
<p class="muted small">
|
||||||
<% if (song.genre) { %><%= song.genre %> · <% } %>
|
<% if (song.genre) { %><%= song.genre %> · <% } %>
|
||||||
<% if (song.year) { %><%= song.year %> · <% } %>
|
<% if (song.year) { %><%= song.year %> · <% } %>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue