feat(slint): system "now playing" notifications on track change
Port the Tauri desktop-notification path to a frontend-agnostic crate so the Slint app (and any future TUI/CLI) fires it from native Rust instead of a webview invoke. New qbz-media-controls::notify module: - Linux: XDG notification portal via ashpd (D-Bus); album art passed as Icon::Bytes after center-crop + downscale to <=512px + PNG re-encode (<=4 MiB). - macOS: notify_rust with image_path (cover cached to disk). - Windows: no-op (parity with Tauri). - Self-contained artwork download + md5 cache + resize, matching the Tauri helpers, with no dependency on the frontend (ADR-006). Wired in playback::refresh_now_playing_meta (the same point that pushes the tray tooltip + OS media controls): - De-dupe by track id so only an actual track change fires (the refresh also runs on resume/seek). - Skip while a remote QConnect renderer drives playback (matches the Svelte skipIfRemote). Fire-and-forget on a tokio task. Enabled by default, matching the Tauri default; a settings toggle is a follow-up. cc
This commit is contained in:
5 files changed
+414
-3
No files matched your search
@@ -515,10 +515,18 @@ fn fmt_remaining(position: u64, duration: u64) -> String {
|
||||
/// Push the now-playing values for the current queue track onto
|
||||
/// `NowPlayingState`. Called when a new track starts so the song card
|
||||
/// updates immediately (the poll loop only refreshes position/progress).
|
||||
/// Last track id we fired a desktop notification for. `refresh_now_playing_meta`
|
||||
/// runs on resume/seek too, so we de-dupe to only notify on an actual track
|
||||
/// change. `u64::MAX` = "nothing notified yet" (no real track id collides).
|
||||
static NOTIFY_LAST_TRACK: std::sync::atomic::AtomicU64 =
|
||||
std::sync::atomic::AtomicU64::new(u64::MAX);
|
||||
|
||||
pub(crate) async fn refresh_now_playing_meta(runtime: &Runtime, weak: &slint::Weak<AppWindow>) {
|
||||
let state = runtime.core().get_queue_state().await;
|
||||
let Some(track) = state.current_track else {
|
||||
// No current track → clear the tray tooltip (Linux) + stop media controls.
|
||||
// Reset the notify guard so replaying the same track after a stop fires.
|
||||
NOTIFY_LAST_TRACK.store(u64::MAX, std::sync::atomic::Ordering::Relaxed);
|
||||
if let Some(t) = crate::tray::handle() {
|
||||
t.clear_track();
|
||||
}
|
||||
@@ -579,6 +587,28 @@ pub(crate) async fn refresh_now_playing_meta(runtime: &Runtime, weak: &slint::We
|
||||
);
|
||||
}
|
||||
|
||||
// Desktop "now playing" notification (1:1 with the Tauri path). De-dupe so
|
||||
// only an actual track change fires; skip while a remote QConnect renderer
|
||||
// drives playback (matches the Svelte `skipIfRemote`). Fire-and-forget.
|
||||
if NOTIFY_LAST_TRACK.swap(track.id, std::sync::atomic::Ordering::Relaxed) != track.id {
|
||||
let notify_meta = qbz_media_controls::NotificationMeta {
|
||||
title: title.clone(),
|
||||
artist: artist.clone(),
|
||||
album: album.clone(),
|
||||
bit_depth: track.bit_depth,
|
||||
sample_rate: track.sample_rate,
|
||||
art_url: artwork.to_mpris_url(),
|
||||
};
|
||||
tokio::spawn(async move {
|
||||
if let Some(svc) = crate::qconnect_service::service() {
|
||||
if svc.is_peer_active().await {
|
||||
return;
|
||||
}
|
||||
}
|
||||
qbz_media_controls::show_track_notification(notify_meta).await;
|
||||
});
|
||||
}
|
||||
|
||||
let _ = weak.upgrade_in_event_loop(move |w| {
|
||||
let np = w.global::<NowPlayingState>();
|
||||
np.set_has_track(true);
|
||||
|
||||
Reference in new issue
Block a user