fix: eliminate NP overlay stale-song on autoplay and cover-load delay
Three improvements: - openNowPlaying(): reads NSBridge.getState() (always-fresh @Volatile snapshot) via ID search to reconcile S.qIdx before render; fixes NP showing old song when deferred evaluateJavascript callbacks haven't fired yet after backgrounding - onPositionUpdate(): periodic check (~every 2s) detects track mismatch via snapshot ID; catches missed/delayed onTrackChanged when NP is open - updatePlayerUI(): pre-warms next song's 1024px cover so auto-advance NP swap is instant from browser cache instead of a 1-second load delay - styles: add backface-visibility:hidden + isolation:isolate to .np-overlay to reduce GPU composite flicker on Android WebView on manual skip Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
050aff6dbe
commit
0788c801fa
3 files changed
+45
-1
No files matched your search
+18
-1
@@ -2,12 +2,29 @@ import { S } from './state.js';
|
|||||||
import { esc } from './utils.js';
|
import { esc } from './utils.js';
|
||||||
import { coverUrl } from './api.js';
|
import { coverUrl } from './api.js';
|
||||||
// Circular imports — safe (only used inside function bodies)
|
// 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 { navigate } from './views.js';
|
||||||
import { openPlModal, showSongInfo } from './ui.js';
|
import { openPlModal, showSongInfo } from './ui.js';
|
||||||
|
|
||||||
// ─── NOW PLAYING OVERLAY ───
|
// ─── NOW PLAYING OVERLAY ───
|
||||||
export function openNowPlaying() {
|
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;
|
S.npOpen = true;
|
||||||
const overlay = document.getElementById('np-overlay');
|
const overlay = document.getElementById('np-overlay');
|
||||||
overlay.classList.add('open');
|
overlay.classList.add('open');
|
||||||
|
|||||||
@@ -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 _lastDurationMs = 0; // ms — set by onPositionUpdate, used for seek calculations
|
||||||
let _skipping = false; // true briefly after a skip — suppresses the transient isPlaying:false
|
let _skipping = false; // true briefly after a skip — suppresses the transient isPlaying:false
|
||||||
let _skipTimer = null;
|
let _skipTimer = null;
|
||||||
|
let _reconTick = 0; // counter for periodic track-reconciliation inside onPositionUpdate
|
||||||
|
|
||||||
// ─── SCROBBLE ─────────────────────────────────────────────────────────────
|
// ─── SCROBBLE ─────────────────────────────────────────────────────────────
|
||||||
let _scrobbleTimer = null;
|
let _scrobbleTimer = null;
|
||||||
@@ -221,6 +222,24 @@ window.NSPlayer = {
|
|||||||
_lastPositionMs = posMs;
|
_lastPositionMs = posMs;
|
||||||
_lastDurationMs = durMs;
|
_lastDurationMs = durMs;
|
||||||
if (S._scrubbing) return;
|
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 dur = durMs / 1000;
|
||||||
const cur = posMs / 1000;
|
const cur = posMs / 1000;
|
||||||
if (dur <= 0) return;
|
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
|
// Preload the hi-res NP cover now so opening the overlay is instant
|
||||||
const _npCv = coverUrl(s.coverArt, 1024);
|
const _npCv = coverUrl(s.coverArt, 1024);
|
||||||
if (_npCv && _npCv !== _npBgLastUrl) { const _pre = new Image(); _pre.src = _npCv; }
|
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 ────────────────────────────────────────────
|
// ── Solid cover-color background ────────────────────────────────────────────
|
||||||
|
|||||||
@@ -577,6 +577,8 @@ button:focus{outline:none}
|
|||||||
transition:transform .35s cubic-bezier(.4,0,.2,1);
|
transition:transform .35s cubic-bezier(.4,0,.2,1);
|
||||||
overflow:hidden;
|
overflow:hidden;
|
||||||
will-change:transform;
|
will-change:transform;
|
||||||
|
backface-visibility:hidden;
|
||||||
|
isolation:isolate;
|
||||||
}
|
}
|
||||||
.np-overlay.open{transform:translateY(0)}
|
.np-overlay.open{transform:translateY(0)}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user