mirror of
https://github.com/danbulant/despot
synced 2026-06-24 09:01:50 +00:00
working slidebar for player
This commit is contained in:
parent
35139b84a9
commit
4ccd69e628
3 changed files with 87 additions and 10 deletions
|
|
@ -1,5 +1,5 @@
|
||||||
|
|
||||||
[target.x86_64-unknown-linux-gnu]
|
[target.x86_64-unknown-linux-gnu]
|
||||||
# linker = "/usr/bin/clang-15"
|
# linker = "/usr/bin/clang-15"
|
||||||
# rustflags = ["-C", "link-arg=--ld-path=/home/dan/.nix-profile/bin/mold"]
|
rustflags = ["-C", "link-arg=--ld-path=/home/dan/.nix-profile/bin/mold"]
|
||||||
# rustflags = ["-C", "link-arg=-fuse-ld=/home/dan/.nix-profile/bin/mold"]
|
# rustflags = ["-C", "link-arg=-fuse-ld=/home/dan/.nix-profile/bin/mold"]
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,10 @@ use std::{
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
|
|
||||||
use cushy::value::{Destination, Dynamic};
|
use cushy::value::{Destination, Dynamic, Source};
|
||||||
use librespot_metadata::audio::AudioItem;
|
use librespot_metadata::audio::AudioItem;
|
||||||
use librespot_playback::player::{Player, PlayerEvent};
|
use librespot_playback::player::{Player, PlayerEvent};
|
||||||
|
use tokio::time;
|
||||||
|
|
||||||
pub type DynamicPlayer = Arc<DynamicPlayerInner>;
|
pub type DynamicPlayer = Arc<DynamicPlayerInner>;
|
||||||
|
|
||||||
|
|
@ -59,11 +60,38 @@ impl DynamicPlayerInner {
|
||||||
track_progress: Default::default(),
|
track_progress: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
fn update_position(&self) {
|
||||||
|
let state = self.state.get();
|
||||||
|
let track_progress = match state {
|
||||||
|
PlayerState::Loading {
|
||||||
|
loading_at: duration,
|
||||||
|
}
|
||||||
|
| PlayerState::Paused {
|
||||||
|
paused_at: duration,
|
||||||
|
} => Some(duration),
|
||||||
|
PlayerState::Stopped | PlayerState::Disconnected => None,
|
||||||
|
PlayerState::Playing => {
|
||||||
|
let started_at = *self.started_at.lock().unwrap();
|
||||||
|
let started_at = started_at.unwrap_or_else(Instant::now);
|
||||||
|
let position = Instant::now() - started_at;
|
||||||
|
Some(position)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
self.track_progress.set(track_progress);
|
||||||
|
}
|
||||||
|
/// Run the DynamicPlayer event loop
|
||||||
|
/// This updates the player state and track progress
|
||||||
|
/// Run this only once per player (usually once per app)
|
||||||
pub async fn run(&self) {
|
pub async fn run(&self) {
|
||||||
let mut channel = self.player.get_player_event_channel();
|
let mut channel = self.player.get_player_event_channel();
|
||||||
|
let mut interval = time::interval(time::Duration::from_millis(100));
|
||||||
|
interval.set_missed_tick_behavior(time::MissedTickBehavior::Skip);
|
||||||
let mut id = 0u64;
|
let mut id = 0u64;
|
||||||
loop {
|
loop {
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
|
_ = interval.tick() => {
|
||||||
|
self.update_position();
|
||||||
|
}
|
||||||
event = channel.recv() => {
|
event = channel.recv() => {
|
||||||
if let Some(event) = event {
|
if let Some(event) = event {
|
||||||
match event {
|
match event {
|
||||||
|
|
@ -82,6 +110,7 @@ impl DynamicPlayerInner {
|
||||||
self.state.set(PlayerState::Loading {
|
self.state.set(PlayerState::Loading {
|
||||||
loading_at: Duration::from_millis(position_ms as u64),
|
loading_at: Duration::from_millis(position_ms as u64),
|
||||||
});
|
});
|
||||||
|
self.update_position();
|
||||||
}
|
}
|
||||||
PlayerEvent::Playing {
|
PlayerEvent::Playing {
|
||||||
play_request_id,
|
play_request_id,
|
||||||
|
|
@ -92,6 +121,7 @@ impl DynamicPlayerInner {
|
||||||
self.state.set(PlayerState::Playing);
|
self.state.set(PlayerState::Playing);
|
||||||
*self.started_at.lock().unwrap() =
|
*self.started_at.lock().unwrap() =
|
||||||
Some(Instant::now() - Duration::from_millis(position_ms as u64));
|
Some(Instant::now() - Duration::from_millis(position_ms as u64));
|
||||||
|
self.update_position();
|
||||||
}
|
}
|
||||||
PlayerEvent::Paused {
|
PlayerEvent::Paused {
|
||||||
play_request_id,
|
play_request_id,
|
||||||
|
|
@ -102,6 +132,7 @@ impl DynamicPlayerInner {
|
||||||
self.state.set(PlayerState::Paused {
|
self.state.set(PlayerState::Paused {
|
||||||
paused_at: Duration::from_millis(position_ms as u64),
|
paused_at: Duration::from_millis(position_ms as u64),
|
||||||
});
|
});
|
||||||
|
self.update_position();
|
||||||
}
|
}
|
||||||
PlayerEvent::Unavailable {
|
PlayerEvent::Unavailable {
|
||||||
play_request_id,
|
play_request_id,
|
||||||
|
|
@ -121,6 +152,7 @@ impl DynamicPlayerInner {
|
||||||
println!("PositionCorrection {play_request_id} {position_ms} {track_id}");
|
println!("PositionCorrection {play_request_id} {position_ms} {track_id}");
|
||||||
*self.started_at.lock().unwrap() =
|
*self.started_at.lock().unwrap() =
|
||||||
Some(Instant::now() - Duration::from_millis(position_ms as u64));
|
Some(Instant::now() - Duration::from_millis(position_ms as u64));
|
||||||
|
self.update_position();
|
||||||
}
|
}
|
||||||
PlayerEvent::Seeked {
|
PlayerEvent::Seeked {
|
||||||
play_request_id,
|
play_request_id,
|
||||||
|
|
@ -130,11 +162,13 @@ impl DynamicPlayerInner {
|
||||||
println!("Seeked {play_request_id} {position_ms} {track_id}");
|
println!("Seeked {play_request_id} {position_ms} {track_id}");
|
||||||
*self.started_at.lock().unwrap() =
|
*self.started_at.lock().unwrap() =
|
||||||
Some(Instant::now() - Duration::from_millis(position_ms as u64));
|
Some(Instant::now() - Duration::from_millis(position_ms as u64));
|
||||||
|
self.update_position();
|
||||||
}
|
}
|
||||||
PlayerEvent::TrackChanged { audio_item } => {
|
PlayerEvent::TrackChanged { audio_item } => {
|
||||||
println!("TrackChanged {}", audio_item.uri);
|
println!("TrackChanged {}", audio_item.uri);
|
||||||
dbg!(&audio_item);
|
dbg!(&audio_item);
|
||||||
*self.track.lock() = Some(audio_item);
|
*self.track.lock() = Some(audio_item);
|
||||||
|
self.update_position();
|
||||||
}
|
}
|
||||||
PlayerEvent::SessionConnected {
|
PlayerEvent::SessionConnected {
|
||||||
connection_id,
|
connection_id,
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
use cushy::{
|
use cushy::{
|
||||||
figures::{units::Lp, Size},
|
figures::{units::Lp, Size},
|
||||||
styles::{Dimension, DimensionRange},
|
styles::{Dimension, DimensionRange},
|
||||||
value::Source,
|
value::{Dynamic, Source},
|
||||||
widget::MakeWidget,
|
widget::MakeWidget,
|
||||||
widgets::{Button, Image, Label},
|
widgets::{Button, Image, Label, Slider},
|
||||||
};
|
};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use librespot_metadata::audio::UniqueFields;
|
use librespot_metadata::audio::UniqueFields;
|
||||||
|
|
@ -16,17 +18,17 @@ use crate::{
|
||||||
pub fn bar(player: DynamicPlayer) -> impl MakeWidget {
|
pub fn bar(player: DynamicPlayer) -> impl MakeWidget {
|
||||||
meta(player).size(Size {
|
meta(player).size(Size {
|
||||||
width: DimensionRange::default(),
|
width: DimensionRange::default(),
|
||||||
height: Dimension::Lp(Lp::inches_f(1.5)).into(),
|
height: Dimension::Lp(Lp::inches_f(1.)).into(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn meta(player: DynamicPlayer) -> impl MakeWidget {
|
fn meta(player: DynamicPlayer) -> impl MakeWidget {
|
||||||
Image::new_empty()
|
Image::new_empty()
|
||||||
.with_url(player.track.map_each(|track| {
|
.with_url(player.track.map_each(|track| {
|
||||||
track
|
dbg!(track
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|track| track.covers.first().map(|cover| cover.url.clone()))
|
.map(|track| track.covers.first().map(|cover| cover.url.clone()))
|
||||||
.flatten()
|
.flatten())
|
||||||
}))
|
}))
|
||||||
.size(Size::squared(Dimension::Lp(Lp::inches_f(1.))))
|
.size(Size::squared(Dimension::Lp(Lp::inches_f(1.))))
|
||||||
.and(
|
.and(
|
||||||
|
|
@ -64,6 +66,11 @@ fn meta(player: DynamicPlayer) -> impl MakeWidget {
|
||||||
.expand(),
|
.expand(),
|
||||||
)
|
)
|
||||||
.into_columns()
|
.into_columns()
|
||||||
|
.align_left()
|
||||||
|
.expand()
|
||||||
|
.and(controls(player.clone()).expand())
|
||||||
|
.and(vol(player).align_right().expand())
|
||||||
|
.into_columns()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn controls(player: DynamicPlayer) -> impl MakeWidget {
|
fn controls(player: DynamicPlayer) -> impl MakeWidget {
|
||||||
|
|
@ -82,12 +89,48 @@ fn controls(player: DynamicPlayer) -> impl MakeWidget {
|
||||||
.and("skip".into_button())
|
.and("skip".into_button())
|
||||||
.and("repeat".into_button())
|
.and("repeat".into_button())
|
||||||
.into_columns()
|
.into_columns()
|
||||||
.and(progress(player))
|
.centered()
|
||||||
|
.and(time(player))
|
||||||
.into_rows()
|
.into_rows()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn progress(player: DynamicPlayer) -> impl MakeWidget {
|
fn time(player: DynamicPlayer) -> impl MakeWidget {
|
||||||
"progress bar here"
|
let duration = player.track.map_each(|track| {
|
||||||
|
track
|
||||||
|
.as_ref()
|
||||||
|
.map(|track| track.duration_ms as f64 / 1000.)
|
||||||
|
.unwrap_or(0.)
|
||||||
|
});
|
||||||
|
let slider = Slider::from_value(player.track_progress.map_each(|progress| {
|
||||||
|
progress
|
||||||
|
.map(|progress| progress.as_secs_f64())
|
||||||
|
.unwrap_or(0.)
|
||||||
|
}))
|
||||||
|
.minimum(0.)
|
||||||
|
.maximum(duration.clone())
|
||||||
|
.size(Size::<DimensionRange> {
|
||||||
|
height: Dimension::Lp(Lp::inches_f(0.2)).into(),
|
||||||
|
width: (..Dimension::Lp(Lp::inches_f(5.))).into(),
|
||||||
|
});
|
||||||
|
player
|
||||||
|
.track_progress
|
||||||
|
.map_each(|progress| {
|
||||||
|
progress
|
||||||
|
.map(|progress| format_time(progress))
|
||||||
|
.unwrap_or_else(|| "0:00".to_string())
|
||||||
|
})
|
||||||
|
.and(slider.expand_horizontally())
|
||||||
|
.and(duration.map_each(|duration| format_time(Duration::from_secs_f64(*duration))))
|
||||||
|
.into_columns()
|
||||||
|
.expand_horizontally()
|
||||||
|
.centered()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_time(time: Duration) -> String {
|
||||||
|
let time = time.as_secs_f64();
|
||||||
|
let seconds = time % 60.;
|
||||||
|
let minutes = time / 60.;
|
||||||
|
format!("{}:{:02}", minutes.round(), seconds.round())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn vol(player: DynamicPlayer) -> impl MakeWidget {
|
fn vol(player: DynamicPlayer) -> impl MakeWidget {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue