feat(slint): wire shuffle, repeat and volume buttons in player bar

Shuffle toggles via core; repeat cycles off/all/one and swaps to the
repeat-1 glyph for one-track mode. Volume minus/plus step the level
by 5%. State flows back through NowPlayingState.

cc
This commit is contained in:
vicrodh committed 2026-05-18 22:43:31 -06:00
1 parent f283de57e3
commit c273506800
4 files changed
+96 -5

No files matched your search

+21
View File
@@ -658,6 +658,27 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
});
}
{
let runtime = app_runtime.clone();
let weak = window.as_weak();
let handle = tokio_rt.handle().clone();
window
.global::<NowPlayingState>()
.on_toggle_shuffle(move || {
playback::toggle_shuffle(runtime.clone(), weak.clone(), handle.clone());
});
}
{
let runtime = app_runtime.clone();
let weak = window.as_weak();
let handle = tokio_rt.handle().clone();
window
.global::<NowPlayingState>()
.on_cycle_repeat(move || {
playback::cycle_repeat(runtime.clone(), weak.clone(), handle.clone());
});
}
// Queue sidebar — clicking a row jumps to that queue index.
{
let runtime = app_runtime.clone();
+39 -1
View File
@@ -14,7 +14,7 @@
use std::sync::Arc;
use qbz_app::shell::AppRuntime;
use qbz_models::{Quality, QueueTrack};
use qbz_models::{Quality, QueueTrack, RepeatMode};
use slint::ComponentHandle;
use crate::adapter::SlintAdapter;
@@ -469,6 +469,44 @@ pub fn set_volume(runtime: Runtime, handle: tokio::runtime::Handle, fraction: f3
});
}
/// Toggle shuffle on the queue and reflect the new state on NowPlayingState.
pub fn toggle_shuffle(
runtime: Runtime,
weak: slint::Weak<AppWindow>,
handle: tokio::runtime::Handle,
) {
handle.spawn(async move {
let on = runtime.core().toggle_shuffle().await;
let _ = weak.upgrade_in_event_loop(move |w| {
w.global::<NowPlayingState>().set_shuffle(on);
});
});
}
/// Cycle the repeat mode Off -> All -> One -> Off and reflect it.
pub fn cycle_repeat(
runtime: Runtime,
weak: slint::Weak<AppWindow>,
handle: tokio::runtime::Handle,
) {
handle.spawn(async move {
let next = match runtime.core().get_queue_state().await.repeat {
RepeatMode::Off => RepeatMode::All,
RepeatMode::All => RepeatMode::One,
RepeatMode::One => RepeatMode::Off,
};
runtime.core().set_repeat_mode(next).await;
let mode: i32 = match next {
RepeatMode::Off => 0,
RepeatMode::All => 1,
RepeatMode::One => 2,
};
let _ = weak.upgrade_in_event_loop(move |w| {
w.global::<NowPlayingState>().set_repeat_mode(mode);
});
});
}
#[cfg(test)]
mod tests {
use super::*;
+32 -4
View File
@@ -134,7 +134,14 @@ export component PlayerBar inherits Rectangle {
alignment: center;
HorizontalLayout {
spacing: 2px;
IconButton { icon: @image-url("../assets/icons/shuffle.svg"); icon-size: 16px; }
IconButton {
icon: @image-url("../assets/icons/shuffle.svg");
icon-size: 16px;
active: NowPlayingState.shuffle;
clicked => {
NowPlayingState.toggle-shuffle();
}
}
IconButton {
icon: @image-url("../assets/icons/skip-back.svg");
clicked => {
@@ -158,7 +165,16 @@ export component PlayerBar inherits Rectangle {
NowPlayingState.next();
}
}
IconButton { icon: @image-url("../assets/icons/repeat.svg"); icon-size: 16px; }
IconButton {
icon: NowPlayingState.repeat-mode == 2
? @image-url("../assets/icons/repeat-1.svg")
: @image-url("../assets/icons/repeat.svg");
icon-size: 16px;
active: NowPlayingState.repeat-mode != 0;
clicked => {
NowPlayingState.cycle-repeat();
}
}
IconButton { icon: @image-url("../assets/icons/plus.svg"); icon-size: 16px; }
IconButton { icon: @image-url("../assets/icons/heart.svg"); icon-size: 16px; }
}
@@ -256,8 +272,20 @@ export component PlayerBar inherits Rectangle {
}
}
}
IconButton { icon: @image-url("../assets/icons/minus.svg"); icon-size: 15px; }
IconButton { icon: @image-url("../assets/icons/plus.svg"); icon-size: 15px; }
IconButton {
icon: @image-url("../assets/icons/minus.svg");
icon-size: 15px;
clicked => {
NowPlayingState.set-volume(Math.max(0.0, NowPlayingState.volume - 0.05));
}
}
IconButton {
icon: @image-url("../assets/icons/plus.svg");
icon-size: 15px;
clicked => {
NowPlayingState.set-volume(Math.min(1.0, NowPlayingState.volume + 0.05));
}
}
}
}
}
+4
View File
@@ -212,6 +212,8 @@ export global NowPlayingState {
in property <string> elapsed: "0:00";
in property <string> remaining: "0:00";
in property <float> volume: 0.7; // 0..1
in property <bool> shuffle: false;
in property <int> repeat-mode: 0; // 0 off, 1 all, 2 one
// Transport callbacks, wired to the Rust playback controller.
callback toggle-play();
@@ -219,6 +221,8 @@ export global NowPlayingState {
callback previous();
callback seek(float); // 0..1
callback set-volume(float); // 0..1
callback toggle-shuffle();
callback cycle-repeat();
}
// Playback queue model, for the Queue sidebar.