fix(miniplayer): restore next/prev playback and remanent shuffle

cx
This commit is contained in:
vicrodh committed 2026-02-26 02:03:15 -06:00
1 parent 9302ffe5e6
commit 339f2b8106
2 files changed
+132 -17

No files matched your search

+66 -15
View File
@@ -608,7 +608,23 @@ impl QueueManager {
state.shuffle = enabled; state.shuffle = enabled;
if enabled { if enabled {
if let Some(curr_idx) = state.current_index {
if curr_idx < state.tracks.len() {
let mut remaining: Vec<usize> = ((curr_idx + 1)..state.tracks.len()).collect();
Self::shuffle_indices_internal(&mut remaining);
state.shuffle_order.clear();
state.shuffle_order.push(curr_idx);
state.shuffle_order.extend(remaining);
state.shuffle_position = 0;
return;
}
}
Self::regenerate_shuffle_order_internal(&mut state); Self::regenerate_shuffle_order_internal(&mut state);
} else {
state.shuffle_order.clear();
state.shuffle_position = 0;
} }
} }
@@ -682,7 +698,29 @@ impl QueueManager {
fn regenerate_shuffle_order_internal(state: &mut InternalState) { fn regenerate_shuffle_order_internal(state: &mut InternalState) {
let mut order: Vec<usize> = (0..state.tracks.len()).collect(); let mut order: Vec<usize> = (0..state.tracks.len()).collect();
// Fisher-Yates shuffle with proper PRNG Self::shuffle_indices_internal(&mut order);
state.shuffle_order = order;
// If there's a current track, find its position in the new shuffle order
// (don't move it to front, just update our position in the shuffled list)
if let Some(curr_idx) = state.current_index {
if let Some(pos) = state.shuffle_order.iter().position(|&x| x == curr_idx) {
state.shuffle_position = pos;
} else {
state.shuffle_position = 0;
}
} else {
state.shuffle_position = 0;
}
}
/// Shuffle a slice of queue indices using Fisher-Yates and a time-based seed.
fn shuffle_indices_internal(order: &mut [usize]) {
if order.len() <= 1 {
return;
}
use rand::{Rng, SeedableRng}; use rand::{Rng, SeedableRng};
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
@@ -698,20 +736,6 @@ impl QueueManager {
let j = rng.gen_range(0..=i); let j = rng.gen_range(0..=i);
order.swap(i, j); order.swap(i, j);
} }
state.shuffle_order = order;
// If there's a current track, find its position in the new shuffle order
// (don't move it to front, just update our position in the shuffled list)
if let Some(curr_idx) = state.current_index {
if let Some(pos) = state.shuffle_order.iter().position(|&x| x == curr_idx) {
state.shuffle_position = pos;
} else {
state.shuffle_position = 0;
}
} else {
state.shuffle_position = 0;
}
} }
} }
@@ -907,4 +931,31 @@ mod tests {
vec![2, 3, 4, 5] vec![2, 3, 4, 5]
); );
} }
#[test]
fn test_shuffle_reorders_only_remaining_tracks_when_current_exists() {
let queue = QueueManager::new();
for i in 1..=6 {
queue.add_track(create_test_track(i));
}
queue.play_index(2); // Current track = 3
queue.set_shuffle(true);
let state = queue.get_state();
assert_eq!(state.current_track.as_ref().map(|track| track.id), Some(3));
let mut upcoming_sorted = state.upcoming.iter().map(|track| track.id).collect::<Vec<u64>>();
upcoming_sorted.sort_unstable();
assert_eq!(upcoming_sorted, vec![4, 5, 6]);
let mut advanced = Vec::new();
for _ in 0..3 {
let track = queue.next().expect("expected next track while remanent queue exists");
advanced.push(track.id);
}
advanced.sort_unstable();
assert_eq!(advanced, vec![4, 5, 6]);
assert!(queue.next().is_none());
}
} }
+66 -2
View File
@@ -11,6 +11,8 @@
togglePlay, togglePlay,
seek as playerSeek, seek as playerSeek,
setVolume as playerSetVolume, setVolume as playerSetVolume,
setIsSkipping,
stop as stopPlayback,
startPolling, startPolling,
stopPolling, stopPolling,
type PlayerState type PlayerState
@@ -25,6 +27,7 @@
syncQueueState, syncQueueState,
startQueueEventListener, startQueueEventListener,
stopQueueEventListener, stopQueueEventListener,
type BackendQueueTrack,
type QueueState type QueueState
} from '$lib/stores/queueStore'; } from '$lib/stores/queueStore';
import { import {
@@ -44,6 +47,7 @@
setMiniPlayerSurface setMiniPlayerSurface
} from '$lib/stores/uiStore'; } from '$lib/stores/uiStore';
import { exitMiniplayerMode, setMiniplayerAlwaysOnTop } from '$lib/services/miniplayerService'; import { exitMiniplayerMode, setMiniplayerAlwaysOnTop } from '$lib/services/miniplayerService';
import { playTrack } from '$lib/services/playbackService';
let playerState = $state<PlayerState>(getPlayerState()); let playerState = $state<PlayerState>(getPlayerState());
let queueState = $state<QueueState>(getQueueState()); let queueState = $state<QueueState>(getQueueState());
@@ -278,19 +282,79 @@
} }
} }
async function playQueueTrack(track: BackendQueueTrack): Promise<void> {
const source = track.source ?? (track.is_local ? 'local' : 'qobuz');
const isLocal = source !== 'qobuz';
const quality = isLocal
? 'Local'
: track.bit_depth && track.sample_rate
? `${track.bit_depth}bit/${track.sample_rate}kHz`
: track.hires
? 'Hi-Res'
: '-';
await playTrack({
id: track.id,
title: track.title,
artist: track.artist,
album: track.album,
artwork: track.artwork_url || '',
duration: track.duration_secs,
quality,
bitDepth: track.bit_depth ?? undefined,
// Local/Plex backends report Hz while Qobuz queue state is already in kHz.
samplingRate: isLocal && track.sample_rate ? track.sample_rate / 1000 : track.sample_rate ?? undefined,
isLocal,
source,
albumId: track.album_id ?? undefined,
artistId: track.artist_id ?? undefined
}, {
isLocal,
source: source as 'qobuz' | 'local' | 'plex',
showLoadingToast: false,
showSuccessToast: false
});
}
async function handleSkipBack(): Promise<void> { async function handleSkipBack(): Promise<void> {
const state = getPlayerState();
if (!state.currentTrack || state.isSkipping) return;
if (state.currentTime > 3) {
playerSeek(0);
return;
}
setIsSkipping(true);
try { try {
await previousTrack(); const previous = await previousTrack();
if (previous) {
await playQueueTrack(previous);
} else {
playerSeek(0);
}
} catch (err) { } catch (err) {
console.error('[MiniPlayer] previousTrack failed:', err); console.error('[MiniPlayer] previousTrack failed:', err);
} finally {
setIsSkipping(false);
} }
} }
async function handleSkipForward(): Promise<void> { async function handleSkipForward(): Promise<void> {
const state = getPlayerState();
if (!state.currentTrack || state.isSkipping) return;
setIsSkipping(true);
try { try {
await nextTrack(); const next = await nextTrack();
if (next) {
await playQueueTrack(next);
} else {
await stopPlayback();
}
} catch (err) { } catch (err) {
console.error('[MiniPlayer] nextTrack failed:', err); console.error('[MiniPlayer] nextTrack failed:', err);
} finally {
setIsSkipping(false);
} }
} }