diff --git a/src/player.js b/src/player.js index 86f1689..28192ab 100644 --- a/src/player.js +++ b/src/player.js @@ -10,9 +10,10 @@ import { updateLyricsHighlight, onNpSongChange } from './nowplaying.js'; 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 _lastDurationMs = 0; // ms — set by onPositionUpdate, used for seek calculations +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, // fall back to HTMLAudioElement when not. @@ -109,23 +110,24 @@ 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 s = S.queue[S.qIdx]; - if (!s) return; - updatePlayerUI(s, true); - onNpSongChange(s.id); - 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; - fillRadioQueue().then(() => { - S.queue.slice(prevLen).forEach(t => - _nsBridge('addToQueue', JSON.stringify(_trackToNative(t)), -1) - ); - }); - } + 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); + 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; + fillRadioQueue().then(() => { + S.queue.slice(prevLen).forEach(t => + _nsBridge('addToQueue', JSON.stringify(_trackToNative(t)), -1) + ); + }); } } catch (e) {} }, @@ -133,6 +135,7 @@ window.NSPlayer = { // ~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 ? `` : '
♪
'; 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 ? `` : '
♪
'; + if (cv && existingImg) { if (existingImg.src !== cv) existingImg.src = cv; } + else cover.innerHTML = cv ? `` : '
♪
'; } _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;