fix: 15s scrobble, play count clarity, star always visible, all-songs actions

- Scrobble timer: 12s → 15s
- Play count moved after action buttons, shown as "23×" (distinct from duration)
- Song star/menu buttons always visible (removed opacity:0 hover-only rule)
- Artist all-songs page: added star + 3-dots to every row (same as top songs)
- Hero card: picks from full recent+frequent pool (was capped at first 5 = all frequent)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
HoneyandClaude Sonnet 4.6 committed 2026-05-21 09:47:06 +02:00
1 parent 7e07fef7ba
commit 3128b06b24
4 files changed
+18 -10

No files matched your search

+3 -2
View File
@@ -46,7 +46,7 @@ export function makeSongItem(s, idx, list, { noArtistLink = false, noAlbumLink =
? `<span style="color:var(--text3)"> · </span><span class="song-link" onclick="event.stopPropagation();s_goAlbum(event,'${s.albumId||''}','${esc(s.album||'')}','${esc(s.artist||'')}','${s.artistId||''}')">${esc(s.album||'')}</span>` ? `<span style="color:var(--text3)"> · </span><span class="song-link" onclick="event.stopPropagation();s_goAlbum(event,'${s.albumId||''}','${esc(s.album||'')}','${esc(s.artist||'')}','${s.artistId||''}')">${esc(s.album||'')}</span>`
: ''; : '';
const playsHtml = S.cfg.showPlayCount && s.playCount const playsHtml = S.cfg.showPlayCount && s.playCount
? `<span class="song-plays">${s.playCount.toLocaleString()}</span>` : ''; ? `<span class="song-plays">${s.playCount.toLocaleString()}<span class="song-plays-x">×</span></span>` : '';
el.innerHTML = `<div class="song-num">${idx+1}</div> el.innerHTML = `<div class="song-num">${idx+1}</div>
<div class="song-thumb">${cv ? `<img src="${cv}" loading="lazy" onerror="this.parentNode.innerHTML='<div class=\\'no-art\\'>♪</div>'">` : '<div class="no-art">♪</div>'}</div> <div class="song-thumb">${cv ? `<img src="${cv}" loading="lazy" onerror="this.parentNode.innerHTML='<div class=\\'no-art\\'>♪</div>'">` : '<div class="no-art">♪</div>'}</div>
<div class="song-info"> <div class="song-info">
@@ -54,11 +54,12 @@ export function makeSongItem(s, idx, list, { noArtistLink = false, noAlbumLink =
<div class="song-artist">${artistHtml}${albumHtml}</div> <div class="song-artist">${artistHtml}${albumHtml}</div>
</div> </div>
<div class="song-meta"> <div class="song-meta">
${playsHtml}<span class="song-dur">${formatDur(s.duration)}</span> <span class="song-dur">${formatDur(s.duration)}</span>
<div class="song-actions"> <div class="song-actions">
<button class="icon-btn${s._starred?' starred':''}" title="Favorit"><svg width="12" height="12" viewBox="0 0 24 24" fill="${s._starred?'currentColor':'none'}" stroke="currentColor" stroke-width="2"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg></button> <button class="icon-btn${s._starred?' starred':''}" title="Favorit"><svg width="12" height="12" viewBox="0 0 24 24" fill="${s._starred?'currentColor':'none'}" stroke="currentColor" stroke-width="2"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg></button>
<button class="icon-btn song-menu-btn" title="Mehr Optionen"><svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><circle cx="12" cy="5" r="1.5"/><circle cx="12" cy="12" r="1.5"/><circle cx="12" cy="19" r="1.5"/></svg></button> <button class="icon-btn song-menu-btn" title="Mehr Optionen"><svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><circle cx="12" cy="5" r="1.5"/><circle cx="12" cy="12" r="1.5"/><circle cx="12" cy="19" r="1.5"/></svg></button>
</div> </div>
${playsHtml}
</div>`; </div>`;
const btns = el.querySelectorAll('.icon-btn'); const btns = el.querySelectorAll('.icon-btn');
btns[0].onclick = (e) => { e.stopPropagation(); toggleStar(s, btns[0]); }; btns[0].onclick = (e) => { e.stopPropagation(); toggleStar(s, btns[0]); };
+1 -1
View File
@@ -27,7 +27,7 @@ function _scheduleScrobble(id) {
_scrobbleTimer = setTimeout(() => { _scrobbleTimer = setTimeout(() => {
if (_scrobbleId) api('scrobble', `&id=${_scrobbleId}&time=${Date.now()}`).catch(() => {}); if (_scrobbleId) api('scrobble', `&id=${_scrobbleId}&time=${Date.now()}`).catch(() => {});
_scrobbleId = null; _scrobbleId = null;
}, 12_000); }, 15_000);
} }
function _cancelScrobble() { function _cancelScrobble() {
clearTimeout(_scrobbleTimer); clearTimeout(_scrobbleTimer);
+11 -3
View File
@@ -133,7 +133,7 @@ async function loadHeroCard() {
// Deduplicate by id, prefer frequent (weighted taste signal), pad with recent // Deduplicate by id, prefer frequent (weighted taste signal), pad with recent
const seen = new Set(); const seen = new Set();
const pool = [...freqList, ...recList].filter(a => a.id && !seen.has(a.id) && seen.add(a.id)); const pool = [...freqList, ...recList].filter(a => a.id && !seen.has(a.id) && seen.add(a.id));
const album = pool.length ? pool[Math.floor(Math.random() * Math.min(5, pool.length))] : null; const album = pool.length ? pool[Math.floor(Math.random() * pool.length)] : null;
if (!album) return; if (!album) return;
const bgUrl = coverUrl(album.coverArt, 1024); const bgUrl = coverUrl(album.coverArt, 1024);
const thumbUrl = coverUrl(album.coverArt, 512); const thumbUrl = coverUrl(album.coverArt, 512);
@@ -579,13 +579,21 @@ export async function loadArtistAllSongs({ albums, artist }) {
el.appendChild(h); el.appendChild(h);
} }
const cv = coverUrl(s.coverArt, 60); const cv = coverUrl(s.coverArt, 60);
if(s.starred && !s._starred) s._starred = true;
const row = document.createElement('div'); row.className = 'topsong-item'; const row = document.createElement('div'); row.className = 'topsong-item';
row.innerHTML = `<div class="topsong-num">${s.trackNumber||''}</div> row.innerHTML = `<div class="topsong-num">${s.trackNumber||''}</div>
<div class="topsong-thumb">${cv?`<img src="${cv}" loading="lazy">`:'<div style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:16px">🎵</div>'}</div> <div class="topsong-thumb">${cv?`<img src="${cv}" loading="lazy">`:'<div style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:16px">🎵</div>'}</div>
<div><div class="topsong-title">${esc(s.title)}</div></div> <div style="min-width:0;flex:1"><div class="topsong-title">${esc(s.title)}</div></div>
<div class="topsong-plays">${s.playCount?s.playCount.toLocaleString():''}</div>`; <div class="topsong-plays">${s.playCount?s.playCount.toLocaleString():''}</div>
<div class="song-actions" style="flex-shrink:0">
<button class="icon-btn${s._starred?' starred':''}" title="Favorit"><svg width="12" height="12" viewBox="0 0 24 24" fill="${s._starred?'currentColor':'none'}" stroke="currentColor" stroke-width="2"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg></button>
<button class="icon-btn song-menu-btn" title="Mehr"><svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><circle cx="12" cy="5" r="1.5"/><circle cx="12" cy="12" r="1.5"/><circle cx="12" cy="19" r="1.5"/></svg></button>
</div>`;
row.onclick = () => playQueue(allSongs, i); row.onclick = () => playQueue(allSongs, i);
row.addEventListener('contextmenu', e => { e.preventDefault(); showCtxMenu(e, s, allSongs, i); }); row.addEventListener('contextmenu', e => { e.preventDefault(); showCtxMenu(e, s, allSongs, i); });
const btns = row.querySelectorAll('.icon-btn');
btns[0].onclick = e => { e.stopPropagation(); toggleStar(s, btns[0]); };
btns[1].onclick = e => { e.stopPropagation(); showCtxMenu(e, s, allSongs, i); };
el.appendChild(row); el.appendChild(row);
}); });
} }
+3 -4
View File
@@ -219,10 +219,9 @@ button:focus{outline:none}
.song-info .song-artist{font-size:11px;color:var(--text2);margin-top:1px} .song-info .song-artist{font-size:11px;color:var(--text2);margin-top:1px}
.song-meta{display:flex;align-items:center;gap:6px} .song-meta{display:flex;align-items:center;gap:6px}
.song-dur{font-size:11px;color:var(--text3);font-family:var(--mono)} .song-dur{font-size:11px;color:var(--text3);font-family:var(--mono)}
.song-plays{font-size:11px;color:var(--text3);font-family:var(--mono)} .song-plays{font-size:11px;color:var(--text3);font-family:var(--mono);min-width:28px;text-align:right}
.song-actions{display:flex;gap:2px;opacity:0;transition:opacity .15s} .song-plays-x{font-size:9px;opacity:.6;margin-left:1px}
.song-item:hover .song-actions{opacity:1} .song-actions{display:flex;gap:2px}
@media (hover:none),(pointer:coarse){.song-actions{opacity:1}}
/* ── PLAYLIST DRAG & DROP ── */ /* ── PLAYLIST DRAG & DROP ── */
.song-list.pl-draggable .song-item{grid-template-columns:18px 32px 44px 1fr auto} .song-list.pl-draggable .song-item{grid-template-columns:18px 32px 44px 1fr auto}
.pl-drag-handle{display:flex;align-items:center;justify-content:center;color:var(--text3);opacity:0;cursor:grab;transition:opacity .15s;touch-action:none} .pl-drag-handle{display:flex;align-items:center;justify-content:center;color:var(--text3);opacity:0;cursor:grab;transition:opacity .15s;touch-action:none}