feat: home hero+genres, fix Android queue desync via song ID verification
Home view: - Add hero card (random pick from recent+frequent combined pool) - Add genre browser chips (sorted by song count, tap = shuffle-play) Android sync fix: - Add currentId to getState() snapshot so JS can verify song identity - onReady: reset stale localStorage queue when native song ID mismatches - onTrackChanged: reject stale old-queue callbacks by ID verification - onPlaybackStateChanged safety net: ID-guard prevents wrong-queue index corruption - connectToService: call updateStateSnapshot() before onReady so getState() is never empty Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
377a6954e3
commit
d3a1bf6a82
5 files changed
+142
-20
No files matched your search
@@ -109,12 +109,84 @@ export async function loadHomeData() {
|
||||
const el = document.getElementById('home-greeting');
|
||||
if (el) el.textContent = `${greeting}${name} 👋`;
|
||||
|
||||
loadHeroCard();
|
||||
loadGenreSection();
|
||||
loadAlbumRow('home-recent', 'getAlbumList2', '&type=recent&size=20');
|
||||
loadAlbumRow('home-random', 'getAlbumList2', '&type=random&size=20');
|
||||
loadAlbumRow('home-newest', 'getAlbumList2', '&type=newest&size=20');
|
||||
loadAlbumRow('home-frequent', 'getAlbumList2', '&type=frequent&size=20');
|
||||
}
|
||||
|
||||
async function loadHeroCard() {
|
||||
const el = document.getElementById('home-hero');
|
||||
if (!el) return;
|
||||
try {
|
||||
// Combine recent + frequent into a pool and pick one randomly.
|
||||
// This gives new users (no play history) recent albums, and returning users a mix.
|
||||
const [freqRes, recRes] = await Promise.allSettled([
|
||||
api('getAlbumList2', '&type=frequent&size=5'),
|
||||
api('getAlbumList2', '&type=recent&size=5'),
|
||||
]);
|
||||
const freqList = freqRes.status === 'fulfilled' ? (freqRes.value.albumList2?.album || []) : [];
|
||||
const recList = recRes.status === 'fulfilled' ? (recRes.value.albumList2?.album || []) : [];
|
||||
// Deduplicate by id, prefer frequent (weighted taste signal), pad with recent
|
||||
const seen = new Set();
|
||||
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;
|
||||
if (!album) return;
|
||||
const bgUrl = coverUrl(album.coverArt, 1024);
|
||||
const thumbUrl = coverUrl(album.coverArt, 512);
|
||||
el.innerHTML = `
|
||||
${bgUrl ? `<img class="home-hero-bg" src="${bgUrl}" alt="">` : ''}
|
||||
<div class="home-hero-overlay">
|
||||
${thumbUrl ? `<img class="home-hero-cover" src="${thumbUrl}" alt="${esc(album.name)}">` : ''}
|
||||
<div class="home-hero-info">
|
||||
<div class="home-hero-label">Meistgespielt</div>
|
||||
<div class="home-hero-title">${esc(album.name)}</div>
|
||||
<div class="home-hero-artist">${esc(album.artist || '')}</div>
|
||||
</div>
|
||||
<button class="home-hero-play" title="Abspielen">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg>
|
||||
</button>
|
||||
</div>`;
|
||||
el.querySelector('.home-hero-cover')?.addEventListener('click', () => navigate('album-detail', album));
|
||||
el.querySelector('.home-hero-artist').onclick = () => navigate('album-detail', album);
|
||||
el.querySelector('.home-hero-play').onclick = () => quickPlayAlbum(album);
|
||||
el.classList.add('loaded');
|
||||
} catch { /* hero is optional — silent fail */ }
|
||||
}
|
||||
|
||||
async function loadGenreSection() {
|
||||
const el = document.getElementById('home-genres');
|
||||
if (!el) return;
|
||||
try {
|
||||
const r = await api('getGenres');
|
||||
const genres = (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);
|
||||
});
|
||||
} catch { el.style.display = 'none'; }
|
||||
}
|
||||
|
||||
async function playGenre(genre) {
|
||||
try {
|
||||
const r = await api('getSongsByGenre', `&genre=${encodeURIComponent(genre)}&count=200`);
|
||||
const songs = r.songsByGenre?.song || [];
|
||||
if (!songs.length) return;
|
||||
const shuffled = [...songs].sort(() => Math.random() - 0.5);
|
||||
playQueue(shuffled, 0);
|
||||
navigate('now-playing');
|
||||
} catch {}
|
||||
}
|
||||
|
||||
export async function loadAlbumRow(id, method, extra) {
|
||||
const c = document.getElementById(id);
|
||||
if(!c) return;
|
||||
|
||||
Reference in new issue
Block a user