diff --git a/psst-core/src/player/worker.rs b/psst-core/src/player/worker.rs index 9102c6b2..da5a5c39 100644 --- a/psst-core/src/player/worker.rs +++ b/psst-core/src/player/worker.rs @@ -112,7 +112,7 @@ impl DecoderSource { norm_factor: f32, event_send: Sender, ) -> Self { - const REPORT_PRECISION: Duration = Duration::from_millis(900); + const REPORT_PRECISION: Duration = Duration::from_millis(200); // Gather the source signal parameters and compute how often we should report // the play-head position. diff --git a/psst-gui/src/data/mod.rs b/psst-gui/src/data/mod.rs index f0a5dab9..6dbd0bd0 100644 --- a/psst-gui/src/data/mod.rs +++ b/psst-gui/src/data/mod.rs @@ -88,6 +88,7 @@ pub struct AppState { pub added_queue: Vector, pub lyrics: Promise>, pub credits: Option, + pub lyrics_offset: Option, } impl AppState { @@ -104,6 +105,7 @@ impl AppState { library: Arc::clone(&library), show_track_cover: config.show_track_cover, nav: Nav::Home, + progress: Duration::default(), }); let playback = Playback { state: PlaybackState::Stopped, @@ -170,6 +172,7 @@ impl AppState { finder: Finder::new(), lyrics: Promise::Empty, credits: None, + lyrics_offset: None, } } } @@ -236,6 +239,7 @@ impl AppState { pub fn loading_playback(&mut self, item: Playable, origin: PlaybackOrigin) { self.common_ctx_mut().now_playing.take(); + self.set_common_progress(Duration::default()); self.playback.state = PlaybackState::Loading; self.playback.now_playing.replace(NowPlaying { item, @@ -247,6 +251,7 @@ impl AppState { pub fn start_playback(&mut self, item: Playable, origin: PlaybackOrigin, progress: Duration) { self.common_ctx_mut().now_playing.replace(item.clone()); + self.set_common_progress(progress); self.playback.state = PlaybackState::Playing; self.playback.now_playing.replace(NowPlaying { item, @@ -260,6 +265,7 @@ impl AppState { if let Some(now_playing) = &mut self.playback.now_playing { now_playing.progress = progress; } + self.set_common_progress(progress); } pub fn pause_playback(&mut self) { @@ -271,13 +277,14 @@ impl AppState { } pub fn block_playback(&mut self) { - // TODO: Figure out how to signal blocked playback properly. + self.playback.state = PlaybackState::Loading; } pub fn stop_playback(&mut self) { self.playback.state = PlaybackState::Stopped; self.playback.now_playing.take(); self.common_ctx_mut().now_playing.take(); + self.set_common_progress(Duration::default()); } pub fn set_queue_behavior(&mut self, queue_behavior: QueueBehavior) { @@ -335,6 +342,18 @@ impl AppState { } } +impl AppState { + fn set_common_progress(&mut self, progress: Duration) { + let mut ctx = (*self.common_ctx).clone(); + ctx.progress = progress; + self.common_ctx = Arc::new(ctx); + } + + pub fn reset_lyrics_offset(&mut self) { + self.lyrics_offset = None; + } +} + #[derive(Clone, Data, Lens)] pub struct Library { pub user_profile: Promise, @@ -541,12 +560,19 @@ pub struct CommonCtx { pub library: Arc, pub show_track_cover: bool, pub nav: Nav, + pub progress: Duration, } impl CommonCtx { pub fn is_playing(&self, item: &Playable) -> bool { matches!(&self.now_playing, Some(i) if i.same(item)) } + + pub fn current_progress(&self) -> Duration { + // Use the stored progress directly for better accuracy + // The progress is updated by the audio player events + self.progress + } } pub type WithCtx = Ctx, T>; diff --git a/psst-gui/src/ui/lyrics.rs b/psst-gui/src/ui/lyrics.rs index 5e0d0ad7..6189d544 100644 --- a/psst-gui/src/ui/lyrics.rs +++ b/psst-gui/src/ui/lyrics.rs @@ -3,12 +3,16 @@ use druid::{Insets, LensExt, Selector, Widget, WidgetExt}; use crate::cmd; use crate::data::{AppState, Ctx, NowPlaying, Playable, TrackLines}; +use crate::data::CommonCtx; use crate::widget::MyWidgetExt; use crate::{webapi::WebApi, widget::Async}; use super::theme; use super::utils; +use std::sync::Arc; +use druid::im::Vector; + pub const SHOW_LYRICS: Selector = Selector::new("app.home.show_lyrics"); pub fn lyrics_widget() -> impl Widget { @@ -73,7 +77,34 @@ fn track_lyrics_widget() -> impl Widget { .center() .padding(Insets::uniform_xy(theme::grid(1.0), theme::grid(0.5))) .link() + .active(|c: &Ctx, TrackLines>, _env| { + let current_progress = c.ctx.current_progress().as_millis() as f64; + let start_ms = c.data.start_time_ms.parse::().unwrap_or(0.0); + let parsed_end = c.data.end_time_ms.parse::().unwrap_or(0.0); + let end_ms = if parsed_end > start_ms { parsed_end } else { start_ms + 800.0 }; + current_progress >= start_ms && current_progress < end_ms + }) .rounded(theme::BUTTON_BORDER_RADIUS) + .env_scope(|env, _| { + let active = env.get(theme::BLUE_100).with_alpha(0.25); + env.set(theme::LINK_ACTIVE_COLOR, active); + }) + .on_update(|ctx, old, new, _env| { + let is_line_active = |ctx: &Arc, line: &TrackLines| { + let current_progress = ctx.current_progress().as_millis() as f64; + let start_ms = line.start_time_ms.parse::().unwrap_or(0.0); + let parsed_end = line.end_time_ms.parse::().unwrap_or(0.0); + let end_ms = if parsed_end > start_ms { parsed_end } else { start_ms + 800.0 }; + current_progress >= start_ms && current_progress < end_ms + }; + + let was_active = is_line_active(&old.ctx, &old.data); + let is_active = is_line_active(&new.ctx, &new.data); + + if !was_active && is_active { + ctx.scroll_to_view(); + } + }) .on_left_click(|ctx, _, c, _| { if c.data.start_time_ms.parse::().unwrap() != 0 { ctx.submit_command( @@ -91,6 +122,31 @@ fn track_lyrics_widget() -> impl Widget { SHOW_LYRICS, |t| WebApi::global().get_lyrics(t.item.id().to_base62()), |_, data, _| data.lyrics.defer(()), - |_, data, r| data.lyrics.update(((), r.1)), + |_, data, r| { + data.reset_lyrics_offset(); + let processed = match r.1 { + Ok(lines) => { + let mut out = Vector::new(); + let len = lines.len(); + for idx in 0..len { + let mut l = lines[idx].clone(); + let end_zero = l.end_time_ms.parse::().unwrap_or(0) == 0; + if end_zero { + if idx + 1 < len { + l.end_time_ms = lines[idx + 1].start_time_ms.clone(); + } else { + if let Ok(start) = l.start_time_ms.parse::() { + l.end_time_ms = (start + 800).to_string(); + } + } + } + out.push_back(l); + } + Ok(out) + } + Err(e) => Err(e), + }; + data.lyrics.update(((), processed)); + }, ) }