From c6da484481ad0eea13f3e24df2c1ab205f4d0791 Mon Sep 17 00:00:00 2001 From: vicrodh <103399283+vicrodh@users.noreply.github.com> Date: Sun, 19 Jul 2026 16:08:02 -0600 Subject: [PATCH] fix(ui): highlight/animate the playing row for offline-cache tracks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Local track rows bind the local-library DB row id, but local_queue_track publishes the Qobuz catalog id for offline copies (so the shared resolver finds them), so NowPlayingState.track-id never matched the row: no equalizer bars, no active highlight, and double-click restarted the track instead of toggling (album view, Tracks tab, folder detail, kiosk). Carry the library row id in source_item_id_hint for qobuz_download and re-publish it as NowPlayingState.local-track-id; TrackPlayCell, TrackRow and KioskAlbum now match on either id — one alias, no per-view hacks. km --- crates/qbz-ui/ui/primitives/TrackPlayCell.slint | 5 ++++- crates/qbz-ui/ui/primitives/TrackRow.slint | 3 ++- crates/qbz-ui/ui/shell/KioskAlbum.slint | 3 ++- crates/qbz-ui/ui/state.slint | 5 +++++ crates/qbz/src/playback.rs | 17 +++++++++++++++++ 5 files changed, 30 insertions(+), 3 deletions(-) diff --git a/crates/qbz-ui/ui/primitives/TrackPlayCell.slint b/crates/qbz-ui/ui/primitives/TrackPlayCell.slint index 8fb4ac208..3bd31dd33 100644 --- a/crates/qbz-ui/ui/primitives/TrackPlayCell.slint +++ b/crates/qbz-ui/ui/primitives/TrackPlayCell.slint @@ -80,8 +80,11 @@ export component TrackPlayCell inherits Rectangle { // Fired when a non-current track should start playing. callback play(); + // Offline-cache tracks publish the Qobuz catalog id in track-id but the + // local-library row id in local-track-id (playback.rs) — match either. property is-active: - NowPlayingState.has-track && NowPlayingState.track-id == root.track-id; + NowPlayingState.has-track && (NowPlayingState.track-id == root.track-id + || (NowPlayingState.local-track-id != "" && NowPlayingState.local-track-id == root.track-id)); property is-playing: root.is-active && NowPlayingState.playing; // This row is the track being fetched/resolved (no audio yet). The global // loading flag + active-row match means only the one loading row spins, diff --git a/crates/qbz-ui/ui/primitives/TrackRow.slint b/crates/qbz-ui/ui/primitives/TrackRow.slint index 3ac4556cd..d87694018 100644 --- a/crates/qbz-ui/ui/primitives/TrackRow.slint +++ b/crates/qbz-ui/ui/primitives/TrackRow.slint @@ -141,7 +141,8 @@ export component TrackRow inherits Rectangle { if (root.multi-select-mode) { return; } - if (NowPlayingState.has-track && NowPlayingState.track-id == root.track.id) { + // Same local-track-id alias as TrackPlayCell (offline-cache tracks). + if (NowPlayingState.has-track && (NowPlayingState.track-id == root.track.id || (NowPlayingState.local-track-id != "" && NowPlayingState.local-track-id == root.track.id))) { NowPlayingState.toggle-play(); } else { root.media-action("track", root.track.id, "play"); diff --git a/crates/qbz-ui/ui/shell/KioskAlbum.slint b/crates/qbz-ui/ui/shell/KioskAlbum.slint index 7bb17593a..870afc0ea 100644 --- a/crates/qbz-ui/ui/shell/KioskAlbum.slint +++ b/crates/qbz-ui/ui/shell/KioskAlbum.slint @@ -120,7 +120,8 @@ export component KioskAlbum inherits Rectangle { for track[i] in AlbumState.tracks: trow-rect := Rectangle { height: 52px; border-radius: Radius.sm; - property current: NowPlayingState.track-id == track.id; + // Same local-track-id alias as TrackPlayCell (offline-cache tracks). + property current: NowPlayingState.track-id == track.id || (NowPlayingState.local-track-id != "" && NowPlayingState.local-track-id == track.id); background: trow.has-hover ? Theme.alpha-6 : self.current ? Theme.accent.with-alpha(0.10) : transparent; diff --git a/crates/qbz-ui/ui/state.slint b/crates/qbz-ui/ui/state.slint index 7ec08e892..baee9edc3 100644 --- a/crates/qbz-ui/ui/state.slint +++ b/crates/qbz-ui/ui/state.slint @@ -4188,6 +4188,11 @@ export global WindowControlActions { export global NowPlayingState { in property has-track: false; in property title; + // Secondary alias for the playing track: the LOCAL-LIBRARY ROW ID when the + // track is an offline-cache copy (queue track-id carries the Qobuz catalog + // id there; set by refresh_now_playing_meta in playback.rs). Empty for every + // other source. TrackPlayCell / TrackRow / KioskAlbum match on either id. + in property local-track-id; in property artist; in property album; in property album-id; diff --git a/crates/qbz/src/playback.rs b/crates/qbz/src/playback.rs index df302985b..2adbbc11f 100644 --- a/crates/qbz/src/playback.rs +++ b/crates/qbz/src/playback.rs @@ -1376,6 +1376,13 @@ pub(crate) fn local_queue_track(track: &qbz_library::LocalTrack) -> QueueTrack { // the session queue store so playback survives a restart. source_item_id_hint: if is_plex { Some(track.file_path.clone()) + } else if is_offline { + // Offline copies: carry the local-library row id. The queue `id` + // above is the Qobuz catalog id (so the shared resolver finds the + // track), but every local surface's track row binds the DB row id — + // NowPlayingState re-publishes it as local-track-id so active-row + // comparisons (TrackPlayCell/TrackRow/KioskAlbum) match either. + Some(track.id.to_string()) } else { None }, @@ -1748,6 +1755,15 @@ pub(crate) async fn refresh_now_playing_meta(runtime: &Runtime, weak: &slint::We .source .clone() .unwrap_or_else(|| if track.is_local { "local" } else { "qobuz" }.to_string()); + // Active-row alias for offline-cache tracks: the local-library row id (the + // queue id is the Qobuz catalog id there, so the row's track-id binding + // never matches). Only trusted for qobuz_download — other builders reuse + // the hint for unrelated things (album id, plex rating key). + let local_track_id = if source == "qobuz_download" { + track.source_item_id_hint.clone().unwrap_or_default() + } else { + String::new() + }; let duration = track.duration_secs; // Plex-aware: a Plex track carries a raw `/library/...` thumb path that // must resolve to a tokenized `PlexThumb` (from current creds) so the @@ -1953,6 +1969,7 @@ pub(crate) async fn refresh_now_playing_meta(runtime: &Runtime, weak: &slint::We np.set_context_kind(context_kind.into()); np.set_context_id(context_id.into()); np.set_track_id(track_id.into()); + np.set_local_track_id(local_track_id.into()); // Mirror the active track + playing flag onto the Purchases globals so a // purchase track-row highlights/animates when it is the now-playing one. mirror_now_playing_to_purchases(&w, track_id_num, true);