77e9feccffdda8f2ae29695a48c9dd810bb74bc7
74
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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 |
||
|
|
acf23abc9b |
feat(qt): port the My QBZ domain — mixtapes, collections and the discography builder
The Slint reference is 3,885 lines of .slint across ten files plus 5,514 lines
of Rust glue over the shared qbz-mixtape crate. None of it existed in the Qt
port; the nav flyout carried a "GAP:" comment and two disabled entries.
Rust (flat in src/, per QTBUG-93443): three bridges rather than one God-object
— QbzMyQbz (grids + detail + edit + cover + mix), QbzMyQbzAdd (the app-wide
picker, kept apart so TrackRow, PlayerBar and the Local Library bulk bars
cannot couple to detail state) and QbzDisco (artist-scoped, reached from the
artist page, never coexisting with the MyQBZ views). Twelve controllers behind
them, ported function by function from myqbz{,_detail,_play,_edit,_cover,_mix,
_add,_prefs,_view_prefs,_builder}.rs. src/toast_qt.rs finally gives the port a
toast host.
QML in qml/views/myqbz/ plus four global overlays. Reused rather than forked:
TrackRow, TrackListHeader, QualityBadgeFull, QbzContextMenu, CardMenu,
QbzCheckbox, QbzSkeleton, QbzScrollBar, RoundedImage. LocalSegToggle and
LocalMultiSelectBar are PROMOTED to controls/QbzSegToggle and
controls/QbzMultiSelectBar (they were ports of shared Slint primitives that
MyQBZ needs too) — a move plus an import fix, with the Local metrics kept as
the defaults so the four Local Library toggles are byte-unchanged and the MyQBZ
call sites pass ViewToggle's own numbers.
Every list is windowed at write time: GridView/ListView with reuseItems and a
cacheBuffer, and expanded mode is an outer recycled ListView whose delegate is
a Column + Repeater, because a nested ListView inside a recycled delegate
cannot size itself. Single-row updates patch in place; replacing the array
resets contentY and throws the user to the top.
Shared controls gained the arms the new views need, with the numbers taken from
the .slint and not guessed: SettingsButton (btnHeight / minWidth /
trailingIconName), QbzEmptyState's four metrics, QbzSelect's `sm` variant
(30px, r6, no outline, 12px label, 14px chevron, 40px narrower —
QbzSelect.slint:74-148), QbzContextMenu.openBelowLeft, and QbzLineEdit gains a
distinct `accepted` signal fired on ENTER ONLY: a modal that submits on
`committed` submits TWICE, because closing it removes focus and fires
`committed` again — a rename would have run twice and a create made two
collections. clearSearch() no longer assigns root.text either; that JS write
destroyed the call site's binding permanently, so Rust could never re-seed the
field after one click on the X.
SourceIcon gained the plain "qobuz" arm it was missing, which had every Qobuz
row in a collection drawing itself as a local file.
Two icons (turntable, image) are baked from the Slint masters into the seven
full tint dirs and registered in MASTERS; three more (circle-check-big,
circle-alert, triangle-alert) back the toast. A missing tint dir or glyph
renders nothing and logs nothing, so this class never surfaces in a build.
cc
|
||
|
|
d4667f5ea9 |
perf(qt): rounded covers use a real mask, the spectrum ticks at 62Hz, the derivative cache is bounded
The track's "ShaderEffect / ColorOverlay / MultiEffect render NOTHING on the
software path this port runs on" doctrine was measured FALSE on 2026-07-29:
qbz-qt runs on the GPU (OpenGL RHI, threaded render loop, vsync 5.56ms). The
claim came from an offscreen session, and QT_QPA_PLATFORM=offscreen forces
software by definition. Every design it justified needed re-deciding.
- RoundedImage was a Canvas (renderTarget: Canvas.Image) that rasterised with
QPainter on the CPU AND uploaded a texture per repaint, for every cover in
the app, purely to round a corner identical in every frame of that image's
life. It is now layer.enabled + MultiEffect { maskSource } — an FBO rendered
once and cached for static content. Rectangle { radius; clip: true } cannot
do this: clip in Qt Quick is a RECTANGULAR scissor and ignores radius.
maskThresholdMin/maskSpreadAtMin are 0.5/1.0, NOT the 0.0/0.0 that looks
natural: 0.0 collapses Qt's mask smoothstep to step(0, alpha) and every
pixel with any alpha snaps fully opaque — measured 0 antialiased pixels at
radius 40 against the Canvas's 292. At 0.5/1.0 it is 308, ramp [0,51,219,255].
A software machine keeps the Canvas arm (GraphicsInfo.api), and
QBZ_QT_ROUND_MODE=canvas pins it on a GPU machine where the mask misbehaves.
- VizSettle drove the spectrum from FrameAnimation, i.e. every rendered frame:
180fps on this box for data the producer publishes at TARGET_FPS = 30. Now a
16ms Timer; the wall-clock maths is unchanged.
- SpectrumBand's per-bar y was a SECOND JS binding derived from height. An
anchor does the same arithmetic in C++ with no binding at all, and each FFT
frame is materialised into a plain array once per publish instead of being
read element by element across the JS/C++ boundary. Bindings per tick: bars
28 (was 56), waveform 48 (was 96), energy 5 (was 10).
QQuickPaintedItem was investigated and rejected with evidence: cxx-qt 0.7.3
emits X(QObject* parent) : base(parent), which does not compile against
QQuickPaintedItem(QQuickItem*), paint() is pure virtual, and there is zero
precedent for a custom base class in this workspace. Qt 6 ShaderEffect takes
no GLSL, only precompiled .qsb, which would make the build depend on a Qt
tool whose path differs on macOS.
- PlaylistCollage point-sampled its mosaic in its own Canvas
(Context2D.drawImage does not filter when it scales — measured RMSE 0.000
against nearest-neighbour). It now draws through RoundedImage.
- The scaled-derivative cache had no eviction at all and the .jpg -> .png key
change had orphaned 1251 files. Boot housekeeping sweeps the orphans once and
bounds the directory at 64 MB, LRU by mtime. Verified by effect: first run
deleted 1259 files / 10.05 MB, every run since reports 0, and the directory
is 14 MB. The eviction is only survivable because RoundedImage clears
_scaled on Image.Error and re-requests.
- For You artwork patched the rails in place instead of republishing the
document, which was resetting contentY. HomeView is destroyed on every
navigation, so the patch map is re-handed on mount through
QbzHome.refreshForyouArt() — otherwise the Radio rail and the Spotlight hero
went blank for the rest of the session after one navigation, and the skeleton
pulse Timer stayed pinned ON, defeating the fix it belonged to.
- Spotlight's placeholder gated on the local path instead of the remote url, so
it flashed a glyph on a cold cache where the Slint does not.
The retired doctrine is corrected wherever it was load-bearing for a decision.
The pre-baked icon tints are deliberately NOT reopened and no drop-shadows are
added: both are visual changes, not perf fixes.
cc
|
||
|
|
0c159c141c |
feat(qt): For You gains Qobuz Mixes pages, Artist Spotlight and Radios
Ports the three sections the tab was missing, against the Slint implementation rather than invented: the section rails, the cards, and the mix landing page with its navigation seam so back/forward behave. The mixes reuse the shared crates for their requests -- the Slint app already makes them, so nothing new talks to Qobuz here. Also carries the qt-run.sh portability work needed for the Mac mini: `free`, `nproc`, `pgrep` and `timeout` all differ or are simply absent on BSD/macOS. Two of those bit during the first Mac build. `pgrep` exits 1 when it matches nothing -- the normal case -- and under `pipefail` that killed the script before a single line of output, which reads exactly like a build that did nothing. And `timeout` does not exist on macOS at all, so the smoke produced a one-line log over which the grep cheerfully reported "0 QML complaints": a test that never ran, rendering as a pass. The smoke now fails outright on a log that short. 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 |
||
|
|
6b5226acf9 |
fix(qt): covers are actually downscaled, and non-square art keeps its ratio
The owner's clue was the whole diagnosis: the same image looked fine at 220px in the hover preview and bad at 150px in a card. So not the source, and not the URL variant either -- every Qt producer picks exactly what Slint picks, `large` (600px) into a 200px cell, 3x headroom. `artwork_qt` was producing the right derivative and `RoundedImage` was drawing the untouched 600px original next to it. `Context2D.drawImage` does not filter, so every card was a nearest-neighbour downscale: measured on a card cropped out of the running binary, RMSE 0.000 against a point-sampled reference. The preview looks right because it is a plain Image with smooth+mipmap and never touches this path. Three defects were latent behind that dead derivative: `thumbnail(w,h)` does not preserve aspect (46 square covers on disk written as 32x28), `to_rgb8` matted transparent logos onto black, and the file was JPEG q75 -- more damage than the resample it was meant to cure. Qobuz playlist art is 800x380, and both the header and the card were stretching it to fill a square. It now keeps its ratio, and the uncovered area takes a gradient derived from the image itself, reusing the ambient colour pipeline rather than starting a second one. cc |
||
|
|
d5bf4398ac |
fix(qt): Library sub-tab switch no longer segfaults, and the pane keeps its corners
Three identical faults in dmesg over three days, all `segfault at 8 ... libQt6QmlModels.so.6.11.1[420c5]`. `nm -DC` puts that offset in `QQmlDelegateModel::model()`; the QVariant return means sret, so %rsi is `this` and address 8 is a null pointer plus the member offset. `QQuickItemView::model()` reads its d-pointer, checks a QPointer guard and then calls straight through without testing the value. The re-entry is `onContentYChanged`, not `onModelChanged` as first assumed: `setModel()` emits it from inside the window where the old delegate model is already gone. Switching to Labels re-derives the model AND clamps contentY to a shorter list, which is the emit -- hence the intermittency, since at contentY 0 nothing is emitted and nothing crashes. Reproduced in plain qml6 with no app and no login, and bisected: readback in onContentYChanged dies, in onModelChanged survives 401 swaps, cached array survives everything. The fix is to stop asking the view for its own model. A tree-wide audit found `.model` is the only member that is unsafe this way in 6.11.1 -- count, contentHeight, indexAt and itemAtIndex all null-check -- and the three LocalLibrary tabs already cached their arrays, which is why this stayed a Library-only bug. Also: `clip: true` is a RECTANGULAR scissor in Qt Quick and does not follow `radius`, so the album/artist header band painted over the content pane's rounded corners. Slint clips to the rounded shape on the same element, which is why the identical structure is correct there. The mask sits OUTSIDE the frame rather than at a high z, because ADR-009 pins in-pane modals at z >= 3000 with no ceiling and a z fix would be a standing arms race. 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 |
||
|
|
c56fab3c4e |
fix(qbz-qt): covers were never actually resized, plus the adversarial parity round
**The scaled-cover fix from the last round produced nothing.** The seam ran — 462 requests in one offscreen smoke — and wrote zero files, so the owner was right that nothing improved. The artwork cache stores files as `.img`, and `image::open` picks its decoder from the EXTENSION, so every call failed silently. Sniffing the content instead yields 101 derivatives on the same run. The header atmosphere had the identical bug and had therefore never produced an image either, which is why the flat fallback was all anyone ever saw. Reported as fixed last round without checking that it ran. Verified by effect this time, not by reading the diff. The rest came through an analyse / refute / implement / verify pipeline, and the adversarial passes earned their keep: - The sidebar mini rail: icons sat left of centre because their slot kept its fixed 24px width while the rail collapsed, so the hit target was rectangular and off-axis. The rail narrows, rows are square, and the mini state gains the folder flyout and tooltips it never had. The reviewer caught a comment claiming the width change was a one-token revert when it is two — corrected rather than left to mislead the next reader. - Playback context now travels from every producer, not the handful that happened to stamp it. - Collapsible per-tab search in Library, and label artwork. - Every View-all destination Home offers, plus the label page and its releases. - Collapsing the sidebar drops a Large bar back to New, live, without overwriting the user's persisted preference. 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 |
||
|
|
960715c4ac |
feat(qbz-qt): skeleton placeholders for the heavy views
Library and Home showed grey squares that filled in unevenly, which reads as broken rather than as loading. One shared QbzSkeleton — a card shape and a row shape — now stands in while data is on its way, and each placeholder clears as its own item arrives rather than all at once, so loading reads as progressive. The animation is the part that could have undone the point of this track, so it is capped: a bounded number of placeholders breathe, and the timer stops when the view is not visible or the window is minimized. Not on focus loss — a tiling desktop keeps windows visible and unfocused, which is the rule already established for the ambient field. 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 |
||
|
|
e0fcf7a089 |
fix(qbz-qt): local context menus, collapsing search fields, toolbar density
Four things the owner hit in Local Library. The context menu did nothing there — local rows and cards now mount the shared menu the rest of the app uses, with the action set Slint gives local and Plex items. Anything with no seam in this port (add-to-playlist, add-to-mixtape, the tag editor, local favourites) is hidden or visibly disabled rather than a row that silently does nothing. The per-view search fields did not open and collapse the way the header's does. That behaviour moved into the shared QbzLineEdit, so the Local toolbar, the queue and the tree rail all get it from one place instead of four copies. The queue's field also escaped its 300px column; it is width-bound now. Filter, sorting and grouping become icon-only buttons with an explanatory tooltip, which is what the owner asked for and what the crowding called for. Found on the way, and affecting Qobuz cards too: the album card's ⋯ menu had no "Play later" at all, and its "Add to queue" row dispatched play-later — the row lied about what it did. Both fixed, with Slint's icon set for the three. cc |
||
|
|
31b1484c01 |
feat(qbz-qt): Local Library complete — five tabs, routed album page, Plex
The view had roughly a folder tree and an album grid inside one 1,847-line file. Slint's is 2,671 lines across five tabs, and the local album detail is a routed page of its own. This closes that gap and splits the result: the view becomes a composition root over qml/views/local/ modules, and local_library_qt.rs becomes a narrow facade over local_* modules by concern. Plex is wired for real. The port hardcoded the cache path to None, so the union was local-only and Plex thumbnails were skipped. The settings store binds per user at session activation, the album and track union includes Plex, thumbnails resolve through the artwork path, rows carry their source badge, and the manual re-sync is on the toolbar behind the availability gate — as in Slint. Tabs: Albums with its quality/format/source filter popup, Tracks paginated, Folders in both flat and two-pane tree modes, Artists as an A-Z master/detail. Every list is windowed from the start, artwork id-keyed and evicted, because 16k tracks is exactly where the Slint build freezes. The tree grows a real select mode with its bulk bar, the ad-hoc "ephemeral folder" pane plays music that was never indexed, and the album page gets its version picker — a local album with two physical copies now switches between them without a database round-trip. Two agents built the QML and the Rust halves in parallel and the QML called thirty QbzLocal members that did not exist. QML resolves lazily, so each would have been a control that renders and does nothing; a static cross-reference caught them before a build ever ran, and they are implemented here. Deliberately logged seams, not half-paths: add-to-playlist, add-to-mixtape and the tag editor have no picker or modal in this port yet, and local favourites are not wired, so those actions log rather than route a local id into the Qobuz API. Plex playback downloads the part instead of range-streaming it, which is Slint's own documented fallback. 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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |