diff --git a/index.html b/index.html
index 6506c73..f17c838 100644
--- a/index.html
+++ b/index.html
@@ -134,8 +134,8 @@
diff --git a/src/main.js b/src/main.js
index 86498b0..38c47bc 100644
--- a/src/main.js
+++ b/src/main.js
@@ -16,7 +16,7 @@ import {
onGlobalSearch, s_goArtist, s_goAlbum, playerBarGoArtist, playerBarGoAlbum,
searchSimilarArtist, searchAndPreview, loadItunesAlbum, expandArtistBio,
loadAlbums, loadStarred, loadRandom, filterStarred, filterPlaylist,
- plReorderEnter, plReorderSave, plReorderCancel,
+ plReorderEnter, plReorderSave, plReorderCancel, toggleAllGenres,
} from './views.js';
import { loadSpotifyImporter, resolveSpotifyLink, clearSpotifyPreview, spotifyDlAll } from './spotify.js';
import {
@@ -304,7 +304,7 @@ Object.assign(window, {
openSidebar, closeSidebar, switchNavMode, showCtxMenu,
// Views
loadAlbums, loadStarred, loadRandom, filterStarred, filterPlaylist,
- plReorderEnter, plReorderSave, plReorderCancel,
+ plReorderEnter, plReorderSave, plReorderCancel, toggleAllGenres,
loadSpotifyImporter, resolveSpotifyLink, clearSpotifyPreview, spotifyDlAll,
executeSpotifyDownload,
openPlModal, closePlModal, addSongsToPlaylist, createAndAddPlaylist, createPlaylistPrompt,
diff --git a/src/player.js b/src/player.js
index 005ab96..424e879 100644
--- a/src/player.js
+++ b/src/player.js
@@ -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();
}
diff --git a/src/views.js b/src/views.js
index 0dc09ef..a365391 100644
--- a/src/views.js
+++ b/src/views.js
@@ -46,6 +46,7 @@ export function navigate(page, data=null) {
if(page==='artist-detail' && data) loadArtistDetail(data);
if(page==='artist-all-songs' && data) loadArtistAllSongs(data);
if(page==='album-detail' && data) loadAlbumDetail(data);
+ if(page==='genre-detail' && data) loadGenreDetail(data);
if(page==='itunes-album-detail'&& data) loadItunesAlbumDetail(data);
if(page==='new-releases') loadNewReleases();
if(page==='playlists') loadPlaylists();
@@ -156,26 +157,60 @@ async function loadHeroCard() {
} catch { /* hero is optional ā silent fail */ }
}
+const _GENRE_COLORS = ['#e8445a','#3b82f6','#f97316','#22c55e','#a855f7','#06b6d4','#eab308','#ec4899','#14b8a6','#f43f5e','#8b5cf6','#84cc16'];
+function _genreColor(name) {
+ let h = 0;
+ for (let i = 0; i < name.length; i++) h = (h * 31 + name.charCodeAt(i)) >>> 0;
+ return _GENRE_COLORS[h % _GENRE_COLORS.length];
+}
+
+let _allGenres = [];
+let _genresExpanded = false;
+const GENRE_LIMIT = 15;
+
async function loadGenreSection() {
- const el = document.getElementById('home-genres');
+ const el = document.getElementById('home-genres');
+ const more = document.getElementById('home-genres-more');
if (!el) return;
try {
const r = await api('getGenres');
- const genres = (r.genres?.genre || [])
+ _allGenres = (r.genres?.genre || [])
.filter(g => (g.songCount || 0) > 0)
.sort((a, b) => (b.songCount || 0) - (a.songCount || 0));
- el.innerHTML = '';
- genres.forEach(g => {
- const chip = document.createElement('button');
- chip.className = 'genre-chip';
- chip.textContent = g.value;
- chip.title = `${g.songCount} Titel`;
- chip.onclick = () => playGenre(g.value);
- el.appendChild(chip);
- });
+ _renderGenres();
+ if (_allGenres.length > GENRE_LIMIT && more) more.style.display = '';
} catch { el.style.display = 'none'; }
}
+function _renderGenres() {
+ const el = document.getElementById('home-genres');
+ if (!el) return;
+ const list = _genresExpanded ? _allGenres : _allGenres.slice(0, GENRE_LIMIT);
+ el.innerHTML = '';
+ list.forEach(g => {
+ const card = document.createElement('div');
+ card.className = 'genre-card';
+ card.title = `${g.songCount} Titel`;
+ const color = _genreColor(g.value);
+ card.innerHTML = `
+
+
${esc(g.value)}
+
`;
+ card.querySelector('.genre-card-play').onclick = (e) => { e.stopPropagation(); playGenre(g.value); };
+ card.onclick = () => navigate('genre-detail', g);
+ el.appendChild(card);
+ });
+}
+
+export function toggleAllGenres() {
+ _genresExpanded = !_genresExpanded;
+ const more = document.getElementById('home-genres-more');
+ if (more) more.textContent = _genresExpanded ? 'Weniger' : 'Alle anzeigen';
+ _renderGenres();
+}
+
async function playGenre(genre) {
try {
const r = await api('getSongsByGenre', `&genre=${encodeURIComponent(genre)}&count=200`);
@@ -187,6 +222,37 @@ async function playGenre(genre) {
} catch {}
}
+async function loadGenreDetail(g) {
+ const genre = g.value || g;
+ const color = _genreColor(genre);
+ document.getElementById('genre-detail-name').textContent = genre;
+ document.getElementById('genre-detail-stats').textContent = g.songCount ? `${g.songCount} Titel` : '';
+ const coverWrap = document.getElementById('genre-cover-wrap');
+ coverWrap.style.background = `linear-gradient(135deg, ${color}33, ${color}11)`;
+ coverWrap.style.borderRadius = '10px';
+ coverWrap.style.border = `2px solid ${color}55`;
+ document.getElementById('genre-back-btn').onclick = () => _navGoBack();
+
+ const list = document.getElementById('genre-songs');
+ list.innerHTML = '
';
+
+ let songs = [];
+ try {
+ const r = await api('getSongsByGenre', `&genre=${encodeURIComponent(genre)}&count=500`);
+ songs = r.songsByGenre?.song || [];
+ } catch { list.innerHTML = '
Fehler beim Laden
'; return; }
+
+ document.getElementById('genre-detail-stats').textContent = `${songs.length} Titel`;
+ document.getElementById('btn-play-genre').onclick = () => {
+ const shuffled = [...songs].sort(() => Math.random() - 0.5);
+ playQueue(shuffled, 0);
+ navigate('now-playing');
+ };
+
+ list.innerHTML = '';
+ songs.forEach((s, i) => list.appendChild(makeSongItem(s, i, songs)));
+}
+
export async function loadAlbumRow(id, method, extra) {
const c = document.getElementById(id);
if(!c) return;
diff --git a/styles.css b/styles.css
index 5ee6597..f7f1cf6 100644
--- a/styles.css
+++ b/styles.css
@@ -133,10 +133,17 @@ nav{flex:1;overflow-y:auto;padding:4px 10px}
.home-hero-play:hover{transform:scale(1.1)}
@media(min-width:700px){.home-hero{height:200px}.home-hero-cover{width:130px;height:130px}.home-hero-title{font-size:22px}.home-hero-info{gap:6px}}
-/* āā GENRE CHIPS āā */
-.genre-chips{display:flex;flex-wrap:wrap;gap:7px;padding:2px 0 4px}
-.genre-chip{padding:6px 13px;border-radius:20px;background:var(--surface2);border:1px solid var(--border);font-size:12px;font-weight:600;color:var(--text2);cursor:pointer;white-space:nowrap;transition:background .15s,color .15s,border-color .15s}
-.genre-chip:hover{background:var(--accent);color:#fff;border-color:var(--accent)}
+/* āā GENRE GRID āā */
+.genre-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:8px;padding:2px 0 4px}
+.genre-card{display:flex;align-items:center;gap:10px;background:var(--surface2);border:1px solid var(--border);border-radius:10px;padding:10px 12px;cursor:pointer;transition:background .15s,border-color .15s;overflow:hidden}
+.genre-card:hover{background:var(--surface3,var(--surface));border-color:var(--accent)}
+.genre-card-bar{width:4px;height:32px;border-radius:3px;flex-shrink:0}
+.genre-card-name{font-size:13px;font-weight:700;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1;min-width:0}
+.genre-card-play{width:28px;height:28px;border-radius:50%;background:var(--accent);border:none;color:#fff;cursor:pointer;display:flex;align-items:center;justify-content:center;flex-shrink:0;opacity:0;transition:opacity .15s,transform .15s}
+.genre-card:hover .genre-card-play{opacity:1}
+.genre-card-play:hover{transform:scale(1.1)}
+@media(max-width:600px){.genre-card-play{opacity:1}}
+@media(min-width:700px){.genre-grid{grid-template-columns:repeat(auto-fill,minmax(170px,1fr))}}
/* āā SECTION āā */
.section-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;margin-top:24px}