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
+43
-19
@@ -79,23 +79,37 @@ window.NSPlayer = {
|
||||
try {
|
||||
const raw = window.NSBridge.getState();
|
||||
const state = raw ? (JSON.parse(raw) || {}) : {};
|
||||
const nativeLen = state.queueLength || 0;
|
||||
const nativePlaying = state.isPlaying || false;
|
||||
const nativeLen = state.queueLength || 0;
|
||||
const nativePlaying = state.isPlaying || false;
|
||||
const nativeIdx = typeof state.currentIndex === 'number' ? state.currentIndex : S.qIdx;
|
||||
const nativeId = state.currentId || '';
|
||||
|
||||
// Helper: true if native's current song matches JS queue at nativeIdx
|
||||
const inSync = nativeIdx >= 0 && nativeIdx < S.queue.length &&
|
||||
(!nativeId || !S.queue[nativeIdx]?.id ||
|
||||
String(nativeId) === String(S.queue[nativeIdx].id));
|
||||
|
||||
if (nativeLen > 0 && nativePlaying) {
|
||||
// Native is actively playing — never rebuild the queue, it would restart the song.
|
||||
// Just re-sync JS state and UI from whatever native reports.
|
||||
S.qIdx = state.currentIndex ?? S.qIdx;
|
||||
_nativePlaying = nativePlaying;
|
||||
_nativePos = state.positionMs || 0;
|
||||
const s = S.queue[S.qIdx];
|
||||
if (s) { updatePlayerUI(s, true); onNpSongChange(s.id); }
|
||||
setPlayIcon(_nativePlaying);
|
||||
renderQueue();
|
||||
} else if (nativeLen > 0 && nativeLen === S.queue.length) {
|
||||
// Paused but queue matches — re-sync UI state only.
|
||||
S.qIdx = state.currentIndex ?? S.qIdx;
|
||||
if (inSync) {
|
||||
// Native is actively playing and we have the right song — re-sync.
|
||||
S.qIdx = nativeIdx;
|
||||
_nativePlaying = true;
|
||||
_nativePos = state.positionMs || 0;
|
||||
const s = S.queue[S.qIdx];
|
||||
if (s) { updatePlayerUI(s, true); onNpSongChange(s.id); }
|
||||
setPlayIcon(true);
|
||||
renderQueue();
|
||||
} else {
|
||||
// Native is playing a song we don't have (e.g. radio advanced while backgrounded).
|
||||
// Reset the stale JS queue to avoid showing the wrong song — native keeps playing.
|
||||
S.queue = [];
|
||||
S.qIdx = 0;
|
||||
}
|
||||
} else if (nativeLen > 0 && inSync) {
|
||||
// Paused but we can confirm the current song — re-sync UI state only.
|
||||
S.qIdx = nativeIdx;
|
||||
_nativePlaying = false;
|
||||
_nativePos = state.positionMs || 0;
|
||||
_nativePos = state.positionMs || 0;
|
||||
const s = S.queue[S.qIdx];
|
||||
if (s) { updatePlayerUI(s, true); onNpSongChange(s.id); }
|
||||
setPlayIcon(false);
|
||||
@@ -138,13 +152,17 @@ window.NSPlayer = {
|
||||
// Catches any edge cases where onTrackChanged was missed or queue drifted.
|
||||
if (state.isPlaying) {
|
||||
try {
|
||||
const live = JSON.parse(window.NSBridge.getState() || '{}');
|
||||
const live = JSON.parse(window.NSBridge.getState() || '{}');
|
||||
const liveIdx = live.currentIndex;
|
||||
const liveId = live.currentId || '';
|
||||
if (typeof liveIdx === 'number' && liveIdx !== S.qIdx &&
|
||||
liveIdx >= 0 && liveIdx < S.queue.length) {
|
||||
S.qIdx = liveIdx;
|
||||
const s = S.queue[liveIdx];
|
||||
if (s) { updatePlayerUI(s, true); onNpSongChange(s.id); }
|
||||
const expected = S.queue[liveIdx];
|
||||
// Only update if song ID confirms native and JS queues are in sync.
|
||||
if (!liveId || !expected?.id || String(liveId) === String(expected.id)) {
|
||||
S.qIdx = liveIdx;
|
||||
if (expected) { updatePlayerUI(expected, true); onNpSongChange(expected.id); }
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
@@ -160,11 +178,17 @@ window.NSPlayer = {
|
||||
// Reconcile with the live native state so stale out-of-order callbacks
|
||||
// (e.g. from rapid skips) don't overwrite the already-correct index.
|
||||
let idx = p.index;
|
||||
let liveId = '';
|
||||
try {
|
||||
const live = JSON.parse(window.NSBridge.getState() || '{}');
|
||||
if (typeof live.currentIndex === 'number') idx = live.currentIndex;
|
||||
liveId = live.currentId || '';
|
||||
} catch {}
|
||||
if (idx < 0 || idx >= S.queue.length) return;
|
||||
// Reject stale callbacks from the old queue: if the song ID at this index
|
||||
// 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;
|
||||
const indexChanged = (idx !== S.qIdx);
|
||||
S.qIdx = idx;
|
||||
const s = S.queue[S.qIdx];
|
||||
|
||||
Reference in new issue
Block a user