From d3631fa377cff0bb4a39633f1d6b15e6e0986e2b Mon Sep 17 00:00:00 2001 From: Cleboost Date: Tue, 8 Jul 2025 00:14:12 +0200 Subject: [PATCH 1/4] Sync lyrics highlighting with playback progress Adds progress and last_update_ms to CommonCtx and updates AppState to keep these in sync with playback. Implements a timer-driven LyricsTicker controller to update the lyrics view, highlights the current lyric line based on playback progress, and allows for offset correction when a line is clicked. Also processes lyrics end times for more accurate highlighting. --- psst-gui/src/data/mod.rs | 22 +++++++ psst-gui/src/ui/lyrics.rs | 128 +++++++++++++++++++++++++++++++++++++- 2 files changed, 149 insertions(+), 1 deletion(-) diff --git a/psst-gui/src/data/mod.rs b/psst-gui/src/data/mod.rs index f0a5dab9..5843d9ce 100644 --- a/psst-gui/src/data/mod.rs +++ b/psst-gui/src/data/mod.rs @@ -104,6 +104,8 @@ impl AppState { library: Arc::clone(&library), show_track_cover: config.show_track_cover, nav: Nav::Home, + progress: Duration::default(), + last_update_ms: 0, }); let playback = Playback { state: PlaybackState::Stopped, @@ -236,6 +238,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 +250,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 +264,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) { @@ -278,6 +283,7 @@ impl AppState { 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 +341,20 @@ impl AppState { } } +impl AppState { + fn set_common_progress(&mut self, progress: Duration) { + let mut ctx = (*self.common_ctx).clone(); + ctx.progress = progress; + ctx.last_update_ms = current_millis(); + self.common_ctx = Arc::new(ctx); + } +} + +fn current_millis() -> u64 { + use std::time::{SystemTime, UNIX_EPOCH}; + SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis() as u64 +} + #[derive(Clone, Data, Lens)] pub struct Library { pub user_profile: Promise, @@ -541,6 +561,8 @@ pub struct CommonCtx { pub library: Arc, pub show_track_cover: bool, pub nav: Nav, + pub progress: Duration, + pub last_update_ms: u64, } impl CommonCtx { diff --git a/psst-gui/src/ui/lyrics.rs b/psst-gui/src/ui/lyrics.rs index 5e0d0ad7..fbdc693e 100644 --- a/psst-gui/src/ui/lyrics.rs +++ b/psst-gui/src/ui/lyrics.rs @@ -3,14 +3,71 @@ 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; +use std::sync::{Mutex, OnceLock}; +use std::time::{SystemTime, UNIX_EPOCH}; +use druid::{TimerToken, widget::prelude::*, widget::Controller}; + pub const SHOW_LYRICS: Selector = Selector::new("app.home.show_lyrics"); +static LYRICS_OFFSET: OnceLock>> = OnceLock::new(); + +fn offset_storage() -> &'static Mutex> { + LYRICS_OFFSET.get_or_init(|| Mutex::new(None)) +} + +const TICK_INTERVAL_MS: u64 = 100; + +struct LyricsTicker { + timer: Option, +} + +impl LyricsTicker { + fn new() -> Self { + Self { timer: None } + } +} + +impl Controller for LyricsTicker +where + W: Widget, +{ + fn lifecycle(&mut self, child: &mut W, ctx: &mut LifeCycleCtx, event: &LifeCycle, data: &AppState, env: &Env) { + match event { + LifeCycle::WidgetAdded => { + let tok = ctx.request_timer(std::time::Duration::from_millis(TICK_INTERVAL_MS)); + self.timer = Some(tok); + } + _ => {} + } + child.lifecycle(ctx, event, data, env); + } + + fn event(&mut self, child: &mut W, ctx: &mut EventCtx, event: &Event, data: &mut AppState, env: &Env) { + match event { + Event::Timer(token) if Some(*token) == self.timer => { + ctx.request_paint(); + let tok = ctx.request_timer(std::time::Duration::from_millis(TICK_INTERVAL_MS)); + self.timer = Some(tok); + } + _ => {} + } + child.event(ctx, event, data, env); + } + + fn update(&mut self, child: &mut W, ctx: &mut UpdateCtx, _old_data: &AppState, data: &AppState, env: &Env) { + child.update(ctx, _old_data, data, env); + } +} + pub fn lyrics_widget() -> impl Widget { Scroll::new( Container::new( @@ -25,6 +82,7 @@ pub fn lyrics_widget() -> impl Widget { .center(), ) .vertical() + .controller(LyricsTicker::new()) } fn track_info_widget() -> impl Widget { @@ -73,7 +131,50 @@ 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 base_progress_ms = c.ctx.progress.as_millis() as f64; + let now_ms = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis() as f64; + let elapsed = now_ms - c.ctx.last_update_ms as f64; + let progress_ms = base_progress_ms + elapsed; + let offset = offset_storage().lock().unwrap().unwrap_or(0.0); + let adj_progress = progress_ms + offset; + 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 }; + adj_progress >= start_ms && adj_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 calculate_progress = |ctx: &Arc, offset: f64| { + let base_progress_ms = ctx.progress.as_millis() as f64; + let now_ms = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis() as f64; + let elapsed = now_ms - ctx.last_update_ms as f64; + base_progress_ms + elapsed + offset + }; + + let is_line_active = |ctx: &Arc, line: &TrackLines, offset: f64| { + let adj_progress = calculate_progress(ctx, offset); + 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 }; + adj_progress >= start_ms && adj_progress < end_ms + }; + + let offset = offset_storage().lock().unwrap().unwrap_or(0.0); + let was_active = is_line_active(&old.ctx, &old.data, offset); + let is_active = is_line_active(&new.ctx, &new.data, offset); + + if !was_active && is_active { + let mut storage = offset_storage().lock().unwrap(); + let new_offset = new.ctx.progress.as_millis() as f64 - new.data.start_time_ms.parse::().unwrap_or(0.0); + *storage = Some(new_offset); + ctx.scroll_to_view(); + } + }) .on_left_click(|ctx, _, c, _| { if c.data.start_time_ms.parse::().unwrap() != 0 { ctx.submit_command( @@ -91,6 +192,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| { + *offset_storage().lock().unwrap() = None; + 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)); + }, ) } From f3d72deb852637f1cd02a7cce147966f51855b67 Mon Sep 17 00:00:00 2001 From: Cleboost Date: Tue, 8 Jul 2025 00:41:43 +0200 Subject: [PATCH 2/4] Sync playback state in CommonCtx and update lyrics timing Added playback_state to CommonCtx and ensured it is updated alongside AppState's playback state. Modified lyrics widget to only advance progress when playback is playing, improving accuracy of lyrics timing during pause and resume. --- psst-gui/src/data/mod.rs | 9 +++++++++ psst-gui/src/ui/lyrics.rs | 14 +++++++++++--- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/psst-gui/src/data/mod.rs b/psst-gui/src/data/mod.rs index 5843d9ce..9abd2f41 100644 --- a/psst-gui/src/data/mod.rs +++ b/psst-gui/src/data/mod.rs @@ -106,6 +106,7 @@ impl AppState { nav: Nav::Home, progress: Duration::default(), last_update_ms: 0, + playback_state: PlaybackState::Stopped, }); let playback = Playback { state: PlaybackState::Stopped, @@ -239,6 +240,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.common_ctx_mut().playback_state = PlaybackState::Loading; self.playback.state = PlaybackState::Loading; self.playback.now_playing.replace(NowPlaying { item, @@ -251,6 +253,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.common_ctx_mut().playback_state = PlaybackState::Playing; self.playback.state = PlaybackState::Playing; self.playback.now_playing.replace(NowPlaying { item, @@ -269,10 +272,14 @@ impl AppState { pub fn pause_playback(&mut self) { self.playback.state = PlaybackState::Paused; + self.common_ctx_mut().playback_state = PlaybackState::Paused; + self.common_ctx_mut().last_update_ms = current_millis(); } pub fn resume_playback(&mut self) { self.playback.state = PlaybackState::Playing; + self.common_ctx_mut().playback_state = PlaybackState::Playing; + self.common_ctx_mut().last_update_ms = current_millis(); } pub fn block_playback(&mut self) { @@ -283,6 +290,7 @@ impl AppState { self.playback.state = PlaybackState::Stopped; self.playback.now_playing.take(); self.common_ctx_mut().now_playing.take(); + self.common_ctx_mut().playback_state = PlaybackState::Stopped; self.set_common_progress(Duration::default()); } @@ -563,6 +571,7 @@ pub struct CommonCtx { pub nav: Nav, pub progress: Duration, pub last_update_ms: u64, + pub playback_state: PlaybackState, } impl CommonCtx { diff --git a/psst-gui/src/ui/lyrics.rs b/psst-gui/src/ui/lyrics.rs index fbdc693e..8924e141 100644 --- a/psst-gui/src/ui/lyrics.rs +++ b/psst-gui/src/ui/lyrics.rs @@ -2,7 +2,7 @@ use druid::widget::{Container, CrossAxisAlignment, Flex, Label, LineBreaking, Li use druid::{Insets, LensExt, Selector, Widget, WidgetExt}; use crate::cmd; -use crate::data::{AppState, Ctx, NowPlaying, Playable, TrackLines}; +use crate::data::{AppState, Ctx, NowPlaying, Playable, TrackLines, PlaybackState}; use crate::data::CommonCtx; use crate::widget::MyWidgetExt; use crate::{webapi::WebApi, widget::Async}; @@ -134,7 +134,11 @@ fn track_lyrics_widget() -> impl Widget { .active(|c: &Ctx, TrackLines>, _env| { let base_progress_ms = c.ctx.progress.as_millis() as f64; let now_ms = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis() as f64; - let elapsed = now_ms - c.ctx.last_update_ms as f64; + let elapsed = if matches!(c.ctx.playback_state, PlaybackState::Playing) { + now_ms - c.ctx.last_update_ms as f64 + } else { + 0.0 + }; let progress_ms = base_progress_ms + elapsed; let offset = offset_storage().lock().unwrap().unwrap_or(0.0); let adj_progress = progress_ms + offset; @@ -152,7 +156,11 @@ fn track_lyrics_widget() -> impl Widget { let calculate_progress = |ctx: &Arc, offset: f64| { let base_progress_ms = ctx.progress.as_millis() as f64; let now_ms = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis() as f64; - let elapsed = now_ms - ctx.last_update_ms as f64; + let elapsed = if matches!(ctx.playback_state, PlaybackState::Playing) { + now_ms - ctx.last_update_ms as f64 + } else { + 0.0 + }; base_progress_ms + elapsed + offset }; From 2512cb5219380af4be463fc76b4542d81e604f56 Mon Sep 17 00:00:00 2001 From: Cleboost Date: Mon, 14 Jul 2025 11:54:14 +0200 Subject: [PATCH 3/4] Refactor progress calculation for lyrics highlighting Moved progress calculation logic into CommonCtx::current_progress for reuse and clarity. Updated lyrics UI to use the new method, simplifying code and improving maintainability. --- psst-gui/src/data/mod.rs | 11 ++++++++++- psst-gui/src/ui/lyrics.rs | 28 ++++------------------------ 2 files changed, 14 insertions(+), 25 deletions(-) diff --git a/psst-gui/src/data/mod.rs b/psst-gui/src/data/mod.rs index 9abd2f41..fb46b792 100644 --- a/psst-gui/src/data/mod.rs +++ b/psst-gui/src/data/mod.rs @@ -358,7 +358,7 @@ impl AppState { } } -fn current_millis() -> u64 { +pub fn current_millis() -> u64 { use std::time::{SystemTime, UNIX_EPOCH}; SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis() as u64 } @@ -578,6 +578,15 @@ 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 { + let elapsed_ms = if matches!(self.playback_state, PlaybackState::Playing) { + current_millis() - self.last_update_ms + } else { + 0 + }; + self.progress + Duration::from_millis(elapsed_ms) + } } pub type WithCtx = Ctx, T>; diff --git a/psst-gui/src/ui/lyrics.rs b/psst-gui/src/ui/lyrics.rs index 8924e141..164ee56c 100644 --- a/psst-gui/src/ui/lyrics.rs +++ b/psst-gui/src/ui/lyrics.rs @@ -2,7 +2,7 @@ use druid::widget::{Container, CrossAxisAlignment, Flex, Label, LineBreaking, Li use druid::{Insets, LensExt, Selector, Widget, WidgetExt}; use crate::cmd; -use crate::data::{AppState, Ctx, NowPlaying, Playable, TrackLines, PlaybackState}; +use crate::data::{AppState, Ctx, NowPlaying, Playable, TrackLines}; use crate::data::CommonCtx; use crate::widget::MyWidgetExt; use crate::{webapi::WebApi, widget::Async}; @@ -13,7 +13,6 @@ use super::utils; use std::sync::Arc; use druid::im::Vector; use std::sync::{Mutex, OnceLock}; -use std::time::{SystemTime, UNIX_EPOCH}; use druid::{TimerToken, widget::prelude::*, widget::Controller}; pub const SHOW_LYRICS: Selector = Selector::new("app.home.show_lyrics"); @@ -132,16 +131,8 @@ fn track_lyrics_widget() -> impl Widget { .padding(Insets::uniform_xy(theme::grid(1.0), theme::grid(0.5))) .link() .active(|c: &Ctx, TrackLines>, _env| { - let base_progress_ms = c.ctx.progress.as_millis() as f64; - let now_ms = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis() as f64; - let elapsed = if matches!(c.ctx.playback_state, PlaybackState::Playing) { - now_ms - c.ctx.last_update_ms as f64 - } else { - 0.0 - }; - let progress_ms = base_progress_ms + elapsed; let offset = offset_storage().lock().unwrap().unwrap_or(0.0); - let adj_progress = progress_ms + offset; + let adj_progress = c.ctx.current_progress().as_millis() as f64 + offset; 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 }; @@ -153,19 +144,8 @@ fn track_lyrics_widget() -> impl Widget { env.set(theme::LINK_ACTIVE_COLOR, active); }) .on_update(|ctx, old, new, _env| { - let calculate_progress = |ctx: &Arc, offset: f64| { - let base_progress_ms = ctx.progress.as_millis() as f64; - let now_ms = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis() as f64; - let elapsed = if matches!(ctx.playback_state, PlaybackState::Playing) { - now_ms - ctx.last_update_ms as f64 - } else { - 0.0 - }; - base_progress_ms + elapsed + offset - }; - let is_line_active = |ctx: &Arc, line: &TrackLines, offset: f64| { - let adj_progress = calculate_progress(ctx, offset); + let adj_progress = ctx.current_progress().as_millis() as f64 + offset; 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 }; @@ -178,7 +158,7 @@ fn track_lyrics_widget() -> impl Widget { if !was_active && is_active { let mut storage = offset_storage().lock().unwrap(); - let new_offset = new.ctx.progress.as_millis() as f64 - new.data.start_time_ms.parse::().unwrap_or(0.0); + let new_offset = new.ctx.current_progress().as_millis() as f64 - new.data.start_time_ms.parse::().unwrap_or(0.0); *storage = Some(new_offset); ctx.scroll_to_view(); } From 8b93b4dc508ead172de990a51a03a4816d1cb2d3 Mon Sep 17 00:00:00 2001 From: Cleboost Date: Mon, 14 Jul 2025 12:06:54 +0200 Subject: [PATCH 4/4] Refactor lyrics timing and playback state handling Reduced playback position report interval for more frequent updates. Removed redundant playback state and timing fields from CommonCtx, simplifying progress tracking. Refactored lyrics offset logic to be managed in AppState, and removed the LyricsTicker controller and related global state from the lyrics UI. --- psst-core/src/player/worker.rs | 2 +- psst-gui/src/data/mod.rs | 32 +++++---------- psst-gui/src/ui/lyrics.rs | 74 ++++------------------------------ 3 files changed, 18 insertions(+), 90 deletions(-) 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 fb46b792..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 { @@ -105,8 +106,6 @@ impl AppState { show_track_cover: config.show_track_cover, nav: Nav::Home, progress: Duration::default(), - last_update_ms: 0, - playback_state: PlaybackState::Stopped, }); let playback = Playback { state: PlaybackState::Stopped, @@ -173,6 +172,7 @@ impl AppState { finder: Finder::new(), lyrics: Promise::Empty, credits: None, + lyrics_offset: None, } } } @@ -240,7 +240,6 @@ 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.common_ctx_mut().playback_state = PlaybackState::Loading; self.playback.state = PlaybackState::Loading; self.playback.now_playing.replace(NowPlaying { item, @@ -253,7 +252,6 @@ 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.common_ctx_mut().playback_state = PlaybackState::Playing; self.playback.state = PlaybackState::Playing; self.playback.now_playing.replace(NowPlaying { item, @@ -272,25 +270,20 @@ impl AppState { pub fn pause_playback(&mut self) { self.playback.state = PlaybackState::Paused; - self.common_ctx_mut().playback_state = PlaybackState::Paused; - self.common_ctx_mut().last_update_ms = current_millis(); } pub fn resume_playback(&mut self) { self.playback.state = PlaybackState::Playing; - self.common_ctx_mut().playback_state = PlaybackState::Playing; - self.common_ctx_mut().last_update_ms = current_millis(); } 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.common_ctx_mut().playback_state = PlaybackState::Stopped; self.set_common_progress(Duration::default()); } @@ -353,14 +346,12 @@ impl AppState { fn set_common_progress(&mut self, progress: Duration) { let mut ctx = (*self.common_ctx).clone(); ctx.progress = progress; - ctx.last_update_ms = current_millis(); self.common_ctx = Arc::new(ctx); } -} -pub fn current_millis() -> u64 { - use std::time::{SystemTime, UNIX_EPOCH}; - SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis() as u64 + pub fn reset_lyrics_offset(&mut self) { + self.lyrics_offset = None; + } } #[derive(Clone, Data, Lens)] @@ -570,8 +561,6 @@ pub struct CommonCtx { pub show_track_cover: bool, pub nav: Nav, pub progress: Duration, - pub last_update_ms: u64, - pub playback_state: PlaybackState, } impl CommonCtx { @@ -580,12 +569,9 @@ impl CommonCtx { } pub fn current_progress(&self) -> Duration { - let elapsed_ms = if matches!(self.playback_state, PlaybackState::Playing) { - current_millis() - self.last_update_ms - } else { - 0 - }; - self.progress + Duration::from_millis(elapsed_ms) + // Use the stored progress directly for better accuracy + // The progress is updated by the audio player events + self.progress } } diff --git a/psst-gui/src/ui/lyrics.rs b/psst-gui/src/ui/lyrics.rs index 164ee56c..6189d544 100644 --- a/psst-gui/src/ui/lyrics.rs +++ b/psst-gui/src/ui/lyrics.rs @@ -12,61 +12,9 @@ use super::utils; use std::sync::Arc; use druid::im::Vector; -use std::sync::{Mutex, OnceLock}; -use druid::{TimerToken, widget::prelude::*, widget::Controller}; pub const SHOW_LYRICS: Selector = Selector::new("app.home.show_lyrics"); -static LYRICS_OFFSET: OnceLock>> = OnceLock::new(); - -fn offset_storage() -> &'static Mutex> { - LYRICS_OFFSET.get_or_init(|| Mutex::new(None)) -} - -const TICK_INTERVAL_MS: u64 = 100; - -struct LyricsTicker { - timer: Option, -} - -impl LyricsTicker { - fn new() -> Self { - Self { timer: None } - } -} - -impl Controller for LyricsTicker -where - W: Widget, -{ - fn lifecycle(&mut self, child: &mut W, ctx: &mut LifeCycleCtx, event: &LifeCycle, data: &AppState, env: &Env) { - match event { - LifeCycle::WidgetAdded => { - let tok = ctx.request_timer(std::time::Duration::from_millis(TICK_INTERVAL_MS)); - self.timer = Some(tok); - } - _ => {} - } - child.lifecycle(ctx, event, data, env); - } - - fn event(&mut self, child: &mut W, ctx: &mut EventCtx, event: &Event, data: &mut AppState, env: &Env) { - match event { - Event::Timer(token) if Some(*token) == self.timer => { - ctx.request_paint(); - let tok = ctx.request_timer(std::time::Duration::from_millis(TICK_INTERVAL_MS)); - self.timer = Some(tok); - } - _ => {} - } - child.event(ctx, event, data, env); - } - - fn update(&mut self, child: &mut W, ctx: &mut UpdateCtx, _old_data: &AppState, data: &AppState, env: &Env) { - child.update(ctx, _old_data, data, env); - } -} - pub fn lyrics_widget() -> impl Widget { Scroll::new( Container::new( @@ -81,7 +29,6 @@ pub fn lyrics_widget() -> impl Widget { .center(), ) .vertical() - .controller(LyricsTicker::new()) } fn track_info_widget() -> impl Widget { @@ -131,12 +78,11 @@ fn track_lyrics_widget() -> impl Widget { .padding(Insets::uniform_xy(theme::grid(1.0), theme::grid(0.5))) .link() .active(|c: &Ctx, TrackLines>, _env| { - let offset = offset_storage().lock().unwrap().unwrap_or(0.0); - let adj_progress = c.ctx.current_progress().as_millis() as f64 + offset; + 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 }; - adj_progress >= start_ms && adj_progress < end_ms + current_progress >= start_ms && current_progress < end_ms }) .rounded(theme::BUTTON_BORDER_RADIUS) .env_scope(|env, _| { @@ -144,22 +90,18 @@ fn track_lyrics_widget() -> impl Widget { env.set(theme::LINK_ACTIVE_COLOR, active); }) .on_update(|ctx, old, new, _env| { - let is_line_active = |ctx: &Arc, line: &TrackLines, offset: f64| { - let adj_progress = ctx.current_progress().as_millis() as f64 + offset; + 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 }; - adj_progress >= start_ms && adj_progress < end_ms + current_progress >= start_ms && current_progress < end_ms }; - let offset = offset_storage().lock().unwrap().unwrap_or(0.0); - let was_active = is_line_active(&old.ctx, &old.data, offset); - let is_active = is_line_active(&new.ctx, &new.data, offset); + 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 { - let mut storage = offset_storage().lock().unwrap(); - let new_offset = new.ctx.current_progress().as_millis() as f64 - new.data.start_time_ms.parse::().unwrap_or(0.0); - *storage = Some(new_offset); ctx.scroll_to_view(); } }) @@ -181,7 +123,7 @@ fn track_lyrics_widget() -> impl Widget { |t| WebApi::global().get_lyrics(t.item.id().to_base62()), |_, data, _| data.lyrics.defer(()), |_, data, r| { - *offset_storage().lock().unwrap() = None; + data.reset_lyrics_offset(); let processed = match r.1 { Ok(lines) => { let mut out = Vector::new();