fix: Spotify-style shuffle — fixed queue order, not random-each-skip

- playQueue: unified shuffle for both web and Android — pre-shuffles the
  songs array with the tapped song pinned first, saves _origQueue for restore.
  Passes S.qIdx (0 when shuffled) to NSBridge.setQueue instead of startIdx.
- _computeNextIdx: removed the random-pick shuffle branch entirely.
  Since the queue is already shuffled, sequential traversal IS the shuffle order.
- toggleShuffle: removed !HAS_BRIDGE guard so queue reorder runs on Android too.
  Re-sends reordered queue to ExoPlayer so native order stays in sync with JS.
  Removed setShuffleMode call (we manage order ourselves via pre-shuffled array).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
HoneyandClaude Sonnet 4.6 committed 2026-06-04 10:51:42 +02:00
1 parent 1f1a5cd3a6
commit 0552b3f5d0
1 file changed
+12 -14
+12 -14
View File
@@ -359,12 +359,9 @@ export function playQueue(songs, startIdx) {
_radioFilling = false; // cancel any in-flight radio fill for the old queue _radioFilling = false; // cancel any in-flight radio fill for the old queue
S._origQueue = null; S._origQueue = null;
_histClear(); _histClear();
// On Android with shuffle on, start at a random position so we don't always // Spotify-style shuffle: build a fixed shuffled order once, pin the tapped song first.
// begin at index 0 (the first album/playlist track). // Applies to both web and Android — queue is traversed sequentially in this fixed order.
if (HAS_BRIDGE && S.shuffle && songs.length > 1) { if (S.shuffle && songs.length > 1) {
startIdx = Math.floor(Math.random() * songs.length);
}
if (!HAS_BRIDGE && S.shuffle && songs.length > 1) {
const start = songs[startIdx]; const start = songs[startIdx];
const rest = songs.filter((_, i) => i !== startIdx).sort(() => Math.random() - 0.5); const rest = songs.filter((_, i) => i !== startIdx).sort(() => Math.random() - 0.5);
S._origQueue = songs.map(s => ({...s})); S._origQueue = songs.map(s => ({...s}));
@@ -387,7 +384,7 @@ export function playQueue(songs, startIdx) {
// @JavascriptInterface call (which blocks the JS thread while Java // @JavascriptInterface call (which blocks the JS thread while Java
// parses the JSON and creates MediaItems). // parses the JSON and creates MediaItems).
requestAnimationFrame(() => { requestAnimationFrame(() => {
try { window.NSBridge.setQueue(trackListJson, startIdx, true); } try { window.NSBridge.setQueue(trackListJson, S.qIdx, true); }
catch (e) { console.warn('NSBridge.setQueue failed', e); } catch (e) { console.warn('NSBridge.setQueue failed', e); }
// fillRadioQueue AFTER setQueue so addToQueue lands in the new native queue, // fillRadioQueue AFTER setQueue so addToQueue lands in the new native queue,
// not the old one that setQueue is about to wipe. // not the old one that setQueue is about to wipe.
@@ -655,11 +652,7 @@ export function setPlayIcon(playing) {
function _computeNextIdx() { function _computeNextIdx() {
if (S.repeat === 'one') return S.qIdx; if (S.repeat === 'one') return S.qIdx;
if (S.shuffle && S.queue.length > 1) { // No special shuffle case — queue is already in shuffled order when S.shuffle is on.
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.qIdx + 1 < S.queue.length) return S.qIdx + 1;
if (S.repeat === 'all') return 0; if (S.repeat === 'all') return 0;
return -1; return -1;
@@ -759,7 +752,7 @@ export function _applyShuffleRepeat() {
export function toggleShuffle() { export function toggleShuffle() {
S.shuffle = !S.shuffle; S.shuffle = !S.shuffle;
localStorage.setItem('ns_shuffle', S.shuffle ? '1' : '0'); localStorage.setItem('ns_shuffle', S.shuffle ? '1' : '0');
if (!HAS_BRIDGE && S.queue.length > 1) { if (S.queue.length > 1) {
if (S.shuffle) { if (S.shuffle) {
const cur = S.queue[S.qIdx]; const cur = S.queue[S.qIdx];
S._origQueue = [...S.queue]; S._origQueue = [...S.queue];
@@ -774,9 +767,14 @@ export function toggleShuffle() {
S.qIdx = idx >= 0 ? idx : 0; S.qIdx = idx >= 0 ? idx : 0;
} }
renderQueue(); renderQueue();
// Re-send the reordered queue to native so ExoPlayer's order matches JS.
if (HAS_BRIDGE) {
const tl = JSON.stringify(buildTrackList());
try { window.NSBridge.setQueue(tl, S.qIdx, _nativePlaying); }
catch(e) { console.warn('NSBridge.setQueue failed', e); }
}
} }
_applyShuffleRepeat(); _applyShuffleRepeat();
if (HAS_BRIDGE) _nsBridge('setShuffleMode', S.shuffle);
} }
export function toggleRepeat() { export function toggleRepeat() {