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:
4 files changed
+96
-5
No files matched your search
@@ -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();
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user