Commit Graph
6183 Commits
Author SHA1 Message Date
vicrodh 942f408fe1 feat(qbz-qt): publish the now-playing quality and output state
The bar had two quality fields and no way to say what was actually being
delivered, so the stamp had nothing to render and the two output LEDs did not
exist at all.

quality_state.rs is a verbatim port of the Slint downgrade arithmetic — the 0.9
rate-family guard with its DSD exemption, the limit-cause classification and
the delivered-tier resolution — kept pure and unit-tested, with the per-track
catalog maximum seeded once on the track edge rather than per tick.
output_labels.rs ports the backend/mode label derivation from
qbz/src/settings.rs; it only reads AudioSettings and never touches the audio path.

Fourteen properties join QbzPlayer: the delivered-vs-catalog block, the two
LEDs with their active flags, and the cast/remote quartet. They refresh from
where they actually change — the labels follow the settings, the downgrade
follows the stream, and set_effective_stream dedupes so a steady stream costs
no Qt-thread hop.

Removes publish_with_cache, a near-copy of publish that was the reason the
cache overlay and the quality fields could disagree. There is one publisher now.

cc
2026-07-28 15:23:59 -06:00
vicrodh 4b34b70f70 perf(qbz-qt): interpolate the spectrum band instead of raising the FFT rate
The band moved at the producer's 30 Hz. Raising TARGET_FPS would have been the
obvious knob, but it lives in qbz-audio and is shared with Slint, and the
amplitudes are already EMA-smoothed in Rust to roughly 6 Hz — a faster producer
would have bought CPU and no smoothness.

The published frames are now replayed across the render frames that follow
them, which is what Slint's dock gets from its per-bar 90ms animate. A late
publish degrades to a hold instead of a jump, and the motion runs at the
display's rate rather than the data's.

Splits the strip out of the dock into SpectrumBand, so the dock file owns only
the gating and the layout arithmetic.

cc
2026-07-28 15:23:44 -06:00
vicrodh 5c631000b7 refactor(qbz-qt): split QbzLibrary out of the God-object
library_bridge.rs has sat in the tree unreferenced since the phase-23 work
stopped: no mod declaration, not in build.rs, zero call sites, and duplicating
members that were still live in bridge.rs. It could not have been wired as it
stood — its module was named qbz_library, which is ambiguous with the
qbz-library crate this binary depends on, so it never compiled. The module is
now qbz_library_bridge; the QML type name comes from the QObject, so QML is
unaffected.

Registering it and deleting the duplicates had to happen together. Wiring it
alone would have compiled cleanly and produced two independent Library states —
QML reading one object while Rust published to the other — with the view simply
frozen and nothing in the log.

Moved with it: four properties, five invokables, three signals, the seven
library-only ui() hops in main.rs, and thirty-four QML references. The
Connections blocks in LibraryView, Sidebar, QueuePanel, AlbumView and
PlaylistCollage retarget wholesale because every handler in them is a library
signal. ArtistView's does not: it mixes onLibraryArtworkReady with
onReleaseSectionReady, so it becomes two blocks — retargeting it whole would
have orphaned the releases pager, and QML resolves handlers lazily enough that
the discography would just have stopped loading with nothing in the log.

bridge.rs is down to 389 lines and five domains.

cc
2026-07-28 14:07:40 -06:00
vicrodh 0799d59e01 feat(qbz-qt): Local Library — folder tree, windowed album grid, detail
The sidebar has had a "Local Library" row since the shell was ported and
nothing behind it: the nav entry did not even navigate. This builds the view.

QbzLocal is its own domain bridge (the phase-23 pattern, one #[cxx_qt::bridge]
per domain) over local_library_qt, which consumes qbz-library for the scanning,
the folder-vs-tag album identity and the queries rather than reimplementing any
of it. The view carries the folder tree, the album grid, album detail, the
track tabs, sorting and search, plus the scanning and empty states.

Windowed from the start, not as a later pass: real local libraries reach 16k
tracks and that is exactly the surface where the Slint port freezes. Artwork
is id-keyed and evicted with the window.

Playback is source-aware — a local track plays from disk through the player's
play_data seam, because the Qobuz tier-walk needs a client and fails on every
local advance. TrackRow gains an opt-out for its drag source: a local row's id
is a local database row, and the sidebar's drop handler forwards whatever it
receives to `playlist add-tracks` as a Qobuz catalog id, so dragging one onto a
playlist would have silently added an unrelated track.

Not included, and tracked as follow-ups: Plex union and thumbnails, multi-select
and the bulk bars, ephemeral folders, the tag editor, A-Z jump strips, the
group-by dropdown, the tree's resize handle, per-tab scroll restore, and local
favourites on rows. Album detail renders inside the view rather than as its own
routed page.

cc
2026-07-28 13:49:15 -06:00
vicrodh cd1f4486b7 fix(qbz-qt): drive the FFT drain by signal instead of a second clock
The band animated like slow motion. The producer runs its own 30fps clock and
the drain slept a fixed 33ms after each publish, so two independent clocks beat
against each other and the drain intermittently missed a frame.

The sink now unparks the drain right after latching a frame of the stream the
visible mode consumes, and the drain parks again once it has published: exactly
one publish per produced frame, no aliasing. It is also cheaper than the poll
it replaces — zero idle wakeups instead of thirty a second — and an unpark that
arrives before the park is remembered, so the latch/park race cannot drop a
wake. Frames for the other two modes are still latched (not signalled) so
switching mode has data on the first frame.

cc
2026-07-28 13:48:48 -06:00
vicrodh 1916d8d794 feat(qbz-qt): wire every integration to its real backend
The Integrations pane rendered rows that did nothing: Last.fm, ListenBrainz,
MusicBrainz and Discord all had prefs but no runtime behind them, and Discord
never pushed a presence at all.

Each row now drives the qbz-integrations APIs for real — connect/disconnect
state, the account label once connected, and the per-service toggles — and the
runtime starts on shell entry, applying the persisted opt-ins and the scrobble
queue's flush watcher. Scrobbles arm on the de-duped track edge (firing from
the republish path would re-arm the timer on every poll tick) and the Discord
presence refreshes on both the track and the play/pause edges, then clears on
logout so a signed-out session stops advertising what was playing.

All of it stays strictly opt-in: nothing here does anything until the user
connects the service.

cc
2026-07-28 13:48:35 -06:00
vicrodh f44f03e414 feat(qbz-qt): now-playing bar buttons 1:1 with the Tauri original
Both bars are brought to the reference's control set, order, geometry and
hover treatment, and the bar's quality readout now mounts QualityBadge in its
compact mode instead of a hand-drawn stamp.

settings_qt publishes `normalization` in the settings document. It was already
settable through settingsBool but never read back, so both bars had to shadow
the toggle in local state and would drift from the real value after any change
made elsewhere.

cc
2026-07-28 13:48:16 -06:00
vicrodh 10461bb49c fix(qbz-qt): playlist artwork — own graphic for Qobuz, collage for the rest
Two bugs with one root: the card bound the playlist's `images*` lists, which
are MEMBER-ALBUM covers, not the playlist's own artwork. So a Qobuz editorial
playlist showed one of its albums' sleeves, and a user playlist — which has no
`images*` at all — showed nothing.

Qobuz playlists carry their graphic in `image_rectangle`, which the model did
not deserialize; the field is added (additive, `serde(default)`, and Playlist
is only ever built through serde) and the card prefers it, contain-fitted so
the artwork is not cropped. Playlists without one fall back to the member-cover
mosaic, reusing the collage the sidebar already draws rather than a second
implementation. Both the All-tab grid and its list mode are covered — the 44px
list thumb binds the same map and would otherwise have gone blank.

cc
2026-07-28 13:48:05 -06:00
vicrodh 9618bfe137 feat(qbz-qt): give LabelCard its artwork and a hover action
The card rendered neither an image nor any hover affordance. Root cause of the
missing artwork: the logo was gated on `item.imageUrl` while its source came
from the host-resolved artwork map, so any mount that resolves covers into a
side map without republishing the item drew nothing. Artwork is now driven by
the resolved source alone and `imageUrl` only chooses the placeholder arm.

The container is round, per the owner, matching ArtistCard rather than the
square art the Slint reference uses; the contain-fit box is inset so a wide
wordmark is not clipped by the circle, keeping the reference's "label logos
are never cropped" rule. The hover row carries a single disc: labels are
opened, not played, and the Slint reference offers exactly one label action.

cc
2026-07-28 13:47:49 -06:00
vicrodh 6e81b1ac78 fix(qbz-qt): forward the overlay button's click, and use it in AlbumCard
CardOverlayButton declared `signal clicked()` with no argument, but three
cards write `onClicked: function (mouse) { menu.openAtCursor(btn, mouse.x,
mouse.y) }` against it — so `mouse` was undefined and the context menus on
TrackCard, PlaylistCard and ArtistCard threw instead of opening. The signal
now carries the event; handlers that ignore it are unaffected.

AlbumCard drew its three hover discs as raw Rectangle + QbzIcon + MouseArea
blocks instead of the shared control every other card uses, which is how its
geometry drifted from theirs in the first place. It now mounts
CardOverlayButton with identical behaviour. One deliberate change rides along:
the favorited heart goes from the red `favorite` tint to the accent tint,
which is what discover/AlbumCard.slint actually specifies — the red was the
drift, not the parity.

cc
2026-07-28 13:47:39 -06:00
vicrodh 2875063626 fix(qbz-qt): one centred hover-action row for every card family
All four card families hand-rolled the same overlay row and all four were
misaligned, each differently. They centred the group with a manual spacer
Item computed as (cardWidth - 44 - 2*36 - 2*12) / 2 — the leftover once you
account for the primary disc, the two ghost discs and the gaps between them.

AlbumCard set spacing: 12 AND kept the spacer, so the spacer itself took a
12px gap and the group sat 12px right of centre. ArtistCard, PlaylistCard and
TrackCard never set spacing at all, so the gaps the formula had already
subtracted never materialised: the discs touched each other and the group sat
12px left of centre. ArtistCard also measured against 190px while its art is
200 wide.

The arithmetic was never needed. CardOverlayRow centres a Row on its parent,
which computes its own width from whatever children it has — so a card that
hides a button (artists without the follow action) stays centred for free,
where the old formula needed a conditional term to keep up.

