diff --git a/.cargo/config.toml b/.cargo/config.toml index 6e30eb5..49c50ea 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,5 +1,5 @@ [target.x86_64-unknown-linux-gnu] # 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"] diff --git a/src/player.rs b/src/player.rs index 81b80f4..fa73494 100644 --- a/src/player.rs +++ b/src/player.rs @@ -3,9 +3,10 @@ use std::{ time::{Duration, Instant}, }; -use cushy::value::{Destination, Dynamic}; +use cushy::value::{Destination, Dynamic, Source}; use librespot_metadata::audio::AudioItem; use librespot_playback::player::{Player, PlayerEvent}; +use tokio::time; pub type DynamicPlayer = Arc; @@ -59,11 +60,38 @@ impl DynamicPlayerInner { 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) { 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; loop { tokio::select! { + _ = interval.tick() => { + self.update_position(); + } event = channel.recv() => { if let Some(event) = event { match event { @@ -82,6 +110,7 @@ impl DynamicPlayerInner { self.state.set(PlayerState::Loading { loading_at: Duration::from_millis(position_ms as u64), }); + self.update_position(); } PlayerEvent::Playing { play_request_id, @@ -92,6 +121,7 @@ impl DynamicPlayerInner { self.state.set(PlayerState::Playing); *self.started_at.lock().unwrap() = Some(Instant::now() - Duration::from_millis(position_ms as u64)); + self.update_position(); } PlayerEvent::Paused { play_request_id, @@ -102,6 +132,7 @@ impl DynamicPlayerInner { self.state.set(PlayerState::Paused { paused_at: Duration::from_millis(position_ms as u64), }); + self.update_position(); } PlayerEvent::Unavailable { play_request_id, @@ -121,6 +152,7 @@ impl DynamicPlayerInner { println!("PositionCorrection {play_request_id} {position_ms} {track_id}"); *self.started_at.lock().unwrap() = Some(Instant::now() - Duration::from_millis(position_ms as u64)); + self.update_position(); } PlayerEvent::Seeked { play_request_id, @@ -130,11 +162,13 @@ impl DynamicPlayerInner { println!("Seeked {play_request_id} {position_ms} {track_id}"); *self.started_at.lock().unwrap() = Some(Instant::now() - Duration::from_millis(position_ms as u64)); + self.update_position(); } PlayerEvent::TrackChanged { audio_item } => { println!("TrackChanged {}", audio_item.uri); dbg!(&audio_item); *self.track.lock() = Some(audio_item); + self.update_position(); } PlayerEvent::SessionConnected { connection_id, diff --git a/src/widgets/playback/bar.rs b/src/widgets/playback/bar.rs index 299f899..f86ed27 100644 --- a/src/widgets/playback/bar.rs +++ b/src/widgets/playback/bar.rs @@ -1,9 +1,11 @@ +use std::time::Duration; + use cushy::{ figures::{units::Lp, Size}, styles::{Dimension, DimensionRange}, - value::Source, + value::{Dynamic, Source}, widget::MakeWidget, - widgets::{Button, Image, Label}, + widgets::{Button, Image, Label, Slider}, }; use itertools::Itertools; use librespot_metadata::audio::UniqueFields; @@ -16,17 +18,17 @@ use crate::{ pub fn bar(player: DynamicPlayer) -> impl MakeWidget { meta(player).size(Size { 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 { Image::new_empty() .with_url(player.track.map_each(|track| { - track + dbg!(track .as_ref() .map(|track| track.covers.first().map(|cover| cover.url.clone())) - .flatten() + .flatten()) })) .size(Size::squared(Dimension::Lp(Lp::inches_f(1.)))) .and( @@ -64,6 +66,11 @@ fn meta(player: DynamicPlayer) -> impl MakeWidget { .expand(), ) .into_columns() + .align_left() + .expand() + .and(controls(player.clone()).expand()) + .and(vol(player).align_right().expand()) + .into_columns() } fn controls(player: DynamicPlayer) -> impl MakeWidget { @@ -82,12 +89,48 @@ fn controls(player: DynamicPlayer) -> impl MakeWidget { .and("skip".into_button()) .and("repeat".into_button()) .into_columns() - .and(progress(player)) + .centered() + .and(time(player)) .into_rows() } -fn progress(player: DynamicPlayer) -> impl MakeWidget { - "progress bar here" +fn time(player: DynamicPlayer) -> impl MakeWidget { + 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:: { + 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 {