feat: scrobbling, genre grid redesign with detail view + play button

- Scrobble to Navidrome on auto-advance (Android) and track end (web)
  so type=recent and type=frequent are accurate for the hero card
- Genre section: pill chips → grid cards with colored left bars
- Top 15 genres shown by default (sorted by song count), expand toggle
- Genre card: click → genre detail page with full song list
- Genre card: hover play button → shuffle-play without navigating
- Genre detail page reuses album-detail layout (back btn, stats, song list)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
HoneyandClaude Sonnet 4.6 committed 2026-05-21 09:18:56 +02:00
1 parent d3a1bf6a82
commit f79817410b
5 files changed
+115 -19

No files matched your search

+6
View File
@@ -189,6 +189,10 @@ window.NSPlayer = {
// doesn't match what native is actually playing, the queues are mismatched.
// Allow through if reason='queue' (we triggered a setQueue) or if no IDs to compare.
if (liveId && S.queue[idx]?.id && String(liveId) !== String(S.queue[idx].id) && p.reason !== 'queue') return;
// Scrobble the track that just finished (auto-advance = it played to completion).
if (p.reason === 'auto' && S.queue[S.qIdx]?.id) {
api('scrobble', `&id=${S.queue[S.qIdx].id}&time=${Date.now()}`).catch(() => {});
}
const indexChanged = (idx !== S.qIdx);
S.qIdx = idx;
const s = S.queue[S.qIdx];
@@ -614,6 +618,8 @@ export function nextSong() {
export function onEnded() {
if(S.repeat === 'one') { S.audio.currentTime = 0; S.audio.play(); return; }
// Scrobble the track that just finished (web path).
if (S.queue[S.qIdx]?.id) api('scrobble', `&id=${S.queue[S.qIdx].id}&time=${Date.now()}`).catch(() => {});
nextSong();
}