fix(miniplayer): restore next/prev playback and remanent shuffle
cx
This commit is contained in:
2 files changed
+132
-17
No files matched your search
+66
-15
@@ -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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user