dc158bd9b691717d5aec1feed88cefb21424577a
18
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
b1dba198c9 |
feat(qt): kiosk profile release hardening
Integrate the Kiosk profile hardening round: 12 dedicated Kiosk views plus 22 shared opt-in hosts, shared navigation history with Back/Forward, Albums as the default library tab, native paged Local Library (artists/tracks), lightweight MyQBZ grid and detail, Ephemeral lifecycle, bounded lists and artwork sized to the physical frame, static skeletons, 64px transport bar, Settings touch reflow and a persistent Immersive exit. Desktop keeps its prior behavior: every shared component defaults to the desktop path and only opts into Kiosk behavior through explicit properties (origin-relative ScrollMemory, viewport-bounded artwork requests). Regression coverage: kiosk navigation and artwork Node tests added to the runtime gate, scroll-origin fixtures for fixed and grouped rows, i18n strings for all eight locales. Verified by 750 release tests, six QML audits, Kiosk smokes at 800x480 and 1280x800 and a Desktop A/B with seven pixel-identical captures. cc |
||
|
|
f9578d042c |
fix(navigation): include local library tabs in history
Record the outgoing tab state and scroll before switching, restore same-view history entries, and route sidebar tab requests through the same path without duplicating the initial landing. Add shared CI and local regressions for source filtering and tab transitions. cx |
||
|
|
d244213a64 |
fix(library): harden cross-source catalog data
Preserve per-disc and collection artwork, multi-value genres and remote metadata across Local, Plex, Jellyfin and Subsonic. Apply source, format and quality filters before pagination, reject invalid CUE rows, and keep Qt library surfaces aligned with the selected physical version. cx |
||
|
|
75a668ed12 |
feat(metadata): expand the local tag editor
Promote metadata editing to a routed full-page workflow with tabular per-track fields, ordered artist credits, provider identifiers, and compilation handling. Add bounded artwork selection and retrieval through MusicBrainz, Discogs, and Last.fm. Direct and sidecar saves preflight immutable rows, verify rich tag writes, cache covers by content, and update exact database ids transactionally. cx |
||
|
|
c7038a6858 |
feat(queue): add extended queue view
cx |
||
|
|
1c1ea00f6b |
fix(qt): break the activeTab binding loop, and time the ephemeral open
BINDING LOOP. The Binding's `when` read `viewLoader.item.activeTab` —
through `typeof`, which still registers a dependency — while the Binding
WRITES that same property. Every flyout navigation logged
QML Binding: Binding loop detected for property "when"
Pre-existing, not from the two-phase commit: reproduced on the
pre-2026-08-21 binary by navigating the same way, where it logs from the
same Binding at its own line number. The guard now lives in a handler on
the Loader's `item`, which runs once per mount and registers no
dependency. Verified by log: 0 "Binding loop", and 0 QML complaints of
any kind on a full navigation pass.
EPHEMERAL OPEN — INSTRUMENTED, NOT SOLVED. Two timers split the work
either side of the parallel read phase, because the remaining ~9 s has no
confirmed owner. The owner reports fooyin opening the SAME NAS folder
near-instantly, which rules out the network as the ceiling and makes this
our code — the obvious suspect is that we read each file's tags in
`extract` and then open and read it AGAIN in `extract_artwork`, plus
decode and re-encode a thumbnail, where a grid-only player needs none of
that up front. That is a design change, not a tweak, so it is measured
rather than guessed at.
The numbers arrive on the next real open ("[ephemeral][perf] parallel tag
reads" / "serial bookkeeping"). I could not trigger one from the test
harness: the entry point is a native folder picker, and the rehydrate
path did not fire from the surface the harness can reach.
cc
|
||
|
|
f50c1b5188 |
feat(settings): the library folder table becomes its own page
The folder block — toolbar, add-path row, filter, the row-per-folder
table and the scan progress — is the tallest thing in Settings and it
sat at the TOP of Local Library, pushing Albums view, Library > All,
Maintenance, Danger zone and the three media servers off the first
screen. Behind a Manage row, the whole panel fits one screen again.
The shape is the one Blacklist already uses for its manager, and this
view is deliberately its twin: same 64px summary row, same metrics, same
route-recorded-by-the-caller contract.
Nothing about the table changed. LibraryFolderTable.qml is mounted
verbatim, still driven by the settings document and still acting through
the settingsString action keys.
Two things had to travel with it, because SettingsView used to supply
them and a routed page has no access to that:
- LibFolderEditModal, or the per-folder pencil would open nothing,
silently.
- the refresh on entry. A view reached by Back runs no per-view load,
so it asks for its own, where BlacklistManagerView puts its reload.
Verified in a running window: the route mounts, the seven folders render
with their per-row actions, and Back returns to Local Library with the
section intact.
cc
|
||
|
|
5b274cab57 |
perf(qt): acknowledge a route change on the next presented frame
Measured by driving the real window and diffing the captured frames: a
click on Discover > Home left the window with ZERO pixels changing for
590 ms — the flyout did not even close. The click handler, the flyout
close, the sidebar highlight, the page blank and the whole mount all ran
inside ONE turn of the event loop, so the first frame that could be
presented was the one that already had the finished page in it.
The breakdown matters more than the total, because only one part of it
is what it looks like:
navigate_to (Rust, GUI thread) 7.6 ms
bridge queue hop 1 ms
the QML mount (`built`) 263 ms
post-mount work before a frame ~290 ms <-- `to-idle`
`built` is under half of it. `to-idle` is the number that matches the
freeze, and any future work here should be judged on that one.
So the route is now committed in two phases: arm on the change, build on
the next PRESENTED frame (frameSwapped). Frame N carries the cheap half
— flyout gone, highlight moved, page blanked — and frame N+1 pays for
the mount. Acknowledgement measured at 73-90 ms across five routes and
repeat runs, against 590 ms before.
Two things this had to get right, both caught by capturing frames:
- The backstop timer was a RACE at 32 ms. Render-at-next-vsync
(16.67 ms here, per QSG_INFO=1) plus a QUEUED frameSwapped delivery
lands in ~16-33 ms, so Home acknowledged at +90 ms while Library got
nothing at all on the same build. 250 ms puts the guard outside that
range; it now only covers the hidden-window case it was written for.
- During the armed frame `viewLoader.item` is still the OUTGOING view,
so the incoming route's discriminators were applied to it. The
activeTab Binding wrote "home" onto LibraryView — which has an
activeTab, so the typeof guard passed — emptying its body and
restarting the page fade over the blank. Both Bindings now require
the commit to have happened.
Not asynchronous: incubation is time-sliced and was reverted the day it
was tried; every view is still built synchronously.
cc
|
||
|
|
81e172457f |
fix(qt): migrate playlist_meta, break a router binding loop, zebra the playlist
RECENTLY PLAYED PLAYLISTS STAYED EMPTY THROUGH EIGHT TRACKS, a restart and a manual refresh — while the plays were sitting in the database the whole time. Read off the live file rather than reasoned about: `playlist_play_events` held 3, 9 and 10 rows for three playlists, so the context edge was working perfectly; `playlist_meta` was empty, and its `.schema` said why — no `covers`, no `own_image`. `covers` and `own_image` were added by inlining them into the CREATE. `CREATE TABLE IF NOT EXISTS` does NOTHING when the table already exists, so anyone who had run the previous build kept the six-column table, every upsert failed on "no such column: covers", and the rail JOINs meta against events — so it rendered its empty state with the evidence one table away. The failure was invisible by construction: swallowed into a warn, and the UI's empty state is also its correct state for a user who has played nothing. A brand-new table is only new ONCE. The moment a build runs anywhere its schema is legacy and every later column is a migration, so the pair moves to the same idempotent ADD COLUMN the album history already uses, and the upsert's failure becomes an error rather than a warn. Verified against a copy of the real database: the columns land and all 22 events survive. A regression test builds the exact pre-covers schema, records a play into it, migrates, and asserts the upsert works and the old event still counts — it reproduces the bug without the fix, and it also asserts a second `init_schema` is a no-op, since every launch calls it. The meta is written by `playlist_qt::load`, so the three playlists already in the events table surface as soon as each is opened once. BINDING LOOP IN THE ROUTER, and this one was self-inflicted: "QML Loader: Binding loop detected for property source". `_stampRoute` runs inside the `source` binding, and it read the last path from a `property string` and then wrote it — a binding depending on a property it mutates. QML detects the cycle, warns, and stops re-evaluating, which is a broken router and not merely a noisy log. The holder becomes an object whose MEMBER is mutated: no change signal, so the dependency is satisfied once and never fires again. (The second warning on the same file, the `when` loop on the nav-flyout `activeTab` Binding, is untouched by any of this work and predates it. Left alone deliberately — it is a warning on a working feature, and rewriting that Binding is its own change.) ZEBRA on the playlist detail's track rows, like the local album page. TrackRow already owns the stripe, so it is one property. Release build clean, offscreen smoke green, 421/421 and 216/216. cc |
||
|
|
f0d753a751 |
fix(qt): fade on tab switches, playlist recents artwork, per-rail sizes, wheel
Four defects from the smoke of
|
||
|
|
26ceeab828 |
feat(qt): mixed playlists, page fade, category filter, per-context recents
Six parity fronts against the Slint/Tauri references, plus the applied-filters
tooltip the app never had.
MIXED PLAYLISTS — the bug. A mixed playlist is a QOBUZ playlist carrying local
and Plex rows through library.db's playlist_local_tracks / playlist_plex_tracks
sidecar tables. This port could WRITE those rows (the picker) and COUNT them
(the Playlist Manager badge), but playlist_qt::load built its rows exclusively
from the Qobuz API response, so a track added to a playlist never appeared on
the page it was added to. Two comments elsewhere claimed the merge already
happened; they were describing the reference. Ported literally:
read_sidecar_rows_blocking (healing + bulk Plex resolve) and interleave_rows
(absolute-slot claim, same-slot collisions all emit), merged in load, with the
merged queue adopted as the open-detail snapshot.
Routing had to split with it. local_playlist_qt::open_id() was the one
predicate for every action, and a mixed detail adopts that same snapshot — so
it now means "a local:<uuid> detail OR a mixed one", which is right for
play/shuffle/row-play and wrong for the writes. Remove and reorder route on the
new local_detail_open() instead; without that, removing a local row from a
Qobuz playlist wrote into the local-playlist repo, found nothing, and reported
success. Removal by source is a third arm: the Qobuz arm parses the display id
as a u64 playlist_track_id, and a local row's display id is a library.db rowid
in the same numeric space. Reorder is refused on a mixed detail for the same
collision, in Rust and in the view.
PAGE FADE. 300 ms OpacityAnimator on the mounted page, OutCubic, honouring
reduceMotion. Zero added latency: the router's Loader is synchronous, the UI
thread is frozen through the mount and no frame exists in which a fade-OUT
could be shown, so the page snaps transparent in the same turn as the rebuild
and only the reveal is animated. An Animator and not a NumberAnimation because
it runs on the render thread — measured 0.53 vs 0.11 of progress with the GUI
thread blocked — and not on the shared pulse because the pulse arrives on the
very thread the mount blocks.
The first cut faded a veil coloured from the pane, and that was wrong in a way
that only shows in the configuration the app actually runs in: AppShell paints
the pane surface-main@0.22 while the ambient background is on, so a veil at
full opacity covered 22% of the page and the fade read as a flicker. Fading the
page has no such failure mode and keeps the ambient art visible.
SPLASH COPY. The login screen was already 1:1 (18 msgids, verified by diff).
The splash was missing the legal disclaimer the reference pins to the bottom of
the screen, drew "QBZ" / "QOBUZ PLAYER" as bare literals (English in all eight
locales), and hardcoded the dark palette — black on the other 34 themes.
CATEGORY FILTER on the Home playlists rail. Slugs now travel per card and the
offered set comes from the same /discover/index response, so the filter is
client-side over the cached 40 with no extra request. The selection lives in
Rust because HomeView is destroyed on every navigation; the reference keeps its
own for the same reason. New trigger + popup, and a `leading` slot on
QbzSectionHeader to hang it off the title line.
APPLIED-FILTERS TOOLTIP. A third QbzTooltip mode taking [{group, values}],
below the control with an upward caret. Overflow is staged summary and never
scroll — the overlay is enabled:false by construction, so a scrollable tooltip
could not be scrolled. Wired where a filter is INVISIBLE (collapsed to a count
or a glyph): the genre chip in four hosts, the Home tags, the Local Library and
Purchases funnels, the Artist Scene genres.
RECENTS BY CONTEXT. QueueTrack already carried context_kind and exactly one
function threw it away: record_queue_track wrote both album histories on every
track start whatever the user had put on, so one 40-track playlist filled a
24-entry album rail with that playlist's contents. album_play_events gains an
additive context_kind column (the other frontend names its columns on insert,
so it writes '' and '' counts as an album — no history reinterpreted), plus a
new playlist_play_history.db and a "Recently Played Playlists" rail, default on.
A separate file rather than a field in recently_played.json, which is a
destructive round trip between frontends: serde drops what it does not know and
the next write puts the truncated object back.
ITEMS PER CAROUSEL, recovered from Tauri and never ported to Slint. It lives in
a NEW TABLE of discover_prefs.db, not in the prefs blob: to_json rebuilds the
object from the keys the running binary knows and save overwrites the whole
column, so a new key would survive only until the other build's next toggle.
Default is uncapped, i.e. today's behaviour.
Two DiscoverySectionId arms were added to the frozen Slint tree, obliged by the
shared enum's exhaustive matches — same two-line follow MostPlayedAlbums took.
Not verifiable here: compiling that crate pulls qbz-ui.
Also fixes two recommendations_qt tests that had been red since
|
||
|
|
a4c29c796c |
perf(qt): build the tab that is showing, not all of them
Entering Discover froze the UI thread for 1.52s — not slow, frozen:
zero pixels changing anywhere in the window — where the Slint build
painted the same page, artwork included, in a single frame. Measured
frame by frame from a screen recording, with the click located at the
moment the nav flyout disappears.
`visible: false` hides an item; it does not stop QML from building it.
HomeView declared its four Discover tabs as sibling Columns gated only
on `visible:`, so every mount instantiated all four and 19 of its 28
section rails belonged to tabs nobody was looking at. Gating each body
on a Loader's `active:` cut a Home mount from 426-512ms to 182-190ms,
and the tab block itself from 364-444ms to 134-140ms.
Synchronous, deliberately: incubation is time-sliced, so `asynchronous`
spreads the same work over more frames instead of removing it.
Three more of the same class, found by auditing for it:
- SlimGrid's delegate Loader had no `active:`, so it built a full
card for the items past `total` and then hid them.
- LocalLibraryView mounted all four tab bodies, the Tracks one being
the 16K-row view.
- AlbumInfoModal built a credits row per track of the album even when
the Description tab was showing.
ContentRouter's header claimed the fix was to keep views alive across
navigation. It is not, and the references say so: Slint destroys its
view tree with `if` (AppShell.slint:426) and Tauri removes it from the
DOM (+page.svelte:6286), and Tauri even refetches from the network on
mount while still transitioning instantly. Neither gets its speed from
caching the view — both keep the mount proportional to one viewport.
The rule is the SIZE of a mount, not its lifetime. Paragraph rewritten
to say that.
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 |
||
|
|
7d98ad044f |
feat(purchases): the Qt frontend — list, album detail, routing and gating
Two screens, a controller and a bridge. `QbzPurchases` publishes one JSON document per screen plus a revision counter, so QML re-parses even when the text is byte-identical. - The list: two tabs with per-type counters, a server-side search on a 300 ms debounce, and a toolbar that DIFFERS by tab — Albums gets group, sort, direction, two filters and grid/list; Tracks gets group and the filters only. Building the Albums toolbar on both would have invented two controls. - The detail: a format dropdown whose selection re-scopes every downloaded mark, the progress section and Add-to-Library together; a determinate bar counted in TRACKS, never bytes; goodies as an album-level action counted separately. - The album download is the frontend loop, not the Rust command the reference registered and never called. Sequential, concurrency 1, cancellable, and a failed track does not abort the album. - Both prefs are read at runtime, not merely written. `show_purchases` is the master gate and `nav_tb_purchases` only relocates the entry to the title bar; both default false, so Purchases still ships hidden. - Scanned local rows keep their raw `qobuz_purchase` source so the gold badge survives in list mode as well as grid, while still classifying under the Offline source filter. Remote feed rows deliberately stay `qobuz`: stamping them would badge every purchase the user merely OWNS as locally downloaded. Defects fixed after the screens were written, none of which any test could have caught: - `QbzPurchases.boot()` was called nowhere. Without the Qt-thread hop every publish is a silent no-op, so both screens would have stayed permanently empty with nothing logged. The cause was an ownership gap rather than the code: `Main.qml` belonged to no lane because the plan listed the files each lane CREATES rather than the files it TOUCHES, and "belongs to nobody" is indistinguishable from "done" until something runs. - A second download of the same album nested inside the first. The store rewrites its destination to the album subfolder after the first successful track so that Add-to-Library adds the album rather than the whole music root, and the download planner read that value first — producing `.../Album [q]/Artist/Album [q]/...`, one level deeper each time. The picked ROOT and the resolved ALBUM FOLDER are now separate fields with names that cannot be confused, with a regression test. - A tab counter could stick at zero forever with its list plainly full: the two ids calls each swallow their error, and the reference's backfill from the list response was missing. - Cancelling made the whole progress block vanish instead of showing the cancelled state, whose msgid was already translated in all eight catalogs. - Add-to-Library re-walked the entire library when the folder id it needed was already in hand. - A live language switch and a logout left the previous state behind. Deliberate divergences from the contract, each recorded with its reason: the page header is a plain `fontSection` title with no icon tile, because nothing else in this application announces itself over a large coloured square; purchase download status travels inside the album document rather than on the shared cache channel, which is keyed by track id alone and would let the two features overwrite each other on the same row. Gate: release build clean, QML audits 278 files / 37 bridges, `cargo test -p qbz-qt` 398 passing, offscreen smoke green with zero QML complaints. Not verified, and cannot be from here: every populated screen. This account owns nothing, so only the empty states have ever rendered. cc |
||
|
|
016eb3cd23 |
feat(qt): artist scene, musician page and the shared scene-discovery engine
Both views were absent from the Qt frontend, not partial. This lands them
together with the core engine they need and the two entry points that reach
them.
- qbz-core gains scene discovery ("artists from the same place"): a
cancellable run with a phase for the progress label and a typed
SceneDiscoveryError, so a view can tell offline, all-upstreams-failed and
an honest empty result apart. CancelToken, ScenePhase and SceneDiscoveryError
are re-exported for frontends.
- qbz-integrations: MusicBrainz location resolution plus its cache layer.
- qbz-qt: ArtistSceneView with its three scene components, MusicianPageView
and MusicianModal, backed by artist_scene_qt.rs, musician_qt.rs and the two
bridges. The five-branch musician dispatcher is wired to all six of its
consumers. Both scene doors are live: the sidebar location link and the
overflow row that shipped grey with action: "stub".
- Translations: the new msgids in all eight locales.
Three defects fixed on the way, all with the same signature — a comment
asserting a mechanism that does not exist:
- Volume opened at 100 %. The comment claimed the poll loop mirrored it from
the engine; now_playing::set_volume has two callers and neither runs in a
local session.
- mbAvailable was documented as "MusicBrainz enabled AND resolved to an MBID"
while being assigned from the opt-in alone. Consumers now test the MBID.
- Session restore could not resume from a cold cursor ("cannot resume - no
audio data available") for want of a has_loaded_audio() guard. Investigation
in qbz-nix-docs/qt-frontend/2026-08-14-cold-cursor-resume.md.
Contract and evidence:
qbz-nix-docs/qt-frontend/2026-08-14-artist-scene-musician/00-CONTRACT.md,
which wins over specs 01..09 and records 20 deliberate divergences from the
reference in section 6.
cc
|
||
|
|
085c35876a |
revert(qt): make the content router synchronous again
An asynchronous Loader plus a router-level skeleton was meant to fix the dead click on navigation. It made the app markedly worse and is reverted whole. Async incubation is TIME-SLICED: it yields to the UI thread between slices, so it trades a short freeze for a much longer wall-clock build, and the Loader stays in Loading until the entire tree exists. For Discover > Home that is all 413 cards -- a one-second freeze became a five-second skeleton on the most used route in the app. Measured after the fact, which is the mistake; a change that moves WHEN something heavy is built has to be timed on the busiest route before it is called a fix. The placeholder is gone with it. It drew the same two text lines and row of five cards for all 40 routes, so the swap moved everything -- the exact jump a skeleton exists to prevent. The dead click is real and remains unfixed. The durable fix is not here: stop rebuilding these views on every navigation (keep the top-level tabs alive) and build the heavy views incrementally. The reasoning is kept in the file so the experiment is not re-run. cc |
||
|
|
98dfcd8173 |
fix(qt): nav flyout entries land on their own tab
Every flyout entry (Discover > For You, Library > Albums, Local > Folders...) navigated to the section but landed on its default tab: the port handed the tab to the mounted view through a depth-capped scene search (NavFlyout.findTabHost, cap 7), and the ContentRouter extraction (kiosk port D3) added a mount level the walk never reached — flagged as a latent desktop regression in the kiosk audit (2026-08-02 AUDIT-REPORTS.md, finding 11) and confirmed by smoke test. The tab now travels the way this codebase crosses documents: the bridge. NavFlyout.activate calls the new QbzShell.navigateToTab(view, tab), which publishes navTab/navTabView/navTabSeq, and a Binding in ContentRouter applies it to the mounted view's activeTab — the same pattern as the MyQBZ kind discriminator. The seq counter re-applies on a repeated click of the same entry, the navTabView guard keeps a cross-view request off the outgoing view, and a plain navigateTo clears the request so section navigation never drags a stale tab along. A click for the already-mounted view skips navigation (no duplicate history entry) and re-applies the tab live. Verified by smoke: cross-section, same-section, repeated-click and plain-navigation cases all land where Slint lands. km |
||
|
|
63e36c7543 |
feat(qt): the kiosk shell, its NavRail, the 4 card primitives and the 8 views
Contract sections 4, 5, 6 and the QML half of 7.
ContentRouter.qml is the D3 extraction: ONE view-mount chain, shared by both
shells. The Slint kiosk's content block is a verbatim copy of AppShell's and
KioskShell.slint:10-17 names this extraction as the follow-up it was waiting
for. 23 desktop arms carried over with their comments, 8 kiosk overrides, and
the kiosk-only nowplaying route. AppShell's four viewLoader.item sites now go
through contentRouter.currentItem; a tree-wide grep confirmed no other
reference (LESSONS L4 -- grep, do not trust memory about "both places").
KioskShell.qml carries the back bar with the full search focus lifecycle, the
content-height arithmetic including the now-playing exception, the offline
gate, the forced small transport, the player-zone focus ring, the root key
handler and the 30ms initial focus grab. Two findings worth keeping:
- the NavRail tiles fire header-menu-navigate("favorites-tracks") in the
reference, which switches the view AND selects a landing tab; navigateTo
carries only the route, so the tiles reuse the port's existing pending-tab
handshake rather than landing on the wrong tab.
- the visualizer tile's action is a sanctioned no-op (R2). Wiring
QbzImmersive would be worse than nothing: `open` would go true over an
empty mount and the key guard would then reject every key against a
surface that renders nothing.
Three fractional font sizes had to be rounded. Qt's font.pixelSize is an INT
property, so the reference's 11.5px (NavRail.slint:47) and 12.5px
(KioskNowPlaying.slint:59,67) are not expressible; font.pointSize is the only
real-valued alternative and is DPI-relative, which would make those three
labels scale differently from every other in the tree.
The NavRail one was caught by the offscreen smoke run IN THE KIOSK PROFILE --
it made the whole rail unavailable, i.e. a kiosk with no footer. The build,
both QML audits and qmllint were all green while that was true, and the
desktop smoke was clean because it never mounts KioskShell. The other two
were still latent: KioskNowPlaying is not on the smoke's route.
Gate: build 9 warnings = baseline, 291 tests, both audits OK over 229 QML
files (+15), qmllint filtered = the same 20 pre-existing ArtistView lines and
nothing else, kiosk-profile offscreen smoke clean.
cc
|