Commit Graph
6 Commits
Author SHA1 Message Date
vicrodh 1beb061198 chore(deps): mpris-server 0.10
cc
2026-08-29 08:26:25 -06:00
vicrodh c0664030ee feat(windows): keep the system awake while a track plays
Linux holds a logind inhibitor fd; Windows has SetThreadExecutionState, and
nothing was calling it.

The plan asked for the call to sit in `set_playback`. It cannot: that API is
PER-THREAD -- Windows drops a thread's requests when it exits -- and this
module's own contract says outbound calls are "free from any thread"
(media_controls_qt.rs:34). In practice they arrive on the mpris-server thread,
a tokio worker, or the GUI thread, whichever pushed. A request made from a pool
worker would evaporate with it, silently. So the state is owned by one thread
that lives as long as the process and receives a bool.

ES_SYSTEM_REQUIRED without ES_DISPLAY_REQUIRED: an audio player keeps the
MACHINE awake, not the screen. No ES_AWAYMODE_REQUIRED either -- that is for
media-centre appliances and it suppresses the sleep the user asked for.

Exposed as a free function rather than a `PlatformHandle` method, and called
BEFORE the `handle()` early return on both publishers. Hanging it off the
handle tied it to SMTC: a failed souvlaki init, or an HWND that never arrived,
would have left playback working and the machine falling asleep mid-track --
two unrelated features failing together for no reason.

`held` is updated only after the API succeeds, so a failed release cannot leave
Windows inhibited while later `false` messages are skipped as no-ops.

cc
2026-08-27 13:55:45 -06:00
vicrodh 114824b7bc feat(media-controls): SMTC with a real HWND, and a native cover path
Windows media keys never worked because SMTC never started. souvlaki's Windows
backend does `config.hwnd.expect(..)`, which PANICS rather than degrading, and
`MediaControls::new` runs inside a tokio task, so a missing handle aborted the
task instead of returning the Err the caller was written for. Phase A bowed out
before reaching it and logged "SMTC disabled: no window handle wired yet".

`spawn` now takes a `NativeWindow`, and `cxx/win_shell.cpp` supplies the
top-level HWND through `QGuiApplication::topLevelWindows()`. The guard stays:
a missing integration is a missing feature, a panic in a tokio task is a dead
session.

Windows reads the handle from a bounded retry on the GUI thread. Both things it
waits for -- the tray bridge's Qt thread and a shown window -- are "not yet",
never "not ever": `tray_bridge::ui` is a no-op until `QbzTray::boot` registers
the thread, and `QbzSession.boot()` runs first (Main.qml:254 vs :380).

NON-WINDOWS IS UNCHANGED, deliberately. An earlier draft routed every platform
through that hop and it was a Linux/macOS regression three ways: initialization
could be dropped outright before boot, MPRIS would start only when the Qt queue
drained (losing the first metadata push), and `main_window_hwnd` calls
`QWindow::winId()`, which CREATES the platform window as a side effect on
platforms with no reason to.

Cover art needed its own fix. souvlaki 0.8.3 branches on
`url.starts_with("file://")`, then trims exactly that prefix and hands the rest
to `GetFileFromPathAsync`. So `file:///C:/x.jpg` arrives as `/C:/x.jpg` -- a
slash before a drive letter, which never opens -- while a bare `C:\x.jpg` no
longer matches the prefix and takes the remote-URI branch instead. The value
that works is the hybrid `file://C:\x.jpg`. UNC is handled too: `file://nas/m/a`
is a real Windows path and was being passed through as the relative `nas/m/a`.

The percent-decode is full and applied once: `art_url` arrives from
`url::Url::from_file_path` (space as %20) and from `fs_url::file_url` (only
% # ? escaped), and one full decode reads both -- `%2520` gives back a literal
`%20` rather than a space. `fs_url::local_path` is deliberately not used here;
its three-escape decode would leave the other producer's %20 intact.

Eight tests cover the matrix. Reviewed adversarially before commit, which is
where the inverted cover conversion and the non-Windows regression were caught;
the souvlaki branch was then read at the source to confirm.

cc
2026-08-27 13:37:24 -06:00
vicrodh 6c9964481a fix(dbus): stop forcing the zbus 5 executor onto tokio
zbus features unify across the dependency graph and "tokio" wins over
"async-io": enabling it via rfd, ashpd (secret portal) and ksni's
default features flipped the shared zbus 5 executor to tokio for
accesskit_unix/atspi too — Slint's accessibility backend opens its
D-Bus connection on a plain thread and panicked at startup ("there is
no reactor running, must be called from the context of a Tokio 1.x
runtime"), killing AT-SPI support. Switch every zbus-5 consumer we
control to the async-io backend (rfd "async-std", ashpd "async-io",
ksni default-features off + "async-io"): async-io connections
self-drive from any thread and the futures still await fine inside our
tokio runtime. The zbus 4 group (keyring/secret-service/mpris-server/
qbz-audio) is a separate feature graph and stays on tokio by design.

cc
2026-07-05 10:26:18 -06:00
vicrodh d08c5f6569 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
2026-06-06 16:45:04 -06:00
vicrodh 7a7e5f82c4 feat(slint): system media controls — MPRIS with GNOME app icon + SMTC/MediaRemote
New frontend-agnostic crate qbz-media-controls (ADR-006): one MediaIntegration
trait, cfg-split backends. Linux uses mpris-server and publishes the MPRIS
org.mpris.MediaPlayer2.DesktopEntry property as com.blitzfc.qbz — the only way
GNOME Shell resolves the app icon for its media widget (souvlaki, the old
cross-platform crate, never set it, so GNOME showed no icon; KDE is lenient).
macOS/Windows keep souvlaki (MediaRemote/SMTC), where the icon comes from the
app bundle, not a DesktopEntry. The Linux server runs on a dedicated thread
with its own tokio runtime; state arrives over an async channel.

Wired into qbz-slint via src/media_controls.rs: started on shell entry,
metadata/playback pushed from the playback paths (mirroring the tray), and
inbound control events (media keys, the desktop media widget, Now Playing)
dispatched to the player through the existing QConnect-aware helpers. Both
Linux and macOS compile clean.

cc
2026-06-06 11:03:11 -06:00