v0.13.0
Highlights
Two PRs land in this release, both centered on the MPRIS surface:
- #110 — Mpris fixes: a triplet of regressions on the systemd
state cache, the MPRIS seeker, and Chrome's cover art handling. - #111 — Mpris UI: the dashboard's transport row moves to HTMX
with optimistic updates and toast error reporting, MPRIS player
cards become collapsible, a dismissible PWA promo lands in the
header, the remaining emoji icons get replaced by inline Lucide
SVGs, and a few more MPRIS edge cases are ironed out along the way.
✨ Features
- Dismissible PWA promo in the header, deep-linking to the
multi-node app atpwa.odio.lovefor the current host. - Collapsible MPRIS player cards — each player collapses to a
single-line label, with open/closed state preserved across SSE
re-renders.
🎨 UI
- Transport actions unified on HTMX. Play/pause, seek, volume,
shuffle, stop, repeat, mute and the default-sink picker all go
throughhx-postwith optimistic updates; backend errors surface
in a bottom-pinned toast rendered from the plain-text response body. - Lucide-style inline SVG icons everywhere — section headers,
power, service actions, mute/play/repeat toggles, the zeroconf
badge and the empty state. Icons inheritcurrentColorand toggle
viadata-attributes, no JS-side icon swap. - MPRIS player template split into a wrapper +
_parts.gohtmlfor
readability, and small MPRIS thumbnails (e.g. Chrome's 150x83
YouTube cache) now scale up to fill the cover slot. - New lime accent (
#5ab81e) for active-button text and
active-service status, replacing the dimleaf/20/green-400.
🐛 Fixes
- MPRIS seeker no longer snaps back on Bluez/AVRCP sources that
re-report a latched Position every poll — freshness is now tracked
per player rather than from the global cache timestamp. - MPRIS cover refresh works in Chrome. Chrome keeps a stable
trackidacross tracks but rotates the/tmpart path; cache-bust
now uses bothtrackidandartUrl. - systemd state cache no longer gets stuck on transitional
states — the brokenwaitForStableStatebackoff (initial wait
above the 8 s cap) is repaired and a final refresh now fires on
timeout. - Service URL preserved across cache refreshes — it was getting
dropped on every fsnotify-triggered refresh because
serviceFromPropsonly sees D-Bus properties. - MPRIS card stops re-rendering on every 5 s position tick —
the seeker is interpolated client-side and only needs a fresh base
when the player state actually changes. - Player display names use MPRIS
Identity(with a cleaned-up
bus_namefallback) instead of the lowercased / instance-suffixed
bus name. - Tricky Position edge cases consolidated in a single
shouldAcceptPositionguard: zero/negative values
(go-librespot quirk), Bluez/AVRCP latched repeats, and
Position == Length(Chrome on track changes). - Volume slider stays visible when muted —
Volumeis now
*float64, so a genuine 0 still serializes as"volume": 0.
🧹 Internal
- CI actions bumped.
Full changelog: v0.12.0...v0.13.0