diff --git a/frontend/app.js b/frontend/app.js index 362112e..47b4601 100755 --- a/frontend/app.js +++ b/frontend/app.js @@ -176,6 +176,7 @@ const DEFAULT_SETTINGS = { quality: 'auto', volume: 1, audioOnly: false, autoPreload: true, repeatMode: 'off', // 'off' | 'all' — repeat the playing list when it ends loopOne: false, // repeat the single current video + shuffle: false, // shuffle upcoming tracks when playing a list theme: 'dark', // 'dark' | 'light' | 'contrast' fontScale: 'normal', // 'small' | 'normal' | 'large' | 'xl' density: 'comfortable', // 'comfortable' | 'compact' @@ -243,6 +244,7 @@ const els = { speed: $('speedSelect'), quality: $('qualitySelect'), fsBtn: $('fsBtn'), + shuffleBtn: $('shuffleBtn'), loopBtn: $('loopBtn'), repeatBtn: $('repeatBtn'), queueBtn: $('queueBtn'), @@ -999,10 +1001,46 @@ function playFromList(list, index, source = '') { queue = list; queueIndex = index; queueSource = source; - Player.loadVideo(list[index]); + unshuffledQueue = null; + if (data.settings.shuffle) applyShuffle(); + Player.loadVideo(queue[queueIndex]); renderUpNext(); } +// ---------- Shuffle ---------- +// Shuffles only the UPCOMING part of the live queue (Fisher-Yates); the +// already-played prefix and the current track stay where they are. The +// pre-shuffle order is kept so turning shuffle off restores it. +let unshuffledQueue = null; + +function applyShuffle() { + if (queue.length <= queueIndex + 2) return; // nothing meaningful to shuffle + if (!unshuffledQueue) unshuffledQueue = queue.slice(); + const upcoming = queue.slice(queueIndex + 1); + for (let i = upcoming.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [upcoming[i], upcoming[j]] = [upcoming[j], upcoming[i]]; + } + queue = queue.slice(0, queueIndex + 1).concat(upcoming); +} + +function toggleShuffle() { + data.settings.shuffle = !data.settings.shuffle; + if (data.settings.shuffle) { + applyShuffle(); + } else if (unshuffledQueue) { + const playingId = current && current.meta && current.meta.id; + queue = unshuffledQueue; + unshuffledQueue = null; + const idx = playingId ? queue.findIndex((v) => v.id === playingId) : -1; + if (idx >= 0) queueIndex = idx; + } + persist(); + updateLoopRepeatButtons(); + renderUpNext(); + toast(data.settings.shuffle ? 'Shuffle on' : 'Shuffle off'); +} + // ---------- Temporary queue ---------- function updateQueueBadge() { const count = data.queue.length; @@ -1115,6 +1153,7 @@ function toggleRepeat() { function updateLoopRepeatButtons() { if (els.loopBtn) els.loopBtn.classList.toggle('active', !!data.settings.loopOne); if (els.repeatBtn) els.repeatBtn.classList.toggle('active', data.settings.repeatMode === 'all'); + if (els.shuffleBtn) els.shuffleBtn.classList.toggle('active', !!data.settings.shuffle); } function playPrev() { if (Player.master.currentTime > 3) { Player.seek(0); return; } @@ -2486,6 +2525,7 @@ document.querySelectorAll('.chip').forEach((c) => { els.playBtn.addEventListener('click', () => Player.toggle()); els.nextBtn.addEventListener('click', playNext); els.prevBtn.addEventListener('click', playPrev); + els.shuffleBtn.addEventListener('click', toggleShuffle); els.loopBtn.addEventListener('click', toggleLoopOne); els.repeatBtn.addEventListener('click', toggleRepeat); els.fsBtn.addEventListener('click', () => { @@ -2590,6 +2630,7 @@ document.querySelectorAll('.chip').forEach((c) => { else if (e.key === 'm') els.muteBtn.click(); else if (e.key === 'l') toggleLoopOne(); else if (e.key === 'r') toggleRepeat(); + else if (e.key === 's') toggleShuffle(); else if (e.key === 'q') { if (current && current.meta) addToQueue(current.meta); } else if (e.key === 'a') { if (current) setAbA(); } else if (e.key === 'b') { if (current) setAbB(); } diff --git a/frontend/index.html b/frontend/index.html index 015cf2c..642c397 100755 --- a/frontend/index.html +++ b/frontend/index.html @@ -160,6 +160,7 @@ + @@ -293,6 +294,7 @@
Volume up / down
FToggle fullscreen
MToggle mute
+
SShuffle upcoming tracks
LLoop current video
RRepeat list when finished
QAdd current video to queue