diff --git a/src/nowplaying.js b/src/nowplaying.js index b37e3b2..150cfdd 100644 --- a/src/nowplaying.js +++ b/src/nowplaying.js @@ -2,12 +2,29 @@ import { S } from './state.js'; import { esc } from './utils.js'; import { coverUrl } from './api.js'; // Circular imports — safe (only used inside function bodies) -import { playCurrent, _syncNpDirect, dlSongLocal, npToggleStar, _syncOverlayState, isPlaying, seekToMs } from './player.js'; +import { playCurrent, _syncNpDirect, dlSongLocal, npToggleStar, _syncOverlayState, isPlaying, seekToMs, updatePlayerUI } from './player.js'; import { navigate } from './views.js'; import { openPlModal, showSongInfo } from './ui.js'; // ─── NOW PLAYING OVERLAY ─── export function openNowPlaying() { + // Reconcile S.qIdx with native state before rendering. + // NSBridge.getState() reads a @Volatile snapshot updated synchronously before every callJs(), + // so it is always fresh even when evaluateJavascript callbacks are still queued (backgrounded app). + if (typeof window.NSBridge !== 'undefined') { + try { + const live = JSON.parse(window.NSBridge.getState() || '{}'); + const liveId = String(live.currentId || ''); + if (liveId && liveId !== String(S.queue[S.qIdx]?.id || '')) { + const found = S.queue.findIndex(s => String(s.id) === liveId); + if (found >= 0) { + S.qIdx = found; + const s = S.queue[S.qIdx]; + if (s) { updatePlayerUI(s, true); onNpSongChange(s.id); } + } + } + } catch {} + } S.npOpen = true; const overlay = document.getElementById('np-overlay'); overlay.classList.add('open'); diff --git a/src/player.js b/src/player.js index b162300..fe1d030 100644 --- a/src/player.js +++ b/src/player.js @@ -16,6 +16,7 @@ let _lastPositionMs = 0; // ms — set by onPositionUpdate, used for prev() de let _lastDurationMs = 0; // ms — set by onPositionUpdate, used for seek calculations let _skipping = false; // true briefly after a skip — suppresses the transient isPlaying:false let _skipTimer = null; +let _reconTick = 0; // counter for periodic track-reconciliation inside onPositionUpdate // ─── SCROBBLE ───────────────────────────────────────────────────────────── let _scrobbleTimer = null; @@ -221,6 +222,24 @@ window.NSPlayer = { _lastPositionMs = posMs; _lastDurationMs = durMs; if (S._scrubbing) return; + // Every 8 ticks (~2 s): verify JS track matches native snapshot. + // Catches the gap between auto-advance and onTrackChanged arriving in JS + // (evaluateJavascript is deferred while WebView is backgrounded/resuming). + if (HAS_BRIDGE && ++_reconTick >= 8) { + _reconTick = 0; + try { + const snap = JSON.parse(window.NSBridge.getState() || '{}'); + const snapId = String(snap.currentId || ''); + if (snapId && snapId !== String(S.queue[S.qIdx]?.id || '')) { + const found = S.queue.findIndex(s => String(s.id) === snapId); + if (found >= 0) { + S.qIdx = found; + const s = S.queue[S.qIdx]; + if (s) { updatePlayerUI(s, true); syncNowPlaying(); onNpSongChange(s.id); renderQueue(); } + } + } + } catch {} + } const dur = durMs / 1000; const cur = posMs / 1000; if (dur <= 0) return; @@ -496,6 +515,12 @@ export function updatePlayerUI(s, _skipMediaSession = false) { // Preload the hi-res NP cover now so opening the overlay is instant const _npCv = coverUrl(s.coverArt, 1024); if (_npCv && _npCv !== _npBgLastUrl) { const _pre = new Image(); _pre.src = _npCv; } + // Pre-warm the next song's 1024 cover so auto-advance NP swap is instant from cache + const _nextForPreload = S.qIdx + 1 < S.queue.length ? S.queue[S.qIdx + 1] : null; + if (_nextForPreload?.coverArt) { + const _nextCv = coverUrl(_nextForPreload.coverArt, 1024); + if (_nextCv) { const _preN = new Image(); _preN.src = _nextCv; } + } } // ── Solid cover-color background ──────────────────────────────────────────── diff --git a/styles.css b/styles.css index 74fe185..5dab7a0 100644 --- a/styles.css +++ b/styles.css @@ -577,6 +577,8 @@ button:focus{outline:none} transition:transform .35s cubic-bezier(.4,0,.2,1); overflow:hidden; will-change:transform; + backface-visibility:hidden; + isolation:isolate; } .np-overlay.open{transform:translateY(0)}