5e4d7ca033aa6d16793cb1ce4156fa7cdc43ee2c
47
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
5e4d7ca033 |
fix(queue): preserve metadata and polish extended view
cx |
||
|
|
2273db9184 |
feat(qt): the disc surfaces — rip wizard, metadata correction, and a pane that
can be read RIP WIZARD, three steps, each answering something the first version got wrong by not asking. What will happen — which disc, how many tracks, and that the output is FLAC, stated rather than implied. The tracks, editable, because a CD-DA carries no titles at all so "named wrong" is the ordinary case and once files are on disk it is a rename job. And what happens afterwards: a ripped album the library cannot see is a folder full of files, so the destination is compared against the registered folders — by path COMPONENTS, not by string prefix, since /music-backup starts with /music and is not inside it — and the right follow-up is offered, naming the folder it would re-scan. "Do nothing" is an explicit choice rather than the silent default. The follow-up runs only on success. Tracks can be deselected. Everything is ticked on arrival and the user goes from all to fewer; a wizard that starts empty makes the common case the one that costs the most clicks. On the Rust side an absent `selected` means yes, so an older form cannot silently rip nothing. A PROGRESS PANEL behind the ripping chip, which is a door rather than a label: it is the only thing on screen during a rip and the two questions it could not answer are the ones that matter. A soft lock says not to eject — soft because nothing here can hold a tray shut, so the honest move is to say it and keep saying it. Per-track state with a bar on the row that is actually moving. Stopping is offered, deletes nothing, and says so; a cancelled job does not jump to 100 %, which is the one lie a progress bar must not tell. CORRECT DISC DETAILS, for a CD and a SACD image and for neither on a folder, which has no identity to write a correction under. The list shows what distinguishes four pressings of one album rather than four identical titles, and flags a track count that disagrees with the disc. Selecting is not applying: the candidate is fetched in full and its track list shown, because that list is the only thing proving a plausible title is the right pressing. Applying renames the live session without renumbering ids — a re-open would orphan a playing track — and writes a user row. Only MusicBrainz re-resolves the cover; Discogs ids are not Cover Art Archive keys and a wrong cover is worse than the old one. The correction can be dropped, because without an escape a bad apply is permanent and that makes people afraid of the button. THE PANE SPLITS BY DISC. A box set scans as one album — measured on a 34-track set whose album_group_key is the box root for every file while the disc number sits in the data unused — so the pane was a flat list 247 rows long. The key carries the disc, unconditionally, because it round-trips through the play-album lookup. Its own client-side filter over what is already rendered, since finding one track meant scrolling past the other 246. EPHEMERAL TRACKS LOSE WHAT CANNOT OUTLIVE THEM. queue_qt has published isEphemeral on every row for a while and the queue panel respected it; the bar never read it. The heart goes inert, the add menu keeps only its queue actions, and the row menu comes back with those three and nothing else. Absent rather than rendered-and-inert. Also: a spinner on Open that blocks a second click onto a drive already being read, held by a counting RAII guard because opening is a relay and the chip must not blink off in the seam; the now-playing card routes a disc to the pane it came from instead of to an album id no catalogue can open; the file pickers use rfd, the platform's own dialog, which the POC-NOTE said was a five-line change once the dependency existed — it existed, Settings had been calling it from this crate for weeks, and on macOS the four Linux choosers do not exist so Open folder and Open SACD image opened nothing at all, in silence; the empty state stands down for the Open pane, which on a machine with no library drew both fully painted; tooltips on every disc control through one hover() helper, since two call sites had already mismatched their key, which does not fail — the bubble simply never goes away. cc |
||
|
|
a922bdd316 |
feat(qt): disc covers, a corner close, and an icon for Rip
Three things the owner asked for after driving the first build. COVER ART. A SACD image carries none, but the file it was downloaded as almost always sits beside one — `cover.jpg` next to the `.iso`, or one level up when the rip is split into "Disc 1"/"Disc 2" folders, which is the shape of the owner's Rheingold. `find_folder_artwork` already knows that walk, so this reuses it rather than growing a second rule about where covers live. CLOSE moves to the pane's top-right corner, for all three media. It used to sit in the action row beside Shuffle — a destructive action one pixel from "play", which is where a hand goes without looking. It is also OUTSIDE the Flickable now: scrolling a 24-track disc must not carry the only way to close the session off the top of the screen. THE RIP ICON is a disc with a folder, drawn as a Lucide-style outline rather than traced from the owner's PNG. The argument is the size it renders at: at 15px a solid, detailed glyph turns to mush while an outline still reads, and the comparison was rendered at both sizes before choosing. Adding it also found a gap in my own tooling. `icon_tint_qt.rs` re-tints from MASTERS at theme-change time, and `masters_cover_every_shipped_glyph` failed the build for a glyph that was baked but had no master — six files written, one line forgotten. `bake_icon.py` now registers the master itself, because a step that has to be remembered is a step that eventually is not. cc |
||
|
|
3cfce8a769 |
feat(qt): the media-server settings panel, driven and verified in the window
ONE component instantiated twice, because it is one form twice. Every difference between the protocols is a property set at the call site, so they are visible in one place instead of drifting across two files: "Test" proves the address for Jellyfin and only "a Subsonic server answered" for Subsonic (which has no credential-free endpoint), the password is discarded for Jellyfin and kept for Subsonic (no session, md5(password+salt) per request), and the sync cost is stated up front — 45.8 s per 4924 tracks against 0.81 s per 6678 — so a long sweep does not read as a hang. The credential never leaves Rust; the panel gets `hasCredential`. Assets: the colour marks for cards, the monochrome ones for rows. Both mono files arrived as BLACK ink and are invisible on a dark theme untinted, so they were normalised to the #ffffff master ink and joined `icon_tint_qt::MASTERS` — the port's only shader-free way to recolour an SVG per theme. The Jellyfin colour asset had a white rounded-rect plate behind the mark; it was stripped, because every other brand mark here is transparent and a white tile beside them reads as a rendering bug. Rendered with Qt itself and looked at before any of it was wired. CAVEAT, recorded in SourceIcon: the Subsonic mark is NAVIDROME's logo, and `subsonic` covers Gonic, Airsonic, Astiga and Ampache too. `ping` reports the server's own type and it is already persisted, so branching later is cheap — a known wrongness, not an unnoticed one. THREE BUGS THAT ONLY THE RUNNING WINDOW COULD SHOW, all found by driving it under Xvfb rather than by reading the log: 1. `MediaServerSettings is not a type` — a .qml missing from build.rs is not in the QML module. It resolves fine on disk, compiles nothing, and blanks the WHOLE settings page at runtime. All three existing audits passed. `qml_module_registration_audit.py` is the missing check and is now in the gate. 2. The toggle would not move. The panel reads its state from the settings DOCUMENT, unlike Plex's toggle which reads a bridge property, so every mutation has to republish it — connect, enable, disconnect and the end of a sweep now do. 3. Every Jellyfin album in the grid drew the LOCAL HARD-DRIVE badge: `local_rows::badge_source` had never been taught the two words and folded them into `_ => local`. Not cosmetic — that string is what the source chips filter on. `play_single_from_source` also grew the arm it needed: a media-server row from the recently-played rail has no library.db row to mint a LocalTrack from, so it resolves through `registry.tracks_of` instead. END TO END, from the UI, against the real server: toggle on -> type the address -> Connect -> "jellyfin sync: 500/4924 ... 4924 saved, 0 pruned, 4924 cached" -> Local Library shows 461 albums with their covers and the Jellyfin badge. The cached quality distribution is identical to what the server reported in the original protocol sweep. 428/428. cc |
||
|
|
931cb06d38 |
feat(qt): ease the visualizers on the pulse, and make Plasma react
TWO FINDINGS, both of which falsified something that was being assumed.
FLUIDITY IS NOT FRAME RATE. Driven at QBZ_PULSE_MS=10 the shell presented ~100
frames/s — qt-batches confirmed it, and the script now prints the pulse it ran
with so two logs cannot be confused — and the bars looked exactly as they had at
30. They would: every published frame was applied as a STEP, so the extra
presents repainted the same discrete values three times over. What the Slint
build does differently is not rate, it is `animate bar-h { 90ms }` — it moves
BETWEEN values.
So the interpolation is back, ON THE PULSE, and it costs nothing. The old
interpolator was expensive because of its private 16 ms Timer, not because it
interpolated: it presented at its own rate, unsynchronised with everything else.
Easing on the pulse presents exactly as often as stepping on the pulse did — one
repaint per period either way, only the value differs. Self-parking survives: a
converged ease writes nothing and schedules no repaint. The price is lag, and it
is the honest one — k = 0.45 is ~83% there in three pulses (~100 ms), matching
the reference's 90 ms, against 46 ms of FFT window and 77 ms of Rust smoothing
already in the chain.
The from/to/progress triple is gone with it, and its consumers moved to `cur`.
Note for the next reader: `SpectrumBand` drove all three of its modes from
`Connections` on those properties, and a Connections handler naming a property
that no longer exists is NOT a compile error — QML warns at runtime and simply
never fires. That is how the whole band went dead at once; grep for
`on<Name>Changed` when removing a property, not only for direct reads.
PLASMA WAS DRIVEN BY THE SLOWEST SIGNALS IN THE PIPELINE while the fast ones
travelled beside it unconnected. `transientAmp`, `bandsLo` and `bandsHi` were
published, parsed and stored, and never handed to the item — the shader declared
them and received zeros — so everything ran off the energy bands, which decay
x0.85 per frame (~473 ms) against the bars' x0.65 (~178 ms).
Worse, `beat` stops being a beat on dense material: it decays x0.88 per tick, so
above ~3 hits/s it never falls far enough to come back as a spike and settles
into a shallow ripple around 0.7. A splat multiplied by it becomes a steady
glow, and the busier the music the flatter the picture — the most frenetic input
produced the least motion. `beatAc` subtracts a slow density floor, so a hit
reads as a hit at any tempo.
The scene itself was rebuilt around the reference the owner filmed (Butterchurn
on a percussion-heavy track). Its recipe is three parts: one thin annulus
injected per frame that the feedback warp marches inward, so the concentric
rings ARE the last N frames of audio laid out in space; retention high enough
for that to stack (this held 0.85, where a trail died in ~15 frames and nothing
ever accumulated); and angular structure from reading the spectrum BY ANGLE.
But a CLOSED ring was wrong, and the owner named why: a perfect inner
circumference reads as a drawn shape, hard-edges the field, and at this
retention successive rings pack into one solid disc — which is what killed the
ambient feel. The annulus is gated by angle now, so a hit paints arcs the
conveyor carries inward, the circle never closes, and the gaps stay open for the
background. Noise bends, frays and thins those arcs so they are made of plasma
rather than of compass.
The worms are back at full strength with tighter cores and a contrast curve — a
raw gaussian is all shoulder and reads as haze — and their orbits now reach past
the arc band instead of being trapped inside it. Colour comes from ONE
album-derived triad, so everything used to be a shade of the same hue; a
Rodrigues hue turn splits it into worms, arcs and an ambient wash that sit apart
whatever album is playing. That wash is what stops the background being void for
the foreground to be a cut-out against. A soft limiter replaces the hard clamp:
at 0.945 retention a clamp pins the middle to white and the structure vanishes.
Release build clean, offscreen smoke green, 421/421.
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 |
||
|
|
e6c058c2f2 |
feat(qt): immersive shader scenes — RHI items, scene menu, GL link pairing
Port the immersive GPU scenes: Plasma, Tunnel, Aurora, Ambient, Spectral Ribbon and Line Bed as QQuickRhiItems/ShaderEffects under a pulse-driven ShaderSceneLayer, plus the Qt-only Tunnel Flow (Tauri Canvas2D port) fed by the Viz16 stream. Scene rows live in the header view menu (no Off row); g cycles the ring. A fresh immersive open defaults to Split > Lyrics (owner ruling). OpenGL link fix (owner smoke: Tunnel black with "uniform _22 declared as type buf and type buf"): every ShaderEffect fragment with its own uniform layout now ships a paired vertex stage declaring the identical block — the shared spectrum.vert only ever linked under Vulkan. RHI hardening: render pass descriptors on the ping-pong targets (the Tunnel Flow segfault / Plasma dead window), mirrorVertically tied to the backend (!== OpenGL), ribbon sub-uploads carry setSourceSize. km |
||
|
|
8bd02e35d3 |
feat(qt): custom-theme editor, artist portrait, playlist rail, About/What's New and diagnostics
Five surfaces the Qt frontend was missing, landed together because they share
five wiring files (build.rs, main.rs, shell_bridge.rs, AppShell.qml,
HeaderBar.qml) and splitting them would produce commits that do not build.
ARTIST PAGE
- The Playlists section used a hand-rolled Rectangle delegate: a MouseArea with
no onClicked, and a RoundedImage with no fit:, so the default crop blew an
800x380 playlist banner up inside a 200x200 box and showed the middle 47% at
2.1x. Replaced by the real cards/PlaylistCard.qml — already live on five other
surfaces — mounted in a new views/PlaylistRail.qml modelled on SectionRail.
ArtistPlaylist publishes the five roles the card reads.
- The portrait gains what the album cover has: the right-click menu the Slint
reference already has (Add/Change/Remove image, Open in browser, Save as…) and
the lightbox this port invented for albums and playlists. Custom images are
keyed by artist NAME into the same custom_artwork.json the Slint build writes,
so they round-trip between the two.
- CoverLightbox is now aspect-aware. It forced a square box on the premise that
covers are square; artist portraits are not (1080x720 measured in the cache).
- Removing an override invalidates the pipeline's resolved path for that url:
artwork_qt::cached_path consults the override FIRST, so coverMap held the
custom file and the header kept showing the picture the user had just deleted.
- artwork_qt::local_path is the inverse of file_url. trim_start_matches("file://")
is not: it leaves the percent-escapes in place, so any path holding % # or ?
came back as a name that does not exist on disk.
- One new msgid, "View image", in the seven non-en locales — matching its sibling
"View cover", since en carries none of that family and falls back to the msgid.
CUSTOM THEME
- The editor, the colour picker and the swatch grid, ported 1:1 from
AppearanceSettings.slint and ColorPicker.slint. The 11 editable base tokens,
their expansion into the full 55-value palette and the on-disk format all stay
in the shared qbz-theme crate, so a theme authored in the shipping Slint build
already rendered here and now round-trips both ways.
- Hue is normalised 0..1 at every Qt.hsva boundary; the reference stores 0..360,
and porting the number instead of the domain would render every swatch red.
- The first-selection seed is captured BEFORE set_theme persists the slug,
because current_slug() re-reads it from disk — a seed taken afterwards would
resolve "custom", find no file and snapshot the default it was meant to replace.
- The in-memory base is dropped when the theme row enters "custom": it is
memoized for the life of the process and filled at bridge construction, so a
process that booted without the file would keep serving the OLED default even
after the shipping Slint build wrote the user's theme.
- The disk write is debounced to drag-end and atomic; the reference persists on
every drag frame with a truncating write, on a file the other build has open.
ABOUT + WHAT'S NEW
- Both modals, with their producers. render_markdown is transcribed line for line
from the reference: h1 and h2 collapse to one level, an indent-0 bullet becomes
a section plus a TOC entry, bold and backticks are stripped rather than styled.
Text.MarkdownText is a different subset and would have rendered a different
document from the same source. Pinned by tests, including the real 2.0.2 body.
- The codename is a single const, so the release checklist has one Qt-side place
to bump.
DIAGNOSTICS
- The panel in Settings > Developer, the Report an Issue modal and its menu row.
- The log bundle carried eight hand-built header fields where the reference
carries about seventy, so every issue filed from a Qt build arrived with no
kernel, no distro, no install method, no GPU, no exclusive-mode / DAC /
sample-rate state and no playback context. The report is injected INTO
qbz_log::bundle rather than replacing it, keeping the <details> wrapper that
makes a pasted bundle readable.
- The GTK / WebKit / GSK / GDK rows are CUT. In a Qt process those libraries are
not mapped, so their runtime column can only read "—", and their saved column
would come from a graphics.json this binary never reads or writes — foreign
state presented as this app's configuration.
SHELL
- The four always-on-top overlays (toast, tooltip, art preview, immersive) relied
on declaration order and carried no z, so any z-bearing modal buried them: a
toast raised by a background event behind an opaque panel is never seen. They
now sit in a band above the modals, keeping their relative order.
- build.rs asked cargo to watch ../../.git/HEAD, which does not exist in a git
worktree (.git is a file). Cargo treats a missing rerun-if-changed target as
always-dirty, so the whole build script — per-shader qsb, the recursive asset
sweep, cxx-qt/moc codegen — re-ran on every cargo invocation.
Verified: cargo test -p qbz-qt 343 passed, cargo test -p qbz-library 83 passed,
qml_singleton_xref and qml_resolution_audit clean over 35 bridges and 265 QML
files, and qmlcachegen compiles every one of them. Smoke-tested by hand.
Contracts, recon and adversarial reviews:
qbz-nix-docs/qt-frontend/2026-08-14-six-item-round/
cc
|
||
|
|
b045f7023b |
chore(qt): vendor the country flag set used by the artist scene
265 SVG flags (1.1 MB) plus the upstream MIT licence and attribution in crates/qbz-qt/third-party/. Kept as its own commit so the asset drop can be rolled back or re-sourced without touching the scene code that consumes it. cc |
||
|
|
f46c5c95d4 |
perf(qt): one repaint pulse for the whole shell (GPU 95% -> 25%)
Qt Quick has no partial redraws: any dirty item presents the WHOLE window, and on the owner's hybrid laptop each full-window present costs ~1.2% GPU flat (KWin composite + dGPU scanout; area-independent, while the app's own render is 0.08 ms/frame). The bill was presents/s, and the shell ran unsynchronised ~30 Hz clocks plus two leaks: - QbzShell.pulseMs: ONE Rust-driven 33 ms edge that every continuous animator ticks on (knob QBZ_PULSE_MS). VizSettle now stashes on publish and applies on the pulse (its 16 ms interpolation timer is gone); AmbientField and ImmersiveAtmosphere ride the same edge. - VizSettle writes NOTHING while not live: the always-mounted but invisible immersive panels were dirtying 28 bar rects per FFT publish (named via QT_LOGGING_RULES=qt.quick.dirty=true). - AmbientField renders inline again: ShaderEffectSource scheduleUpdate() double-presents (minimal repro fbo_repro.qml in the investigation folder). - Background mode 2 (ImmersiveAtmosphere) composites its four drifting layers + base + dim + scrim in ONE opaque ShaderEffect (atmosphere.frag, baked by build.rs); numeric parity vs the Image stack verified (atmo-shader-parity-check.py, delta 0.0). The four-Image stack stays as the software/failure fallback. Measured on the owner's machine: 93-97% -> 25-26% GPU, ~34 W -> 12-13 W, ~33 presents/s in both background modes. Full story: qbz-nix-docs/qt-frontend/2026-08-11-scenegraph-batches/GPU-COST-INVESTIGATION.md km |
||
|
|
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 |
||
|
|
21e941bdf6 |
feat(qt): renderer tier, GPU selection and a real shader build
Closes PARITY-DEBT #83 and #104, and unblocks the immersive shader scenes. THE TIER. Slint resolves one bool, use_gpu_renderer, and hands it to three features: the shader scenes, the dynamic background and half of reduce-motion. This port had none of it, which is why the scenes could not be offered — off the GPU tier they render black and nothing could tell. QbzShell now carries rendererApi, gpuTier, shaderScenesAvailable and appBackgroundAvailable, all written from one latch. Only QML can source it: QRhi picks the backend after QGuiApplication, so the pre-init code knows only what it asked for. Per-item GraphicsInfo.api stays where it is. "Can this item draw?" and "should this feature be offered?" are different questions, and routing the first through Rust would make every cover wait on a round trip. SHADERS ARE BUILT NOW. build.rs bakes every .frag/.vert to .qsb with ten variants and the batching rewrite. They used to be baked by hand and committed carrying no GLES variants at all, so any GLES context failed them with "No GLSL shader code found" — which is exactly how the NPB Large visualiser went blank while this branch was being tested. Adding a scene is now dropping a .frag in the folder. GPU SELECTION enumerates real devices through Vulkan and lists them by model, never by class: a machine with no discrete GPU can no longer be told to use one. Legacy class keys still resolve, but only against hardware that exists. Picking a non-default device implies the Vulkan backend, which the row's own description states in all eight locales — measured as the only route that keeps a working context: DRI_PRIME lands on llvmpipe and the NVIDIA PRIME pair lands on OpenGL ES. The renderer row gains the auto-revert sentinel and the frame-liveness watchdog it shipped without. The sentinel arms only for a choice that truly forces a backend, and liveness means frames rendered over a window, not merely a window that opened. Also fixes the kiosk toggle overwriting the tier half of reduce-motion. Verified by 312 tests, both QML audits, the offscreen smoke, and a probe against the real hybrid GPU: both devices enumerate by name and the stored preference applies. cc |
||
|
|
8ba9483462 |
feat(qt): HiFi Wizard — guided DAC setup ported from Slint
Six-step modal (Welcome / Check / DACs / Configure / Test / Done) opened from Settings > Audio. It probes the audio stack and renders per-distro remediations, enumerates DACs with a manual node.name escape hatch, generates the PipeWire / WirePlumber snippets to copy, and plays four tracks spanning 16/44.1 to 24/192 with a live requested-vs-negotiated read-back. Read-only: it never writes a system file or runs a command. The logic was unreachable from Qt — every public function in qbz-dac-wizard takes a &AppWindow and writes straight into Slint globals. It is lifted into a new frontend-agnostic crate, qbz-dac-wizard-core, which returns plain data. qbz-dac-wizard itself is left untouched. Rust owns everything it computes plus `open`, so the reset and the probe are one act; QML owns the step navigation and the four progress checkboxes, which is how the reference splits them too. Two deliberate divergences from the reference: the row sits at the end of OUTPUT rather than BIT-PERFECT, and it is offered on every backend instead of PipeWire only — someone on ALSA direct is exactly who needs the walkthrough. The green glyphs use the accent tint, not success: success has no qrc bake directory, so a degraded tint set would render nothing at all. Every success-toned text keeps theme.success (#3fae6a verbatim). Verified by 309 tests, both QML audits and the offscreen smoke, plus a forced-route probe that mounts Settings and walks the wizard's steps — the standard smoke only mounts Home, so it is no evidence for a modal. The modal was also driven and screenshotted over VNC. cc |
||
|
|
af95ce164a |
fix(qt): search top-result track hero no longer invades the Artists rail + crown icon
Two Most-popular regressions on the search results page: - When the top result is a TRACK, the hero painted from its slot's center ~100px into the Artists carousel: SearchTrackHero's root Rectangle had no size, and a bare Item's implicit size is 0x0 (it does not size from children), so the slot's horizontalCenter anchor pushed the whole card sideways. The artist/album arms carry explicit sizes and never shifted. The hero now declares its 200px width explicitly. - The header glyph was "award", which reads as a mic. The reference uses the crown (SearchResultsView.slint:584); crown.svg is ported into the icon masters (primary + the warning bake) and the header uses it. Both verified live on a VNC-driven session with the exact "all things" search from the report. km |
||
|
|
b46924bcc0 |
feat(qt): IconTextButton control and external-link icon
Shared button-with-icon control (used by the queue panel, Plex settings, the log viewer and the library folder editor) plus the external-link icon in every theme tint. cc |
||
|
|
6b17db90dd |
feat(qt): the mini footer, the hover capsule and always-on-top
Block B3. The owner's verdict on the four blocks that landed before it was that the miniplayer was "no draggeable, no hay controles de ningun tipo. Ni de reproducción, ni para cambiar entre modos, no es pinneable." All of that lived here. qml/miniplayer/ — MiniFooter with its three modes, and the five primitives the reference declares as inner components: MiniSeek, MiniTransport, MiniVolume, TBtn and CapBtn, one file each because a 396-line Slint footer folded into one QML file lands well past the size rule. MiniWindowControls is the hover capsule: five surface tabs, a separator, then expand, pin, backdrop, drag and close, plus the collapsed square trigger. The transport in full and compact is centred by a PHANTOM SPACER the width of the volume button, not by a centred layout — micro has no volume and is the one mode genuinely centred. The seek carries the reference's pending machine: the -1.0 sentinel, the 0.02 convergence window and the 8s watchdog. The capsule is a conditional MOUNT, so leaving the card destroys its expanded flag and its 250ms timer with it. ALWAYS-ON-TOP is new work, not a port: three comments in the Slint tree promise it and no code implements it. The owner asked for it back because Tauri had it and Slint structurally cannot. Qt.WindowStaysOnTopHint, a pin button, and a persisted pref that defaults ON. COLOUR LITERALS: six were carried over from the reference verbatim and every one of them was WRONG. Slint writes #RRGGBBAA; Qt parses #AARRGGBB, so #ffffff33 is not white at 20% but opaque pale yellow — which is exactly how the capsule rendered until it was driven and looked at. This is the second time this trap has landed in this port. Each converted literal now carries the reference form beside it. One of the six is MiniShell's, from B2. mini_qt.rs — surface 0 leaves the temporary clamp, because the micro footer IS the card and now exists. Surfaces 3 and 4 stay clamped until B4. Driven and seen, not inferred: the footer in artwork and micro, the capsule revealing on hover and collapsing on leave, the micro card at 380x57 with its centred transport, and the pin persisting mini_always_on_top across a toggle. The drag handle is wired to startSystemMove on pressed; the VNC platform cannot move a window, so that one is owed a real compositor. cc |
||
|
|
221f8704d5 |
feat(qt): kiosk profile — resolution, boot and the live toggle
Port of contract section 8. Three Slint blocks, faithfully:
- main.rs:7855-7876 kiosk_profile_active() / resolved_shell_screen()
- main.rs:8586-8618 kiosk entry (forced reduce-motion; fullscreen ONLY via
QBZ_KIOSK_FULLSCREEN, windowed otherwise -- incident 2026-07-11: a user who
toggles kiosk on the desktop and restarts would be trapped, since the kiosk
shell has no titlebar control and neither Esc nor F11 leave fullscreen)
- main.rs:11767-11780 the live toggle, in the reference's order: persist,
flip the flag, then swap the screen. The flag first means the shell that
mounts next reads a correct kioskProfile on its FIRST frame.
"kiosk" is a SIBLING screen of "shell", not a mode inside it, which is how
app.slint:38 models it (AppScreen { splash, login, shell, kiosk }, sibling
mounts at :145 and :205). Both shells read the same bridges, so the toggle
tears nothing down and playback keeps running.
The toggle reads the CURRENT profile rather than re-resolving: resolving
would let a QBZ_PROFILE env override pin the reading and a user on a kiosk
image could never toggle out (the reference says so at :11764-11765).
ViewModeMenu's delegate called QbzImmersive.openFromMenu() unconditionally,
so flipping any row's `live` would have made it open Immersive. Each row now
carries its own action. Miniplayer stays the one inert row.
The small bar's view button becomes the toggle itself in kiosk
(PlayerBarSmall.slint:746-750): every other flyout row is gated
!kiosk-profile there, so the menu would carry a single row.
reduce_motion's kiosk half is now live -- shell_bridge.rs had the seam
pre-built with a `false` literal and a note saying one line, on their side.
Also: the gear icon. NavRail tile 7 uses settings.svg; this port only had
settings-2.svg, which is two sliders, a different glyph.
288 tests (4 new), 9 warnings = baseline, both QML audits OK.
cc
|
||
|
|
5130e872e7 |
feat(qt): hotkeys dispatcher, focus lifecycle, multi-select seam (B2)
km |
||
|
|
9ce7ef4ca0 |
perf(qt): draw the spectrum in one ShaderEffect per mode (macOS F3)
The 28/48/5 Rectangle delegates cost one dirty node + batch churn each per frame on Qt's Metal path (QRhiMetal::setShaderResources per batch is where the ~6x vs Linux lives). Each mode's Loader now instantiates ONE ShaderEffect — one node, one draw call: heights go to scalar uniforms (b0..bN) pushed in one JS pass per settle tick, bar/gap/ gradient geometry is computed in the fragment shader with the same colors and 1px rounding as the Rectangle version. The pre-existing Repeater delegates are kept verbatim as the fallback arm (software renderer or shader-compile error — the renderer preference added in #104 makes that arm user-reachable). Runtime findings on Qt 6.11 (documented in the file header): inline GLSL strings are dead (fragmentShader is a URL property), so shaders are baked with qsb --glsl 440 --msl 12 and shipped under qml/assets/shaders/ (auto-qrc); and the built-in default vertex stage emits no qt_TexCoord0, so one explicit shared vertex shader is baked alongside. Verified: build + 204/204 tests + both QML audits; RFB (fallback arm, all three modes live during playback); Xvfb/GL (shader arm, all three modes, zero shader errors; QSG_VISUALIZE=batches shows one uniform batch block for the strip). km |
||
|
|
c8559a68b3 |
feat(qt): port Qobuz Connect — service, flyout, routing and touchpoints
Full-parity port of the Slint QConnect integration per the contract at qbz-nix-docs/qt-frontend/2026-08-01-qconnect-port/00-CONTRACT.md (three adversarial review rounds on the spec, §12 trap sign-off PASS on both halves, RFB-verified incl. a real cloud connect, live session owner-tested). The shared crates are consumed verbatim — zero backend changes. - adapter quartet: qconnect_transport_qt (credentials, identity on the shared qconnect_settings.db), qconnect_engine_qt (start_track_stream fallback chain + feeder helpers + CMAF cursor sync), qconnect_event_sink_qt, qconnect_qt (the facade: connect/disconnect recipe, controller routing, echo guards, offline parking B12, startup auto-connect) - QbzQConnect bridge singleton + on_session_entered wiring; one shared flyout for both bars (tooltip on the large bar only), Developer > Connect diagnostics modal, device-row glyphs, settings rows live - poll loop: peer-reflection branch before cast, ~2 s renderer reports, sync-on-add on every Qobuz-castable add path - routing: cast → QConnect → local (mute QConnect-first), play arms after the set_queue funnel, reorder cloud arm with no local fallback, volume/mute/seek remote-first - touchpoints: lyrics remote anchor, scrobble guarded / Discord unguarded, cast suspend/resume, volume-lock lifted when remote km |
||
|
|
ac9740b83a |
assets(qt): add the glyphs and brand marks the playlist block needs
63 tinted icon variants and 3 brand marks, split out of the feature commit so the code diff stays readable. The icons cover the Playlist Manager (grip-vertical for the grid reorder handle, chart-no-axes-column for the play-count badge, folder-plus, library-big, refresh-cw), the folder editor's preset picker, and the queue's circle-stop marker. Each ships in every tint directory the baker consumes; a glyph present in only some of them renders nothing in the others, because icon_tint_qt fingerprints the icon SET and a partial set bakes a partial cache. The brand marks are the importer's allowed-source row. cc |
||
|
|
d1862e90da |
fix(qt): the Source column draws the real Qobuz and Plex marks
Owner: "si es de qobuz, icono de qobuz, si es de local (folder o plex) cada uno tiene sus svg; Slint y Tauri los tenían bien." They did. The port did not, and the reason was an asset gap rather than a styling choice: `qobuz-logo-filled.svg` and `plex-logo.svg` were never carried over from the Slint tree, so `SourceIcon` substituted `cloud-download` for every Qobuz kind and a tinted `hard-drive` for Plex — drawing "downloaded" and "local file" where the design calls for two brand marks. Both assets now live in `qml/assets/brand/`, beside the Discogs / Last.fm / MusicBrainz marks that are there for exactly the same reason. The rule this restores is one `SourceGlyph.slint:4-8` calls load-bearing: the full-colour logos render UNTINTED and only the monochrome local glyph is tinted, to MUTED text and never accent. Slint spells out why — routing a logo through a tinting primitive "would turn the logos into a flat accent silhouette, the bug this primitive replaces". `QbzIcon` floods every pixel with its tint, so the two logo arms deliberately do not go through it. `qobuz_purchase` is the one arm that must tint a multi-colour asset (the same mark in #eab308 — the colour is what says "purchased"), so it is the one arm that uses an effect, and it degrades on a software renderer to the untinted mark rather than to nothing. The two ROW call sites now pass SourceGlyph's own numbers (15px, muted) instead of inheriting the version picker's (14px, secondary) — the two Slint components this file stands in for disagree on both, which is why they are properties. Verified by driving the app: the Qobuz rows show the Qobuz mark and the Plex rows the gold Plex chevron, in their own colours, with zero "Cannot open" in the log. 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
|
||
|
|
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 |
||
|
|
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 |
||
|
|
75ea846768 |
fix(qbz-qt): the app menu carries actions again, not settings
Intelligent search, Ambient background and Use system title bar were sitting in the hamburger menu. They are settings, not app-menu actions, and all three already have their real rows in Settings > Appearance — which is where Slint keeps them — so the menu was duplicating them in the wrong place. Documentation is added; it is the one Slint row that needed no surface the port lacks, just the external-link invokable QbzShell now has. Still missing against Slint, each waiting on a surface that does not exist here yet: Open Music Link, Keyboard Shortcuts, Report an Issue, What's New, About QBZ. They are named in the file header rather than shipped as rows that do nothing. build.rs now watches qml/ for changes. Cargo reruns a build script only when a source file changes, so dropping the new icon into qml/assets/ produced a binary whose qrc did not contain it — the file was on disk and the app logged "Cannot open" at runtime. That was going to bite every future asset. 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 |
||
|
|
e8a821dd9e |
feat(qbz-qt): the quality stamp, ported from Slint per bar mode
The port had one hand-made badge taken from the archived Tauri tree. Slint has three distinct components and each bar mounts a specific one, so the wrong tree had cost the stamp its LEDs entirely — they exist only in Slint, derived from the output backend and mode, and no amount of polishing the Svelte port would have produced them. QualityBadge becomes Slint's icon-only mark with its hover tooltip, for cards and rows. QualityBadgeFull is the icon plus stacked tier and detail. AudioStamp is the inline two-row form that carries the LEDs: backend blue when engaged, mode green, purple for a cast session, amber with the renderer name for a peer Qobuz Connect. New and Classic mount the SongCard stamp column, Large and Small the inline stamp, matching the .slint mount sites. The downgrade case follows Slint too: row one reports what is actually being delivered with an amber arrow, and the catalog maximum moves into the tooltip next to the reason it was limited. PlayerBar's LEDs stop reading the settings document, which never knew whether a backend was engaged, and read the now-playing output state instead — where Slint reads them. cc |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |