ed081cac6b26e382b1469b0e73d9e5e14aa22357
21
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
bd9aec1440 |
perf(qt): make the local albums grid actually recycle, and stop it asking
for the whole library
The owner's report is precise and is what this targets: the grid OPENS
fine; scroll, scrollbar drag and search are what feel sluggish, and "the
first two keystrokes hurt" — the ones where the filter still matches
hundreds. That shape is a model swap, not a slow filter.
UNSMOKED. Committed to save progress; no binary matches this tree.
1. FIXED CELL COUNT. `reuseItems` was already on the ListView and bought
nothing, because the pool recycles the row SHELL while the inner
`Repeater { model: modelData.items }` got a new array on every recycle
and rebuilt its AlbumCards anyway. The card is where the cost is: ~29
items, a RoundedImage with TWO render targets (its own layer plus a
separate mask layer), image probes, a skeleton. The shell was reused
and the expensive part was not. The Repeater now runs over `root.cols`
— a constant — so cells survive recycling and a scrolled or refiltered
row only reassigns `item`.
2. THE HEADER WINDOW BUG. `loIdx` fell back to 0 whenever the first
visible entry was a group header, because headers carried no `base`.
In grouped mode, scrolling with a header at the top of the viewport
asked `queueWindowReport` for artwork from album 0 through the current
letter — hundreds of covers dispatched and later evicted, per scroll.
Headers now carry the flat index their group starts at.
3. SEARCH DEBOUNCE on the albums box, which the Tracks box in the same
file already had. `albumsSearch` is not a query, it is the filter
input: writing it per keystroke refilters 1267 albums, rebuilds the
entry list and hands the ListView a new model, tearing down every
delegate. Note the tradeoff — this ADDS 200ms before any work, so the
box may feel more deliberate even as it stutters less.
Also here, from the album pages (lower priority per the owner — their
cost is paid once and the view is fine in use afterwards): the disc
header, which carried a CardMenu per TRACK, is Loader-gated; a genuinely
quadratic multi-disc scan in LocalAlbumView is hoisted out of the
per-row call; and the track rows are windowed. That windowing was
measured broken twice (a band computed against its own parent's derived
height — a dependency cycle) and the fix is unverified.
Credit where due: 1 and 2 came out of an independent review that also
judged the previous round's lazy menus and `reuseItems` "mostly beside
the main point". That is a fair call.
cc
|
||
|
|
dc2496e5c0 |
feat(qt): award pages, reachable from the album sidebar and from each other
Ports the Slint/Tauri award pair: the landing (gold laurel hero, magazine line, follow heart, "Acclaimed Releases" with its count and See All, the preview grid and the "Other awards" rail) and the paginated listing with its client-side search. Routes "award" and "awardalbums", the label pair's shape — one controller for both, since they share the hero, the follow state and the identity. The three rules from the Tauri review are in the module header because they are the ones a re-implementation gets wrong: the grid is ALWAYS /award/getAlbums (/award/page is user-scoped and may omit releases, so it feeds the hero and nothing else); the favorite split is plural-read, singular-write, because the backend derives `award_ids` from the write kind; and getAlbums has no has_more, its `total` is a hint, so the only sound predicate is `total > loaded`. The album sidebar's AWARDS cards navigate now — with an id directly, and by NAME through the resolver when Qobuz omits it, which it does. They were also reading `album.awards` when the field lives on `album.header`, so the block had rendered EMPTY for every album since it was written: an empty Repeater draws nothing and reports nothing, and the sidebar's own gate counted the same empty list. The LABEL card beside it had a "no label view yet" note and no handler, years after the label view landed. Beyond the reference: an all-awards dropdown pinned to the page's top-right, so any award is reachable without first finding an album that won it. It costs nothing new — the /award/explore crawl already existed to resolve names to ids. The award icon was lucide's MICROPHONE under the wrong filename. Search had routed around it with `crown` and left a note; kiosk was missed and drew a mic for "Most popular". The asset is the laurel the other two frontends use, and kiosk now asks for the crown it wanted. cc |
||
|
|
b136c9cf5d |
feat(qt): ambient background port, clip sweep and measurement tooling
Accumulated qt-frontend work from the 2026-08-11/12 sessions (cc), uncommitted until now: the app-wide dynamic background in both modes (AmbientField with GPU/Canvas arms, AppShell layering, ambient palette pipeline), the wave-1 clip/batch sweep across the tree (alpha batches 449 -> 327, reproducible), the rounded-cover and icon-tint arms, and scripts/qt-batches.sh (batches + presents/s sampler). The GPU behaviour of these pieces was finalized in the follow-up pulse commits; see qbz-nix-docs/qt-frontend/2026-08-11-scenegraph-batches/ GPU-COST-INVESTIGATION.md for the full campaign. km |
||
|
|
79e3cdc46b |
feat(qt): multi-select bulk bar on every Qobuz track listing
The album-page pattern (selection in QML, actions in Rust) generalized: - New shared engine bulk_tracks_qt.rs: one catalog batch-resolve that preserves the selection's visible order, then queue / play-next / play-later / add-to-playlist / add-to-mixtape / add-to-favorites / make-offline, with the queue context stamped per host. - PlaylistView: toggle in the header action row, bar inline above the column header, selection resets when the playlist changes. - ArtistView: the Popular Tracks header's dimmed toggle goes live; one selection covers Popular Tracks + Appears On + the in-library rows; the PopularTrackRow fork gets the select checkbox AND its offline cell/menu block stop being inert stubs. - LabelView: Popular Tracks gets the same treatment. - AlbumView: the bar moves from a floating overlay to the inline spot the Local Library album view uses (bar in flow, content below). km |
||
|
|
d112c726b6 |
fix(qt): custom album covers propagate to every surface
The per-builder sweep was whack-a-mole — the report caught empty cards in Home > Recently Played Albums and the NPB, and mosaics still showing the Qobuz art. Overrides are keyed by album id but most surfaces carry only the art URL, so the link is now the artwork HASH: every size variant of one Qobuz cover shares its filename stem. Setting a cover records hash -> path in a Qt-side store (custom_cover_keys.json — same reason as the playlist store: the Slint file round-trips known keys only), the album page backfills it for covers set before the map existed, and artwork_qt's cached_path / cached_raw_path answer the override for any URL whose hash has one — cards, NPB, MPRIS, queue, collage tiles, no per-surface wiring. download_missing skips overridden URLs. km |
||
|
|
83b8bd8890 |
feat(qt): offline downloads + multi-select on the album page
Two AlbumView parity features sharing the row machinery: OFFLINE CACHE (the menu rows and the per-track state the report was missing): - offline_qt.rs activates the per-user OfflineCacheState on login / session-restore / offline-session (one fan-out point in bind_per_user_stores) and tears it down on logout — same on-disk store as the other frontends, so copies made in Slint/Tauri show here and play here. - playback_qt's play funnel now passes the active cache + the row sink to play_track_resolved (it passed None, None: the offline tier was unreachable even for downloaded tracks). - offline_cache_qt.rs ports the action set: cache_track / cache_tracks / cache_album / redownload_album / remove_cached / refresh_cached, with toasts and a bridge-signal row sink (trackCacheStatusChanged). - TrackRow's download column is live: cloud / spinner / accent check / red alert per status (0/1/2/3/4), click map ready->remove else download, and the context menu's offline block (Make available offline / Refresh / Remove). Status is seeded per row from the session cached-id set (album/artist/label/for-you docs) and pushed live over the signal. - Album ⋯ menu: "Make available offline" swaps to "Refresh offline copy" on the live all-cached aggregate. MULTI-SELECT: the toolbar toggle is live; selection state stays in QML (LibraryView precedent), rows check off in select mode, the bulk bar carries the reference's full inventory — select-all, play-next, play-later, queue, add-to-playlist, add-to-mixtape/collection, Add to Library, make-offline, clear — and the Ctrl+A/Escape router duck-types the view's selectAll/exitMultiSelectMode like the other tabbed views. Still out (documented in place): the Offline Manager view, the row padlock on unlock, per-disc actions, playlist-level cache button. km |
||
|
|
d9b1c26ff3 |
feat(qt): album page parity — cover menu + lightbox, header actions, album info modal
Four wired surfaces on AlbumView, all previously inert stubs: - Cover menu (right-click, AlbumPageView.slint:300-370): Add/Change/Remove custom cover over the SAME custom_artwork.json store the Slint app uses (a cover set in one app shows in both), Open in browser, Save as…. Plus the NEW lightbox (menu entry + left-click): the art at its best available quality fitted to the window, custom override preferred. - Header action row: radio (the existing foryou_qt album-radio seam), booklet (visible when a PDF goody exists; downloads via save-as — no in-app reader, like the reference), and add-to-mixtape/collection — the MyQBZ picker's Accepts engine already lets albums target Collections (the Tauri rule), so the button just feeds it. - Album Info modal (the extensive one): fresh /album/get → credits grouped through the shared qbz_qobuz::performers parser, per-track copyright, review tab when the album has editorial text, label link, musician navigation, per-track play in album context. Tauri layout (left fixed meta column, right scrollable tabs) per the project ruling for this feature; the header's label link is wired along the way (LabelView has existed for a while — the POC-NOTE was stale). - … menu: Add to playlist (the loaded view's track ids into the picker), Add to mixtape, Share Qobuz link, Share Album.link (UPC -> Deezer -> album.link, #514 path — the resolver half of share.rs is now ported for the album arm). The offline rows stay out: no Qt album-cache seam. i18n: "View cover" + "Could not load the image" catalogued in all seven locales. Gate: both QML audits + 305 tests + debug/release builds green. km |
||
|
|
ec8588687b |
feat(qt): artist page parity - load more, section sort, discography
The release-section "Load more" fetched a page and appended it to a structure
no view ever re-read. The handler pushed IN PLACE into the object behind
`readonly property var artist: JSON.parse(QbzArtist.artistJson)`, and the
releaseSections projection returned that same array on the unfiltered arm, so
both Repeaters were re-handed the identical reference and nothing was dirty.
The tree's own rule, written in cards/PlaylistCollage.qml: rebind requires a
NEW object reference. A second bug sat on the same path - the artist document
republishes progressively (stories, then each MusicBrainz section as it
resolves) and every republish re-runs the parse, wiping anything appended, so
fixing only the first would have left a bug that looks random. Both
pre-existing; the in-flight placeholder added earlier is what made an
always-silent no-op visible.
Appended pages now live in an overlay keyed by release type. It survives a
republish, emits new arrays so the Repeaters rebuild, dedupes by album id,
carries the reference's 4-page cap per bucket (artist.rs:718-720, which hands
off to the discography page beyond it), and is guarded against a page
requested on one artist landing on another's grid - releaseSectionReady
carries no artist id and every artist has an "album" bucket.
"See discography" was a pointer-cursor MouseArea with no onClicked under a
POC-NOTE saying the page was out of scope. ArtistReleasesView.qml +
artist_releases_qt.rs port ArtistReleasesView.slint: infinite scroll rather
than a button, plus the loading, error-with-retry, empty and offline arms.
The album page's "More from <artist>" view-all is a second door to the same
page. Its paging offset counts what the SERVER returned rather than the
rendered row count the reference uses: blacklist filtering and id dedupe make
the rendered count smaller, so the reference re-requests seen rows and skips
the tail whenever the blacklist is non-empty.
The per-section sort was a dimmed Rectangle with no MouseArea, under a
comment asserting no seam existed. It is now a real QbzSelect with the
reference's five options and index mapping, persisted per release type in
artist_prefs. Choosing "Default" after a sort does not restore the server
order until the page reloads - that is the reference's behaviour
(album_map.rs:262 is a bare no-op arm and resort sorts the live model), kept
deliberately rather than diverged.
Also, two visual fixes from the same round that share these files: Popular
Tracks drew its cover at radius 8 where the shared rows/TrackRow.qml uses 4,
and the album page's rail mounts said `width: parent.width` inside a Column
with 32px padding - QML padding shifts children without narrowing
parent.width, so the rails hung 32px off the right and the right page chevron
landed outside the window.
Carries scripts/qt-diag.sh, which has been sitting untracked since the
rendering round: it answers "which backend is this actually running on" from
a real graphical session, refusing ssh and refusing offscreen because either
would answer the wrong question. That refusal is the point - the doctrine it
disproved ("shader effects render nothing on this port's software path") came
from an offscreen observation, and offscreen forces software by definition.
cc
|
||
|
|
b6cf06ff2a |
feat(source): wire My QBZ through the seam, and land the owner's container rules
Stage 2. The measured win: opening a 34-item collection now costs **2** library.db opens instead of 23-34 — the local source owns a pooled handle and hands out no guard, only `with(|db| ...)`, so a `!Send` `&LibraryDatabase` cannot cross an await either. Bug 1 from the seam design is dead by construction, not by care. The owner's rules, decided today, now live in one place instead of eight: - **Collection** takes albums, singles and EPs from ANY source — qobuz, folders, Plex, and whatever is added next. **Mixtape** takes albums, playlists and tracks. This was a single boolean (`restrict_to_mixtape`) that could express neither release types nor "collections accept local". - An unsupported combination must never be VISIBLE: the picker stops offering the impossible action, and the error stays a log-only safety net. Owner: "el bloqueo no debería ser visible nunca, más bien esto hay que ajustarlo en la UI". - **Ephemeral honours its name** — playable, queueable, and offerable to nothing else. - A row's source comes from the ROW. Eight QML sites built the add-to-mixtape payload with `"source": "qobuz"` written as a literal, so a Plex or local track was stored as Qobuz and afterwards could not play at all: the app asked the Qobuz API for an id that never existed there. `QobuzSource` reads the client through a lens on every call instead of holding a clone. That is not style: `qbz-core` REPLACES its client (core.rs:346 and :384, both on paths that run before `set_session`), so a cached clone goes stale and fails silently — the hazard the crate's own doc comment described. The lens is installed in `on_boot`, first of everything, and finding out why is the lesson of this commit. The gate was green — build, 142 tests, both QML audits, a clean offscreen smoke — and the feature was visibly broken: every Qobuz row showed "No results found", an empty Quality cell and a wrong "SINGLE" type, while the Plex rows resolved fine. The registry is a OnceLock, so whoever touches it first BUILDS it, and one built without the lens leaves Qobuz permanently detached. Only driving the real app surfaced it; the crate's error string named the cause exactly, which is the one thing that made it a ten-minute fix instead of an afternoon. cc |
||
|
|
494596616e |
feat(qt): port the Blacklist manager for artists and albums
The port had a 67-line settings row showing two counters and a header saying the Manage chevron was "NOT shipped — this port has no Blacklist Manager view for it to open". The Slint has 1,114 lines of manager plus the consumer surfaces that make the feature mean anything. - src/artist_blacklist.rs is a 1:1 port of the reference's per-user lifecycle, including the fail-open asymmetry (is_enabled reads TRUE when there is no session, everything else false/empty) and the byte-identical NO_SESSION_ERR. src/reco_dismiss_qt.rs carries the reco-dismissal store. Both are bound in ALL THREE session-activation paths — the offline one included, because the reference records that Tauri never bound it offline and blacklisted artists leaked into offline surfaces — and torn down on logout, or the next account inherits them. - The manager view: three tabs (bordered, ADR-008 — no pills), search, sort, the enable toggle, the disabled warning banner, the four body branches per tab in their load-bearing order, windowed lists, and the clear-all confirm. Reached from Settings only, exactly like the Slint; no sidebar entry. - Every one of the ten store mutations publishes TWICE — its own document and publish_settings() — because the Settings counters live in a different document and nav_qt::back() re-runs no per-view load. Without it, Manage -> unblock -> Back showed pre-mutation counters. The Slint gets this free from a shared global. - The search field is driven imperatively, not by a binding: QbzLineEdit's X used to destroy the binding, after which the box went blank while the list stayed filtered. - Consumer surfaces, which is where the feature actually lives: the album card and album list row gained "Block this album", the artist card gained the 3-arg "Not interested", the album header menu gained Block/Unblock swapping on state, and the artist page gained the Show/Blacklist flip plus the amber hidden banner with its "Show artist" button. album_qt and artist_qt now seed isAlbumBlocked / isBlacklisted at document build time — without the seed an already-blocked album offered "Block this album" and the first click UN-blocked it, the same lie the pin glyph told before isPinned existed. - search_qt passed &Default::default() for both blacklist snapshots, so nothing was filtered anywhere and a greyed row still played. It now passes the real snapshots at both call sites. - The 17 toasts the spec had dropped for want of a toast host are wired: that premise stopped being true when toast_qt landed, and a Clear All that failed with NO_SESSION_ERR was completely silent. Their msgids are in all eight catalogues; the five album-axis ones existed only in es and nl. Also fixed in passing, pre-existing on this branch and unrelated to the blacklist: ArtistView's library tab read `libTracks` unqualified from a child of the object that declares it, which QML resolves against the component root, not the enclosing object — so the "Tracks" heading threw a ReferenceError and never rendered. Found by forcing the artist route in the offscreen smoke, which no earlier gate did. cc |
||
|
|
a3b89107fc |
fix(qt): table headers line up, the mini rail is centred, tooltips have a background
Three of the owner's visual reports, one theme: geometry that was derived in two places and drifted. The track table laid its header out from the LABELLED columns only, so the heart and the overflow menu took no space there and every label sat right of the column it names. Header and rows now come from one shared set of widths, including the responsive arms -- if the header hid columns on a different condition than the row, the drift would come back only on narrow windows. The collapsed rail goes back to Slint's 64px. It had been 50 by owner request, and the measurement showed why that was the actual defect: the rail centred its row at 25 while the header's own sidebar toggle, sitting directly above it, centres at 30. Symmetric padding inside a 50px rail still reads as misaligned because the eye compares the glyphs to the header control, not to the rail; chasing it from inside bought the alignment at the price of a visibly asymmetric hover pill. At 64 both references land 2px apart and the padding is symmetric again. The row stays a literal 34 rather than being derived from the width -- deriving it is what coupled the two tokens and would have silently grown every row to 48. Tooltips: the port had none, while Slint has a shell-level overlay. This adds the overlay rather than a Popup per call site. cc |
||
|
|
81c43a114e |
fix(qt): glyph colours follow the theme, on light palettes and on accent fills
Two defects with one cause: a tint name that means a frozen hex. "primary" is the legacy alias of literal #ffffff, not Theme.text-primary, so every control passing it drew a white glyph. On the eleven light themes that is white-on-light -- the header back/forward chevrons, the sidebar toggle and the carousel arrows the owner marked. They now pass the theme-following names, which icon_tint_qt bakes at runtime. The stale `hslLightness > 0.5 ? "primary" : "black"` workarounds that predate the runtime tinter collapse into those names, and the qrc floor underneath them becomes polarity-aware: dir_for() returns "" whenever the cache dir is unavailable, a bake fails or the theme document has not been published, and that path used to resolve textPrimary straight back to the white bake -- reproducing the bug it was fixing, app-wide, in the degraded state. On accent fills the fix is a deviation from Slint, approved by the owner after measurement. A fixed white glyph on Theme.accent falls under 2.6:1 on 16 of the 35 shipped palettes, including high-contrast at 1.70:1 and wcag-dark at 1.82:1 -- the two themes whose reason to exist is contrast. Slint hardcodes #ffffff there (CircleAction.slint:72 and three others), so the bug is upstream. accentText alone is not the answer either: it is picked for the theme's text ramp and gives 2.56:1 on Rose Pine Dawn, worse than the black it replaces. So QbzTheme picks the best-contrast tint by real sRGB luminance against the live accent, and the labels beside those glyphs use its colour twin -- BrowseGenreButton and LibraryView were drawing a white glyph next to an accentText label inside the same pill. Rounds out the two parity items the same files owe: QbzNavButton gains the ambient arm from Carousel.slint (wired at the six carousel sites, not left dead), and QbzCircleAction rounds its glyph to whole pixels the way CircleAction.slint does. cc |
||
|
|
2a5b5ac16e |
feat(qbz-qt): theme-following icons, the real header atmosphere, sharp covers, playback context
**Icons follow the theme.** The bakes are frozen dark-palette hexes, so on light themes secondary, muted and accent were simply wrong, and accent was wrong on any theme whose accent is not #4285f4. Slint recolours at render time, which this path cannot do — so the tint is generated once per colour into the cache and loaded through the same Image+svg path the bakes already use. No effect, no shader, so it cannot hit the "renders nothing on software" failure. The sweep found the part the diagnosis had missed: "primary" was two meanings welded together — max-contrast-on-theme-surface, and literal white for glyphs on hosts that are dark under every theme. Remapping it globally would have painted dark glyphs on dark scrims in fourteen places. The vocabulary is split instead, with "primary" left as an alias so nothing changes until it is migrated deliberately. **The album header gradient was Slint's fallback, not its design.** Slint paints a blurred artwork field and only falls back to the flat two-band gradient when that image is unavailable; the port shipped the fallback. The field needs no shader — it is a 128x128 image, so the pipeline ports verbatim: the cover down to 8x8, back up to 128, blur, colour adjust, vignette. The 8x8 step is the look, not an optimization; everything after it works from eight pixels of colour, which is why it reads as atmosphere rather than as a blurred cover. It comes out of the same decode that already produced the flat tint. **Covers were point-sampled.** Canvas drawImage does not filter when it scales, measured at RMSE 0.000 against a nearest-neighbour reference — a 600px cover in a 200px cell throws away eight of every nine pixels, and the error is aliasing, not softness. Rust now produces a derivative at the drawn device size so the draw is 1:1 and there is no resample at all. **The now-playing context stack.** It had no Qt counterpart: the model carried context_kind and context_id, some builders stamped them, and nothing ever read them back. The album path only appeared to work by coincidence, because the glyph was hardcoded to open the album. Plus: Discover's missing rails with real play history, Recommendations on the integrations, queue drag reorder, and the playlist detail collage. cc |
||
|
|
a1c771c39e |
fix(qbz-qt): row buttons, cover triggers, light-theme contrast, flyouts, genre filter
**Track-row buttons did nothing.** Declaration order is z-order in QML, and the row-wide MouseArea added for right-click was declared last — so it covered the heart, cloud and menu buttons and swallowed their presses. propagateComposedEvents cannot rescue that: a press is not a composed event, which is why only the row's own double-click still worked. **Covers appeared only after a scroll.** Three compounding defects, not one. The window report fired on contentY and on model change, both of which happen while the tab body is still hidden behind its loading flag, so the report that mattered hit the not-visible early return and was discarded with nothing to re-fire it. The debounce reset on every change, so a burst of mount-time reports kept pushing its trailing edge out. And a single pending slot meant that when two surfaces are mounted at once — the artist rail and that artist's grid — the second overwrote the first, so one pane was never requested at all. Eviction had the same shape: each report deleted everything outside its own array, so the two panes deleted each other's covers. **Latest release had no cover** because the artist page's collector skipped it, along with Appears On and the playlists strip. **Light themes.** The mechanism is Slint's, from 2.0.2 and issue #638: the on-surface arm on circle actions, header-light deciding the header foreground only when a dark backdrop is actually painted, and the circular backing behind the track-row play glyph. Ported as specified rather than approximated. Two icons rendered in their SVG's own colour because they pointed at the raw asset instead of a baked tint — a sweep found the rest of that class. **Navigation flyouts** exist, one component serving both the sidebar and the header nav modes. **Filter by genre and the Discover sort now work**, everywhere the control is drawn. The genre ids were already a parameter on the discover fetch that the port always passed empty. cc |
||
|
|
9ab3d7db96 |
fix(qbz-qt): local albums route to the local view, and the detail-page parity round
**The empty album view.** Opening a local album from Library "All" landed on a blank page. The mixed feed hands Qobuz and local albums to the SAME card, and the card always called the catalog open — so a local id, which is a group key or a filesystem path, went to /album/get and came back 404. It was in the owner's log before this round as `get_album(/mnt/nas/…) returned 404`; what changed is that clearing the stale document turned "shows the previous album" into "shows nothing", which is how it surfaced. Routing now happens in open_album itself, using the predicate the feed already uses to route favourites, so every call site is covered at once instead of card by card. **Header atmosphere.** Both detail pages get Slint's two-band gradient, tinted from THIS page's artwork through the ported header_tint — not the now-playing ambient triad, which is a different subject with a different algorithm. **Artist page.** The catalog/library toggle existed but was broken: a hand-rolled copy of the shared tab bar whose delegate walked the wrong parent chain, so it threw on every evaluation and never took its active colours. It now mounts the shared control. Added the JUMP TO label, the tab-strip search with the client-side filter it drives, and the year under release-section albums. The Network panel ran the full window height and never auto-opened; it is bounded below the tab strip now and opens unless the content is constrained, as Slint does. The action circles were the wrong size with the wrong glyph colour and no overlay arm — which would have left them dark-on-dark once the gradient landed. **Quality in track rows.** The column rendered a bare tier; it carries the badge now, in the one shared row component every surface mounts. playlist_qt was the only producer not emitting the bit-depth line. **Skeletons hand over instead of disappearing.** They cleared when the PATH arrived, but the image still had to load and rasterize after that, so the placeholder vanished and left an empty cell. They now wait for the image to be ready. **Local covers.** Most local covers ARE already the scanner's thumbnail, but re-hashing their path produced a different filename, so each one was decoded, resized to its own size and re-encoded into a duplicate before being shown. That was most of the wait. cc |
||
|
|
f3a33245b2 |
feat(qbz-qt): staged loading, skeletons everywhere, lyrics, gestures, richer menus
**Deferred external rows.** The album page awaited the Last.fm row before publishing anything, so the tracklist waited on an external service plus MusicBrainz. It publishes in stages now — header and tracks first, then each Qobuz rail as it lands, then Last.fm — with every patch merged under a generation counter so a late reply for album A cannot paint onto album B. The artist page was already staged this way; its views were the part still showing bare "Loading…" text. **Skeletons as the standard.** One control, new variants rather than new components, mounted across the views. Its second job is the one the owner named: cleanup. A cover that never resolves settles the placeholder once and reveals the card's own empty state instead of shimmering forever — local albums with no embedded art produce no artwork entry at all, so a placeholder gated only on "not arrived yet" would never stop. **Local artwork streams.** Fifteen seconds to first cover on a 1274-album grid was one batch emitted after the whole window resolved. Each cover emits as it resolves now, visible band before prefetch margin. **Lyrics migrated.** Smooth sync instead of a 1 Hz tick, word-level karaoke where the document carries it, the surrounding-line dimming, the controls flyout and the translation layer the v10 backend already supported. Exactly one row subscribes to the 30 Hz output; the timer drops to 250ms when paused or hidden. **Navigation by mouse and touchpad.** Side buttons and a two-finger horizontal swipe, matching #653 — including the part that matters, which is that a swipe over a carousel scrolls the carousel. The layer is declared first in the shell so everything else gets first refusal. **Context menus.** Brought to the Slint inventory, Play later included, with right-click opening the same menu as the button everywhere. Entries whose seam does not exist are hidden rather than shipped inert. Two controls that rendered and no-opped are now honestly disabled: the My QBZ nav row, which routes to a view the shell has no case for, and three sidebar menu entries with no backing call. The sidebar caret was never a condition bug. The row is a positioner and the name cell's width ignored the count label, so a folder WITH content pushed the chevron past the clip — which is why it vanished exactly when it mattered. cc |
||
|
|
d76f694781 |
feat(qbz-qt): close the two declared debts on the album and artist pages
The Last.fm similar-albums row exists now. The recommendation building was already frontend-agnostic in qbz-external-reco, so this only adapts QbzCore to its catalog trait and publishes the result; the row seeds on the album's artist and excludes what the two Qobuz rows already show, including ids the pre-resolution dedup misses. Resolved rows cache per album for thirty days in the same store the Discover tab uses, so re-opening an album makes no Last.fm or Qobuz calls, and only a non-empty result is cached — a transient failure must not hide the row until restart. With Last.fm not connected it resolves to nothing and makes no network call, so the rail simply never appears. Relationship rows navigate. They carry a name rather than a catalog id, so the click resolves through MusicBrainz first and only a CONFIRMED match opens an artist — landing the user on a same-name musician is worse than a row that does nothing. The role travels with the name so a match in another role is not mistaken for this one. cc |
||
|
|
49ab5f5b9d |
feat(qbz-qt): fill the Album and Artist pages from the integrations
The two pages were missing most of what Slint shows. With MusicBrainz, Last.fm and ListenBrainz wired this round, the sections that depend on them can exist: the artist biography and network, relationship rows, listener figures and the credit blocks the Slint pages carry. Nothing is faked. A section whose data an integration cannot supply is absent rather than an empty frame, and a section fed by an integration the user has not connected degrades to absent without a network call — integrations stay strictly opt-in. The relationship rows are non-navigable for now because resolving a musician name to a catalog artist needs a bridge invokable that is not there yet; an informational row is better than a dead click. cc |
||
|
|
ade156a72a |
refactor(qbz-qt): split QbzAlbum and QbzArtist out of the God-object
Two singletons rather than one merged object, per the owner: each maps to the view it backs. Registering them and deleting the duplicates from bridge.rs is one change, because wiring one without removing the other compiles fine and leaves two independent states with the view frozen and nothing in the log. view_param_id goes with them. It had no consumer in QML, verified, and none in Rust once the two opens stopped writing it — so it is deleted rather than carried into a new bridge for symmetry's sake. bridge.rs is down to three domains: settings, search and playlist. cc |
||
|
|
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 |
||
|
|
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 |