cc
2026-07-28 13:10:41 -06:00
vicrodh 71c2356f7b feat(qbz-qt): port the quality badge 1:1 from the Tauri original
The port had reinvented this badge: an icon-only replica with no MP3 tier, no
full or compact form, and none of the playback states. QualityBadge.svelte and
QualityBadgeStatic.svelte in the archived Tauri tree are the reference, and
every number here comes from their CSS.

Four modes, the union of both originals: full (the 136x36 pill with tier row
and bit-rate), compact (the narrow-bar "24/96" form from issue #303), bare
(full's content without the pill chrome, for toolbars) and iconOnly (the
Hi-Res brand mark alone, or a framed 30px square for CD/MP3 so both line up in
a grid). Tier resolution follows the original's order, MP3 first so it can
never come out as CD.

The four playback states come with it: system resampling, ALSA plughw
fallback, deliberate quality adjustment and confirmed bit-perfect, each with
its own border hue and indicator glyph, plus the rich hover tooltip carrying
source, output, the reason and the fix. Rows that only advertise stored
quality leave the flags false, which is exactly what QualityBadgeStatic was.

QualityMini becomes an alias over iconOnly mode rather than a second drawing
of the same thing, so the six existing call sites keep working while there is
one badge again.

Brings the assets the port was missing: cd.svg, mp3.svg and the LINE Seed JP
face the badge is set in. Their tint is baked to #b3b3b3 because Tauri applied
it with a CSS filter and QML has none on the software path. Adds theme.alphaTier(pct)
so the badge names the alpha ramp the design specifies instead of a raw index.

build.rs now collects the whole asset tree recursively — the hand-written root
list meant a new asset compiled clean and failed at runtime with "Cannot open".

18/18 tests; offscreen smoke clean.

cc
2026-07-28 12:49:50 -06:00
vicrodh aabde3be63 fix(qbz-qt): unresolvable QML imports, focus-based freezing, and dock render cost
Three views instantiated components the phase-23 directory move had left
unimportable: PlaylistView and SearchView use TrackRow (now in rows/) and
SectionRail uses AlbumCard (now in cards/). QML resolves lazily, so these
survived cargo check, the test suite and the offscreen smoke — they only
failed when the user opened a playlist. Adds a repo-wide audit of every
instantiated component against the importing file's visible directories; the
three above were all of them.

The ambient field and the new dock band froze on lost focus. That is wrong on
any tiling or mosaic desktop, where windows stay fully visible while
unfocused — the field would stop while the user is looking at it. Both now
gate on the window not being minimized or hidden, which is the real "nobody
can see this". Freezing on not-visible stays.

Dock band render cost, in descending order of impact: the drain publishes
only the stream the active mode consumes (Bars mode was marshalling a
512-float waveform QList 30 times a second for nothing); each mode marshals
its QList into JS once per tick instead of once per delegate; one shared
Gradient replaces 28 per-delegate instances; and the per-bar Behaviors are
gone since qbz-audio already smooths and clamps the amplitudes upstream.

18/18 tests; offscreen smoke clean; component-resolution audit clean.

cc
2026-07-28 12:26:40 -06:00
vicrodh 70bd337d16 feat(qbz-qt): Large NPB dock — sidebar space reservation + FFT spectrum band
The Large dock was a bare cover overlay: the sidebar knew nothing about it,
so the playlist rows kept running to the sidebar's bottom edge and sat
underneath the album art. Sidebar.slint reserves the in-sidebar portion of
the dock; the port had dropped that.

Reserve it the same way, from a single source of truth. QbzShell gains
largeDockHeight (shell_bridge.rs owns the arithmetic: 9 top + band + gap +
208 art + 4 bottom), the sidebar subtracts largeDockHeight - npbLargeHeight
from the playlist list, and AppShell pins the overlay at height -
largeDockHeight. The 112px bar height becomes a theme token so the shell and
the reservation cannot drift apart.

The band the cover was missing needs the FFT tap, so wire it: the runtime is
built with_visualizer, viz_qt.rs runs the frontend-agnostic producer from
qbz-audio and drains it at 30fps into a new QbzViz domain bridge as typed
QList<f32>. Bars (28 mirrored columns over the 14 active bins), Waveform (48
downsampled L columns) and Energy (5 bands) render as Slint does, the cover's
eye button toggles the band, clicking it cycles the mode, and both persist to
ui_prefs.

Cost is zero when unused: the tap starts disabled, the eye toggle gates
capture, the drain thread parks while off, and the transport is mirrored so a
paused player parks the producer instead of re-analyzing a stale buffer. The
QML binds bar HEIGHTS against static-model Repeaters — the lists are replaced
per frame, so a model binding would rebuild every delegate 30 times a second.

Adds the eye/eye-off icon pair to the seven baked tint variants.

18/18 tests; offscreen smoke clean (0 QML errors, home published).

cc
2026-07-28 12:05:37 -06:00
vicrodh 9a4fec6b70 refactor(qbz-qt): phase 23 part 1 — QML module layout + first five domain bridges
Two fronts of the modularization, both gated green before this commit.

QML leaves the flat qml/ directory for seven modules: cards, controls,
rows, settings, shell, theme, views. Only Main.qml and LoginScreen.qml
stay at the root. build.rs matches the tree exactly (55 files), relative
imports and asset paths were re-anchored for the new depth.

The QbzBridge God-object starts breaking up: QbzSession, QbzShell,
QbzPlayer, QbzQueue and QbzHome are now their own #[cxx_qt::bridge]
singletons, each with its own OnceLock<CxxQtThread> and ui() hop, booted
in order from Main.qml. Bridge files stay directly under src/ because
cxx-qt-build only accepts rust_files from one directory (QTBUG-93443).

bridge.rs keeps 5.5 domains (library, album/artist, settings,
integrations, search, playlist) and dies once they are split out.

cc
2026-07-28 11:08:20 -06:00
vicrodh 580b72084f refactor(qbz-qt): phase 22 — full QML duplication sweep beyond cards
TrackRow.qml unified from 4 copies (context arms, net -716 LOC with
strictly more behavior); QbzSectionHeader, QbzTabBar, QbzNavButton,
QbzIconButton, QbzCircleAction, QbzEmptyState, QbzOfflinePlaceholder
consolidated; sort dropdowns onto QbzContextMenu; QualityMini routed
everywhere. Deliberately separate per the Slint architecture: QueueRow,
FeedListRow, underline tab strips, ReleaseSection header. Restored the
offline 'Open Settings' action the POC had stubbed.

km
2026-07-28 08:42:54 -06:00
vicrodh a19e2dac60 refactor(qbz-qt): phase 21 — one shared card component per family
Consolidated to the Slint architecture (one component per family with
context arms, mounted by every surface): ArtistCard, PlaylistCard,
TrackCard, LabelCard, SlimCard promoted to shared files; 7 duplicates
deleted across HomeView/LibraryView/SearchView (net -1038/+93).
Migration made several surfaces MORE Slint-true (ArtistGridCard
200x246 on all rails/heroes, real overlays+pin badges on Home/Pinned/
Search cards). SearchTrackHero kept distinct — it IS a distinct Slint
component. CardMenu/CardOverlayButton/QualityMini promoted shared.

km
2026-07-28 07:42:08 -06:00
vicrodh 4dc7034e03 chore(qbz-qt): drop dead app_background_index helper
km
2026-07-28 06:51:49 -06:00
vicrodh 245eaa4d45 feat(qbz-qt): phase 20 — live i18n across the 8 bundled locales
tr() gained a revision parameter (trRev qproperty bumps on switch) so
all ~465 QML call sites re-evaluate — the Qt equivalent of Slint's
reseed after select_bundled_translation(). Rust-built documents
(home/library/settings option labels, plural tf() strings, active
album/artist docs via record-free republish paths) re-publish on
switch. Settings > Appearance language selector applies live (Auto +
8 locales, native names, shared ui_prefs.language key); boot honors
the persisted pref (was unconditional resolve_auto). Verified es/ru/ja/
de mid-run with zero tofu (fontconfig covers CJK/Cyrillic); restart
persistence verified; owner's en pref restored.

km
2026-07-28 06:44:10 -06:00
vicrodh ea644f5e05 feat(qbz-qt): phase 19 — full Settings subnav, Appearance + Integrations, live theming
Theme system as data: qbz_theme registry (36 themes + auto/custom)
resolved from the shared ui_prefs slug into a themeJson document;
QbzTheme rebound to it so the whole app repaints live (OLED->Light
probe-verified). Full subnav (8 sections + Share logs footer);
Appearance 1:1 (theme dropdown with dark/light filter, ambient/titlebar
rows in their real home, typography, library visuals, window chrome,
tray via tray_settings.db, renderer); Integrations 1:1 (MusicBrainz,
Last.fm 2-step browser connect + authed state, ListenBrainz token
validate, Discord toggle -> live DiscordRpc.set_enabled). Offline/Local
Library/Blacklist/Developer render honest placeholders.

km
2026-07-28 06:13:04 -06:00
vicrodh c59c883ac1 feat(qbz-qt): phase 18 — all four NPB modes + ViewMode flyout
PlayerBar.qml covers New/Classic/Large (full-width seekbar with times,
responsive 3-column layout, glass Classic SongCard variant, right
cluster), NowPlayingBar seam Loader picks per mode; Small unchanged.
ViewMode flyout wired (New/Classic/Small/Large live, Miniplayer/
Immersive/Kiosk inert replicas), mode persisted to the shared
ui_prefs npb_mode key, Large forces the sidebar open and docks the
208px cover (SidebarNowPlayingDock equivalent); AppShell bar height
mode-aware (42/112). Empty and playing states verified per mode;
user's persisted small pref restored after probes.

km
2026-07-28 05:02:11 -06:00
vicrodh 8c1ff34205 feat(qbz-qt): phase 17 — PlaylistView 1:1 + drag & drop as Slint
Full playlist surface: header with owner/foreign gating, play/shuffle,
favorite/follow optimistic, copy-to-library, pin, rename, delete,
per-row remove for owners, search filter, full sort set, custom-order
drag reorder persisted per user. Sidebar and every playlist card
navigate; card overlay/menu actions un-stubbed. Drag & drop per the
Slint gesture: row-body press-drag (>6px) from album/playlist/search/
library rows, window-top ghost pill, sidebar playlist rows drop-hot
highlight, drop -> add_tracks_to_playlist + reload reconcile; in-list
release reorders. Round-trip proven end-to-end on a throwaway playlist
(create -> drag -> drop -> refetch shows track -> delete).

km
2026-07-28 03:39:35 -06:00
vicrodh 22e65e939b feat(qbz-qt): phase 16 — card overlays everywhere + window-centered modals
Overlay opacity bindings referenced a nonexistent parent chain — all
card overlays evaluated invisible. Fixed per family: LibTrackCard
(scrim + fav/play/more row, full Slint menu), LibArtistCard rewrite
(circle-clipped scrim, play + more; play_artist with Popular-tracks
first and studio-discography fallback), LibPlaylistCard overlay fixed,
LibLabelCard verified overlay-less like the Slint. Modals: shared
AppShell-level layer (full-window scrim, window-centered panel)
replacing the view-local content-frame-centered ones.

km
2026-07-28 02:26:24 -06:00
vicrodh 9eb46d5e25 feat(qbz-qt): phase 15 — search 1:1 (cortinilla + full page with filters)
Intelligent search engine reused from qbz_app (SWR cache, interaction
recording, rank_within, top_for_query; app-menu kill switch on the
shared pref). Header field fully live: 220ms debounce, keyboard
semantics (arrows/Enter/Esc), clear, idle-close. Cortinilla as a plain
overlay (pointer stays on the field — Slint's exact reason): top
result, spec-ordered sections with View more, skeleton, row activation
dispatches (album/artist views, track play). Search page: 5 searchType
filter radios (Main Artist/Performer/Composer/Label/Release Name),
5 scope tabs, Most-popular hero, carousels, per-type grids/lists with
paged Load more. Perf: cortinilla 245ms cached / 1.1s cold. Fixed an
artwork attach bug the screenshots exposed (url/path slot mixup).
18/18 tests.

km
2026-07-28 00:49:53 -06:00
vicrodh 9f86a40f52 feat(qbz-qt): phase 14 — app-wide ambient background (modo Cider)
Port of the spec's palette derivation (16x16 downscale, coverage-
weighted 24-bin hue histogram, genuine secondary cluster or deeper
same-hue, +180 accent, B/W defaults) driven by the now-playing artwork
on track change. QPainter AmbientField with the WGSL scene's exact
four blob orbits, dim scrim, 30fps cap paused when inactive; chrome
surfaces translucent per the AppShell alpha tokens (content frame
surface-main @ 0.22 frosted). Same shared app_background pref (owner
has it 'ambient'), app-menu toggle applying live, QBZ_BG_* env knobs.
4 new unit tests (14/14 total).

km
2026-07-27 23:37:13 -06:00
vicrodh cc71337293 feat(qbz-qt): phase 13 — correct card families in Library + Discover tabs
Library audit: the phase-7 conversion never actually mounted the shared
AlbumCard — ALL kinds used the invented feed card. Now per-kind 1:1:
LibTrackCard (discover/TrackCard replica with wired next/later/queue
enqueue), shared AlbumCard for albums, LibArtistCard (ArtistGridCard
circle), LibPlaylistCard (collage, follow-aware), LibLabelCard
(gradient + contain-fit logo), list rows rewritten to the real
FavoritesView inline row, Tracks tab on a TrackRow replica. Discover:
Editor's Picks and For You tabs rendered from the same discover-index
fetch, prefs-ordered per DiscoverPrefsStore (incl. mostStreamed#album
variant, Qobuz Mixes gradient tiles, Pinned in the user's own position);
Recommendations tab gated on the user's pref with an explicit engine
placeholder. No refetch on tab switches.

km
2026-07-27 22:35:21 -06:00
vicrodh 71d41b927c fix(qbz-qt): phase 12 — card rounding, menu anchoring, opaque window, titlebar toggle
Rounding: QML clip is rectangular (radius+clip does NOT round-clip) —
new shared RoundedImage (software Canvas raster clip, GL-independent)
applied at 16 artwork sites incl. true circular artist crops. Menus:
new QbzContextMenu (cursor-anchored / below-trigger, window-clamped)
converted at 7 sites, replicating TrackRow.slint positioning. Window:
main window is OPAQUE in Slint (transparency is miniplayer-only;
rounded corners are KWin's) — dropped the translucent frameless
misread, squared app corners, opaque root. Titlebar: wired the EXISTING
shared ui_prefs use_system_title_bar key (app-menu checkable item,
restart semantics like Slint).

km
2026-07-27 21:43:53 -06:00
vicrodh da36d86ccc feat(qbz-qt): phase 11 — final polish vs owner's Slint reference shots
Fixed an ALSA enumeration zip bug that silently dropped the last device
(the stored DAC now shows with its BP badge). Home rails now follow the
persisted DiscoverPrefsStore (order + disabled sections) like the Slint
shell; Pinned rail built from pinned_items.db (albums/artists/playlists,
ArtistGridCard circle); View-all header links on endpoint rails; pin
badge hover-gated like the Slint overlay buttons. Restore path now
binds the pinned + local-favorites stores (was login-only). Settings
panels verified row-for-row against the 3440px references.

km
2026-07-27 20:01:02 -06:00
vicrodh d57cec974f feat(qbz-qt): phase 10 — Settings > Audio and Playback 1:1
settings_qt.rs controller over the shared stores (AudioSettingsState,
PlaybackPreferencesState, ui_prefs.json, qconnect_settings.db) with the
upstream cascades (dac-passthrough->skip-sink-switch, streaming-only->
gapless, backend switch resets routing) and live apply through public
player APIs (reload_settings/reinit_device/clear_audio_cache). ALSA
device regrouping (Defaults/Bit-perfect/Plugin/Other + BP badges),
searchable QbzSelect, QbzToggle/QbzSlider/SettingRow replicas, exact
row order and gating. Persistence probe verified (gapless +
streaming-quality round-trip, original values restored).

km
2026-07-27 19:12:06 -06:00
vicrodh b10ff98620 feat(qbz-qt): phase 9 — Queue homologation + Lyrics side panel
Queue panel 1:1: History tab (click replays per queue.rs semantics),
#442 'Next in queue'/'Next up' section markers from manual_next_count,
40-row pagination + live search + page clamp, row actions (play,
remove, remove-all-after, heart), press-drag reorder via move_track,
clear queue, plural-aware footer count, exact empty states. New Lyrics
panel: qbz-lyrics service + per-user cache wired, Qobuz-first chain
with LRCLIB fallback, synced line view with line-level highlight +
centered auto-follow + click-to-seek, exact status bodies. Both panels
stack Feishin-style in the right column; NPB lyrics button wired.

km
2026-07-27 18:22:22 -06:00
vicrodh 86de977f3c feat(qbz-qt): phase 8 — AlbumView and ArtistView 1:1
AlbumPageView replica: 224px cover, credits, meta, description modal,
action row, quality badge + track search, exact tracklist (playing-row
pill, disc/work headers, live hearts, row menus), label/awards
sidebar, same-artist + suggestions carousels. ArtistPageView replica:
circular portrait, bio modal, From catalog/In library toggle (both
arms live), JUMP TO tabs with jump-scroll, Popular Tracks with Load
more, server-bucketed release sections paged per section, Appears On,
Network sidebar (labels, similar artists). Card clicks navigate
everywhere via the nav stack; new playback paths (play from track,
shuffled album, track lists). Owner data live: album 654ms, artist
670ms loads.

km
2026-07-27 17:49:08 -06:00
vicrodh 4d196223cd feat(qbz-qt): phase 7 — shared AlbumCard, rounded frameless window, sidebar playlist tree
One AlbumCard.qml replicating the Slint shared card (pin badge with
PinnedItemsService wired, heart, hover play overlay, exact context
menu with Play next/Add to queue wired) used by BOTH Home rails and
the Library All grid. Frameless translucent window with 12px rounded
corners, replicated WindowControls, header drag, 8 resize grips.
Sidebar playlist tree: 56 playlists / 12 folders live, expand state,
5-option sort with direction caret, recursive search filter, collage
covers, create-playlist wired. Fixed a std-Mutex self-deadlock in the
sidebar loader (two lock temporaries in one log statement).

km
2026-07-27 16:45:44 -06:00
vicrodh c06297c915 feat(qbz-qt): phase 5.5 — visual polish pass audited against the Slint sources
Screen-by-screen audit with before/after screenshots: rounded
surface-main content panel with 8px bezel (the Slack-style frame),
real splash card, QbzPrimaryButton/QbzCheckbox/QbzTextLink replicas,
accent arc spinner replacing BusyIndicator, custom thin auto-hide
scrollbars (Home + Library grid/list), carousel edge fades, toolbar
pill offset, QbzSlider/SeekBar metrics, alpha-format re-audit of all
8-digit hex literals. Pure look — zero functionality changes.

km
2026-07-27 15:19:24 -06:00
vicrodh 1b81b289d1 feat(qbz-qt): phase 5 — Library view 1:1 with All grid/list and perf marks
Six live tabs (All/Tracks/Albums/Artists/Playlists/Labels) with real
count badges, paged favorites/playlists/purchases loaders, library_all
merge semantics, heart routing (Qobuz API vs local store —
LocalFavoritesService now initialized at session entry), sort popup
with direction carets, grid/list toggle with natively windowed
GridView/ListView delegates, id-keyed artwork via signal with hand-
rolled eviction. Perf marks on the owner's account: 344 items, fetch
wall 2.7-3.4s (97% network, sequential), map+merge ~11ms, JSON
serialize 10ms, QML parse 7ms (~20us/item => ~140ms at the 2200-item
reference scale), RSS +29MB after publish.

km
2026-07-27 14:57:29 -06:00
vicrodh b405755c1a merge: pull smoke-2026-07-25 backend for the Library All POC phase
Brings the windowed Library All backend (qbz-library) and the latest
Slint reference implementation into the POC base. POC branch only;
pre-release untouched.

km
2026-07-27 14:05:08 -06:00
vicrodh 80dcd85a91 feat(qbz-qt): phase 4 — live playback, queue, and transport via QbzCore
Album-card click plays the album: get_album -> QueueTrack vec ->
set_queue -> play_track_resolved (tier walk stays in the core). 1 Hz
poll publishes position/meta/quality into the NowPlaying model; end-of-
track edge auto-advances with meta refresh (traced live: 24-bit/48kHz
CMAF stream, position advancing, track 2/3 transitions). Transport
invokables rewired to real core calls; queue panel shows NOW PLAYING +
numbered UP NEXT from get_queue_state. Public QbzCore API proved
sufficient — protected audio/player crates untouched. Named gaps:
no mute API, no playback event stream, auto-advance/prefetch semantics
remain frontend glue (~150 lines every UI must port).

km
2026-07-27 14:04:58 -06:00
vicrodh 95df643a25 feat(qbz-qt): phase 3 — Discover > Home with live Qobuz data and artwork
Pure mapping port of home.rs/foryou.rs (quality tiers, award ribbons,
slim grids) into Slint-free rows; discover index + favorites +
release-watch + top artists fetched concurrently (12 sections, ~170
cards live-verified). Artwork via the shared qbz-cache ImageCacheService
(semaphore-16 downloads, file:// rows, native QML decode). HomeView.qml:
tab pill, five rail kinds, shimmer skeleton, error+Retry, offline
placeholder — exact msgids throughout. Sections travel as one JSON
document (cxx-qt-lib cannot nest QVariant containers — documented).

km
2026-07-27 13:36:01 -06:00
vicrodh 744bce3819 feat(qbz-qt): phase 2 — app shell with 1:1 Slint visual parity
HeaderBar (42px, sidebar cycle, back/forward, centered search replica,
offline badge + flyout, app menu), three-state Sidebar (open 240 / mini
64 / closed 0, 160ms animation, nav rows with exact msgids), NowPlaying
bar SMALL (3px three-color seekbar, art+title+time left, transport
center, quality/volume/queue-toggle right, exact empty state), Queue
panel (300px animated column, Queue/History tabs, exact empty states).
Nav history stack + NowPlayingModel with empty-state defaults behind
new bridge properties/invokables (transport log-and-noop until
playback lands). Icons as pre-baked tinted SVG variants (ColorOverlay
is broken on the software path); Inter fonts bundled; full dark theme
tokens ported from the Slint foundation files.

km
2026-07-27 12:56:38 -06:00
vicrodh df7eb3588e feat(qbz-qt): phase 1 — login screen with full offline guardrails
QbzBridge QML singleton (12 properties, 8 invokables) driven by tokio
tasks via CxxQtThread. Port of auth.rs (browser OAuth loopback listener
with qbzd-style nonce, 180s timeout, cancel via JoinHandle abort,
scoped offline-gate lift for cold bundle init, D1 token-keep on network
failure / clear on explicit rejection, D4 subscription grace marks).
OfflineModeEngine + ConnectivityActor started at boot with live watch
forwarding (connectivity/captive portal/recovery banner/has-previous-
session). Boot: splash -> silent token restore -> shell | login.
Start-offline enters the guest profile. LoginScreen.qml is a faithful
port with exact msgids routed through qbz-i18n. 5 unit tests green.

km
2026-07-27 12:09:31 -06:00
vicrodh 469f83781d feat(qbz-qt): phase 0 — crate skeleton + cxx-qt toolchain gate
New qbz-qt bin crate (cxx-qt 0.7 + QML) as the only default workspace
member in this worktree, so plain cargo build produces just the Qt
binary. Gate verified against system Qt 6.11.1 (codegen, moc,
qmltyperegistrar, qrc load, offscreen run all green).

km
2026-07-27 11:36:18 -06:00
vicrodh ae519279f6 fix(cast): error-tolerant DLNA discovery loop + per-device logging (#667)
The window loop broke on the FIRST device whose description fetch/parse
failed, silently dropping every other responder in that window — and
every subsequent window, forever, with zero log lines. One malformed
UPnP device on the LAN (router, TV, printer) could starve the whole
DLNA list. Now each error is logged and skipped, and every discovered
renderer is logged at info level (name / ip / capability flags), so the
next field report is self-diagnosing.

km
2026-07-27 09:57:57 -06:00
vicrodh a69491a49e feat(player): re-wire the low-memory profile into qbz/qbzd (#660)
The system_capabilities profiler (Normal vs LowMemory, built for #331)
lost all its consumers in the Tauri removal; 2.0.2 shipped it as dead
code and qbzd got OOM-killed (exit 137) on 1 GB Pi hosts at track
transitions. Phase A wiring:

- Frontends read memory_profile() at startup and push it down via the
  new player memory_tuning (initial-buffer cap, L1 budget, low-memory
  gate) before the Player is created.
- L1 audio cache sized from the profile (400 MB Normal / 50 MB
  LowMemory; was hardcoded 400 MB).
- Oversized tracks (>L1/4 = 12.5 MB on LowMemory) skip the promotion
  clone and the parallel CMAF RAM accumulator entirely; the finished
  track is streamed to the L2 disk cache in 1 MB chunks straight from
  the playback buffer — no second in-RAM copy. Gapless degrades to a
  small gap for those tracks (a gap beats an OOM kill).
- Stream buffer preallocates total_size up front (try_reserve_exact,
  1 GiB cap, dynamic fallback) — no more ~8 doubling reallocs + memcpy
  per track (weak-CPU relief).
- New memory-pressure watchdog (15 s): critical pressure evicts the
  L1 (L2 disk kept) and halts prefetch; low pressure pauses HiRes
  prefetch (downgrades to Lossless). Spawned by both frontends; qbzd
  aborts+joins it for the #521 shutdown ordering.
- Prefetch honors the profile (depth cap, HiRes->Lossless downgrade).

Normal-profile behavior is byte-identical (every behavioral change is
gated on the LowMemory class; profile values equal the old hardcodes).

Phase B (follow-ups): tempfile spill of the live stream buffer for
giant tracks, POLLERR flood rate-limit, desktop kick_prefetch halt
check, L2-read on the play_track cache-hit path.

km
2026-07-26 23:56:39 -06:00
vicrodh 96acc4386a test(qbz-app): install the rustls CryptoProvider in shell tests
Follow-up to #663: with reqwest no longer pulling a rustls provider,
QbzCore::new (MusicBrainzClient) panicked "No provider set" in 6
shell tests. The test ctor now calls ensure_crypto_provider(), exactly
like the real binaries do at startup.

km
2026-07-26 23:56:38 -06:00
vicrodh ad6c98ff7d feat(queue): Play later everywhere — every context menu and bulk bar (#442)
Extends Play later (end of the manual block) from track rows to every
surface that already offered Play next / Add to queue:

- playback.rs: enqueue_album_later, enqueue_playlist_later (mixed
  sidecar interleave), enqueue_artist_top_later, enqueue_tracks_later,
  enqueue_local_tracks_later — all NATURAL order (each add_track_later
  appends, no reverse), QConnect controller routing via
  play_later_batch_on_peer_if_active, and post-add queue sync.
- local_playlist::enqueue_later_by_id (source-aware rows, offline-only
  stamp preserved), myqbz_play RowMode::PlayLater + BulkEnqueueMode.
- 19 dispatch arms in main.rs (album/playlist/artist/label/mix/
  favorites/local-library/album-page/disc/myqbz dialects).
- 12 context menus + 10 bulk bars in .slint: list-plus icon between
  Play next and Add to queue, popup heights bumped.

The immersive tap-action dropdown is deliberately untouched (it is a
configurable single-tap behavior setting, not a context menu).

km
2026-07-26 21:08:12 -06:00
vicrodh 09380d4634 Merge branch 'pre-release' into chore/internal/smoke-2026-07-25 2026-07-26 16:45:03 -06:00
b0bbywan 82fea6257c fix(qbz-audio): use alsa::pcm::Frames for ALSA buffer and period sizes
HwParams::set_buffer_size_near and set_period_size_near take alsa::pcm::Frames, which is libc::c_long: i64 on 64-bit targets but i32 on 32-bit ones. The three buffer_size computations in alsa_direct.rs cast to i64 explicitly, so the crate does not compile for any 32-bit target.

Casting to the alsa type instead is a no-op on 64-bit, where Frames already is i64, and correct on 32-bit. No behavioural change on any currently supported target: every value involved is a sample rate divided by 2, 4 or 8, so at most 96000, far below an i32 overflow.

Verified with cargo check -p qbz-audio:
  x86_64-unknown-linux-gnu       passes before and after
  arm-unknown-linux-gnueabihf    6 x E0308 before, passes after
2026-07-26 16:42:38 -06:00
Victor RH fe2be63f0f Merge pull request #663 from fengalin/fix/external/qbzd-add-tls-provider
fix(qbzd): add tls provider
2026-07-26 16:42:35 -06:00
vicrodh 6ca5813fb1 feat(mpris): expose xesam:url with the public Qobuz track link (#658)
playerctl scripts/widgets can now identify the exact song:
https://open.qobuz.com/track/<id> on every Qobuz catalog row (offline
qobuz_download keeps the real id, so it qualifies too). Local/Plex rows
omit the field — QueueTrack carries no file path to map to file:// and
a synthetic local id would mint a bogus Qobuz link. Wired in both the
app (refresh_now_playing_meta) and qbzd (track_meta).

km
2026-07-26 16:38:38 -06:00
vicrodh def93528bc fix(audio): open ALSA mixer on the card ctl id, not the PCM alias (#659)
set_hardware_volume attached the mixer to the stream's raw PCM id
(iec958:/hdmi:/front:/DEV-qualified hw:) — snd_mixer_attach needs the
per-card ctl device, so hardware volume silently no-oped on plugin-alias
devices (HiFiBerry Digi as iec958:CARD=sndrpihifiberry,DEV=0). Derive
hw:CARD=<name> / hw:<n> first. With this, qbzd's 'Hardware volume'
toggle reaches the DAC's mixer control (e.g. 'Digital') as intended;
with the toggle off, ALSA Direct stays fixed-volume by design
(bit-perfect, no software gain).

km
2026-07-26 16:32:30 -06:00
vicrodh 4d2ddec66e fix(ui): heart-crack icon for Remove from Library/favorites actions
Replaces the trash can (Favorites bulk bars, tracks + albums) and the
plain heart (Local Library bulk bar) with Lucide heart-crack — trash
reads as file deletion, not as un-favorite.

NOTE: touches .slint, so the next build recompiles qbz-ui (long path).

km
2026-07-26 16:26:58 -06:00