fix: player bar freezes on manual song start in bridge mode

- playQueue: update UI immediately before NSBridge.setQueue so the bar
  reflects the new song without waiting for onTrackChanged
- onTrackChanged: remove index guard that blocked UI refresh when
  S.qIdx was already set; always call updatePlayerUI/onNpSongChange
- nextSong/prevSong: optimistic UI update before calling native next/prev
- _computeNextIdx: extracted helper for shuffle/repeat/linear logic
- _lastPositionMs: new variable for prev() 3s threshold in bridge mode
- updatePlayerUI/_syncNpDirect: guard img.src assignment to prevent
  redundant reloads and flicker on double update

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
HoneyandClaude Sonnet 4.6 committed 2026-05-09 13:04:33 +02:00
1 parent cf2921c56f
commit 7084fc4118
1 file changed
+49 -11
+49 -11
View File
@@ -12,6 +12,7 @@ const HAS_BRIDGE = typeof window.NSBridge !== 'undefined';
// Track native playback state; driven by window.NSPlayer callbacks below.
let _nativePos = 0; // ms
let _nativePlaying = false;
let _lastPositionMs = 0; // ms — set by onPositionUpdate, used for prev() decision
let _lastDurationMs = 0; // ms — set by onPositionUpdate, used for seek calculations
// Unified duration / position / seek helpers — use native state when available,
@@ -109,14 +110,16 @@ window.NSPlayer = {
// Native moved to a different track (auto-advance, seek, or queue replace).
onTrackChanged(payloadJson) {
try {
const payload = JSON.parse(payloadJson);
if (typeof payload.index === 'number' && payload.index !== S.qIdx) {
S.qIdx = payload.index;
const p = JSON.parse(payloadJson);
if (typeof p.index !== 'number') return;
if (p.index < 0 || p.index >= S.queue.length) return;
const indexChanged = (p.index !== S.qIdx);
S.qIdx = p.index;
const s = S.queue[S.qIdx];
if (!s) return;
updatePlayerUI(s, true);
onNpSongChange(s.id);
renderQueue();
if (indexChanged) renderQueue();
// Radio mode: top up the queue when running low; push new songs to native.
if (S.radioMode && S.queue.length - S.qIdx < 5) {
const prevLen = S.queue.length;
@@ -126,13 +129,13 @@ window.NSPlayer = {
);
});
}
}
} catch (e) {}
},
// ~250 ms position tick while playing.
onPositionUpdate(posMs, durMs) {
_nativePos = posMs;
_lastPositionMs = posMs;
_lastDurationMs = durMs;
if (S._scrubbing) return;
const dur = durMs / 1000;
@@ -238,8 +241,12 @@ export function playQueue(songs, startIdx) {
_clearPreload();
if (HAS_BRIDGE) {
window.NSBridge.setQueue(JSON.stringify(buildTrackList()), startIdx, true);
const s = S.queue[S.qIdx];
if (s) { updatePlayerUI(s, true); onNpSongChange(s.id); }
renderQueue();
try {
window.NSBridge.setQueue(JSON.stringify(buildTrackList()), startIdx, true);
} catch (e) { console.warn('NSBridge.setQueue failed', e); }
if (S.radioMode) fillRadioQueue();
return;
}
@@ -335,7 +342,7 @@ export function updatePlayerUI(s, _skipMediaSession = false) {
const art = document.getElementById('player-art');
const cv = coverUrl(s.coverArt, 256);
const existingImg = art.querySelector('img');
if (cv && existingImg) existingImg.src = cv;
if (cv && existingImg) { if (existingImg.src !== cv) existingImg.src = cv; }
else art.innerHTML = cv ? `<img src="${cv}" style="width:100%;height:100%;object-fit:cover">` : '<div class="no-art">♪</div>';
updateStarBtn();
document.title = `${s.title} — NovaSonic`;
@@ -384,8 +391,8 @@ export function _syncNpDirect(s) {
const cover = document.getElementById('np-cover');
if (cover) {
const existingImg = cover.querySelector('img');
if (cv && existingImg) existingImg.src = cv;
else if (cover) cover.innerHTML = cv ? `<img src="${cv}" style="width:100%;height:100%;object-fit:cover">` : '<div class="no-art">♪</div>';
if (cv && existingImg) { if (existingImg.src !== cv) existingImg.src = cv; }
else cover.innerHTML = cv ? `<img src="${cv}" style="width:100%;height:100%;object-fit:cover">` : '<div class="no-art">♪</div>';
}
_crossfadeNpBg(cv);
const t = document.getElementById('np-title'); if(t) t.textContent = s.title || '—';
@@ -424,16 +431,47 @@ export function setPlayIcon(playing) {
}
}
function _computeNextIdx() {
if (S.repeat === 'one') return S.qIdx;
if (S.shuffle && S.queue.length > 1) {
let n = S.qIdx;
while (n === S.qIdx) n = Math.floor(Math.random() * S.queue.length);
return n;
}
if (S.qIdx + 1 < S.queue.length) return S.qIdx + 1;
if (S.repeat === 'all') return 0;
return -1;
}
export function prevSong() {
_clearPreload();
if (HAS_BRIDGE) { _nsBridge('prev'); return; }
if (HAS_BRIDGE) {
if (_lastPositionMs > 3000) { _nsBridge('prev'); return; }
if (S.qIdx > 0) {
S.qIdx--;
const s = S.queue[S.qIdx];
if (s) { updatePlayerUI(s, true); onNpSongChange(s.id); renderQueue(); }
}
_nsBridge('prev');
return;
}
if(S.audio.currentTime > 3) { S.audio.currentTime = 0; return; }
if(S.qIdx > 0) { S.qIdx--; playCurrent(); }
}
export function nextSong() {
_clearPreload();
if (HAS_BRIDGE) { _nsBridge('next'); return; }
if (HAS_BRIDGE) {
const ni = _computeNextIdx();
if (ni >= 0 && ni < S.queue.length) {
S.qIdx = ni;
const s = S.queue[S.qIdx];
if (s) { updatePlayerUI(s, true); onNpSongChange(s.id); renderQueue(); }
}
_nsBridge('next');
if (S.radioMode && S.queue.length - S.qIdx < 5) fillRadioQueue();
return;
}
if(S.shuffle) S.qIdx = Math.floor(Math.random() * S.queue.length);
else if(S.qIdx < S.queue.length - 1) S.qIdx++;
else if(S.repeat) S.qIdx = 0;