750c59034b04cfb5010fffdc033e245b776f7c93
11
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
750c59034b |
feat(qt): Excel-style multi-select — Shift ranges across every surface
Selection existed but had no keyboard: every click was a single toggle,
so picking 40 tracks meant 40 clicks. Slint and Tauri have had ranges
since 2026-06; this is that port.
NINE surfaces, not the five the track views suggested: Album, Artist,
Playlist, Label, Local Album, plus Library's tracks and albums and Local
Library's tracks and albums. Each carried its own hand-copied `selected`
map and its own `toggleSelected`, so the rule went into
controls/SelectionModel.qml and the maps stayed where they were — the
smallest change that stops it being copied a tenth time.
The rules are Tauri's `applyShiftRange`, unchanged: a plain click
toggles one row without clearing the rest, Shift fills the range from
the anchor ADDITIVELY (so re-dragging a range cannot eat the set) and
leaves the anchor where it is, and Ctrl/Cmd is the same as a plain click
— not a shortcut, but the behaviour both references already have,
because an accumulating click is what "add without disturbing" means.
Qt makes this CHEAPER than Slint, and the difference is worth recording:
Slint had no modifiers at click time and had to read a thread-local set
from a ModifiersChanged handler, with a documented stale-read edge case
when the window lost focus mid-chord. A QML MouseArea delivers
`mouse.modifiers` on the event itself, so the modifier and the click can
never disagree and no such seam exists here.
Three judgement calls, stated because they are not forced:
- Library gets TWO models, one per kind. Its rows are mixed, and one
anchor would let a Shift-click on a track sweep up the albums drawn
between it and the anchor.
- Ranges run over the VISIBLE rows. Local Library's search box is a
view filter, and a range over rows the user cannot see is not the
range they asked for — the same call `select-all` already made.
- The Local Library tree rail keeps plain toggles. Its selection lives
in Rust (QbzLocal.treeToggle*Select), so there is no QML map to hang
an anchor off; teaching the Rust side about an anchor is its own
change, and the file says so where a reader would expect otherwise.
The anchor is an ID, not an index, because every one of these views can
re-sort or filter under a live selection — an index anchor silently
points at a different track the moment the order changes. Slint's
selection.rs made the same call for the same reason.
Verified by driving the app: in Abbey Road, click row 1 then Shift-click
row 5 selects exactly rows 1-5 and leaves 6-8 untouched, with the bar
reading "5 selected"; the same in a playlist, additively over a row that
was already selected.
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 |
||
|
|
313a7e7e4e |
fix(qt): the local album page gets its zebra, across the whole row
The shared TrackRow has had a `zebra` flag all along, off by default, and the Qobuz album page turns it on. The local one never did — and turning it on at the call site would not have been enough either, because LocalTrackRow wraps the shared row without forwarding the flag. Forwarding it is still not the fix. TrackRow tints its own background rectangle, which is `parent.width` wide, and this wrapper hands the shared row `root.width - 26 - 46`: a 26px gutter for the source glyph and 46px for the context menu the wrapper draws itself. A stripe painted from in there stops 72px short and leaves both controls outside it — which is what the smoke caught. The stripe is therefore painted by the wrapper, full width, declared before the shared row so it lands underneath and takes no pointer events. Same colour literal and same parity rule as rows/TrackRow.qml, so a local list and a Qobuz list stripe identically. cc |
||
|
|
47a7641fc2 |
fix(qt): bound the queue panels, and make the gate catch what it missed
Follow-up to
|
||
|
|
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 |
||
|
|
35508b5f87 |
fix(qt): the defects found smoking the playlist and My QBZ round
Un-following a Qobuz playlist left it in the sidebar and in My Library. toggle_follow called unsubscribe_playlist and settled the ownership snapshot and stopped there, so the row only left at the next full refetch. Both surfaces are patched directly now, in both directions: following from a card buys the metadata with one get_playlist rather than re-reading getUserPlaylists, which lags the write. The un-follow collapses a hearted-and-followed playlist to the one row the next load will actually produce instead of dropping it, because the local heart is a library.db flag that survives the unsubscribe — removing every row made it vanish and come back, which is the original complaint wearing the other shoe. The Plex mark on album cards was a hard-drive glyph tinted accent — a blue hard drive — and the three Qobuz kinds drew no badge at all. The badge now ports discover/AlbumCard.slint:326-354 exactly: 24x24 at radius 4 pinned bottom-right with a 6px inset, plex-logo untinted at 16, qobuz-logo at 18 (22 for a download), the purchase arm gold on its amber chip, hard-drive white at 14 for local only. The brand marks are plain Images: routing them through QbzIcon floods every pixel with the tint and flattens a logo to a silhouette. SourceIcon moves to controls/ since it is no longer local-only, and every other surface drawing a hard-drive was audited against its .slint. The local album detail loses the two nav chevrons above the art. They are a Tauri-era leftover from before the app had global navigation. My QBZ used a bespoke card in the grid. MyQbzDetailCard is deleted and the standard cards/AlbumCard is mounted with its overlay. localMode bundled four separate things — action routing, the heart, the pin and Block this album — so a multi-source container had to switch all four off together; it is split, and the catalog affordances now follow the ITEM. On a Qobuz cell the heart works through the shared favourite seam; on a track, playlist, local or Plex cell it is absent rather than dead. Row covers carry two rungs: the list keeps _50, the grid gets _600. Not the _230 that would merely cover a 200px card — the complaint was that the cover reads softer than a normal AlbumCard, and a normal AlbumCard is fed image.large, which IS _600. _200 is not a rung the CDN serves at all (it answers 404). The My QBZ grid sets reuseItems, and AlbumCard assigns to its own isFavorite and isPinned when toggled, which destroys the binding that fed them. A recycled delegate therefore showed the previous row's heart and the click added the new album while the glyph said remove. Restored with the Qt.binding idiom this port already uses on TrackCard, PlaylistCard and TrackRow. The details view no longer force-opens every row on entry, and the per-row open state persists. Library > Artists showed placeholders for nearly every portrait. The side panel never reported its artwork window — the one caller of libraryArtworkWindow is LibraryView.reportWindow, and the panel had no report call at all, so no key ever reached the resolver and artMap stayed empty for its rows. The rail now reports through that same path, with the section-header sentinel reportWindow already skips. Note the reference has no portrait fallback either: an artist Qobuz serves no image for is a placeholder on both sides. cc |
||
|
|
a16cf56c86 |
feat(qt): playlist manager, folders, importer and the Library parity round
Ports the whole playlist-organisation domain, which had no counterpart in this frontend, plus the Library work the owner asked not to defer. Playlist Manager. New controller (playlist_manager_qt, _rows, _ops) over the already-ported folders_qt repository, which until now had zero consumers. Grid, list and folder-tree modes; sort, filter and search; folder cards and chips; drag reorder and up/down arrows. Reached from the sidebar's "..." menu, whose New folder / Manage playlists / Import rows had been rendering dimmed and inert behind a comment saying the seam did not exist. Folders. QbzFolderEdit plus the create/edit modals and PmFolderIcon: preset or custom-image icons, colour, hidden flag, delete. Deleting a folder nulls its LOCAL members' folder_id explicitly, because the app keeps the foreign_keys pragma off, so the schema's ON DELETE SET NULL never fires for them. QbzPlaylistEdit. One shared rename/description/delete editor replacing the inline popup in PlaylistView. Three corrections the old path had wrong: the description is carried through a rename instead of being written as NULL, a FOLLOWED playlist routes to unsubscribe (playlist/delete returns 200 and silently no-ops on a playlist you do not own), and the post-delete back navigation is conditional on actually being on that playlist's page, so deleting from the manager no longer navigates you off it. Importer. Spotify / Apple Music / Tidal / Deezer, over the shared qbz-playlist-import crate. Picker local mode. The reference's four-case matrix — target local or Qobuz, payload local refs or Qobuz ids. Local playlists list unconditionally and Qobuz ones only when online. Local refs onto a Qobuz playlist write the library.db sidecar at next_playlist_sidecar_position(pid, qobuz_count); the 0-based enumerate the reference used before collided slots and lost rows. The carried payload can no longer be parsed as a catalog id when it is a library row id. Unblocks the local bulk add-to-playlist. Library. A-Z jump strip, Albums list mode, Artists side panel, per-tab persistence and multi-select. Preferences land in the same <data-dir>/qbz/favorites_ui.json the shipping build writes, so switching between the two frontends keeps them. LibraryView is split into views/library/ on the way past 1800 lines. Local library. Group-by now persists; playback queues the order on screen rather than the SQL page order; switching album-identity mode invalidates the Artists tab; artist matching folds diacritics and compares normalized credit parts, so "Air" stops listing "Airbourne" and "Beyonce" stops being two artists. Offline. activate_offline never persisted a last-user marker, so every per-user path resolved to None and all local reads came back empty; a session that started offline saw no local playlists at all. The sidebar's load-once latch was function-local and therefore unreachable from the logout reset, so a second login never rebuilt the tree. Both are session-scoped now, and the sidebar Refresh row runs the local-safe verb instead of being dimmed with no way to recover. Also fixes, found while verifying rather than reported: artwork derivatives were written straight to their final path, so a QML Image resolving one mid-encode opened a truncated png and Qt cached that failure against the URL for the rest of the session; Library's track group separators referenced a Component declared in another delegate's scope and could never render; the genre filter was drawn on Tracks and Albums but applied only on All; an All-feed row called a player method that does not exist; and the sidebar's drop-hot highlight used Slint's #RRGGBBAA literals in Qt's #AARRGGBB slot. 17 new msgids across all 8 locales. 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 |
||
|
|
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 |
||
|
|
e0fcf7a089 |
fix(qbz-qt): local context menus, collapsing search fields, toolbar density
Four things the owner hit in Local Library. The context menu did nothing there — local rows and cards now mount the shared menu the rest of the app uses, with the action set Slint gives local and Plex items. Anything with no seam in this port (add-to-playlist, add-to-mixtape, the tag editor, local favourites) is hidden or visibly disabled rather than a row that silently does nothing. The per-view search fields did not open and collapse the way the header's does. That behaviour moved into the shared QbzLineEdit, so the Local toolbar, the queue and the tree rail all get it from one place instead of four copies. The queue's field also escaped its 300px column; it is width-bound now. Filter, sorting and grouping become icon-only buttons with an explanatory tooltip, which is what the owner asked for and what the crowding called for. Found on the way, and affecting Qobuz cards too: the album card's ⋯ menu had no "Play later" at all, and its "Add to queue" row dispatched play-later — the row lied about what it did. Both fixed, with Slint's icon set for the three. cc |
||
|
|
31b1484c01 |
feat(qbz-qt): Local Library complete — five tabs, routed album page, Plex
The view had roughly a folder tree and an album grid inside one 1,847-line file. Slint's is 2,671 lines across five tabs, and the local album detail is a routed page of its own. This closes that gap and splits the result: the view becomes a composition root over qml/views/local/ modules, and local_library_qt.rs becomes a narrow facade over local_* modules by concern. Plex is wired for real. The port hardcoded the cache path to None, so the union was local-only and Plex thumbnails were skipped. The settings store binds per user at session activation, the album and track union includes Plex, thumbnails resolve through the artwork path, rows carry their source badge, and the manual re-sync is on the toolbar behind the availability gate — as in Slint. Tabs: Albums with its quality/format/source filter popup, Tracks paginated, Folders in both flat and two-pane tree modes, Artists as an A-Z master/detail. Every list is windowed from the start, artwork id-keyed and evicted, because 16k tracks is exactly where the Slint build freezes. The tree grows a real select mode with its bulk bar, the ad-hoc "ephemeral folder" pane plays music that was never indexed, and the album page gets its version picker — a local album with two physical copies now switches between them without a database round-trip. Two agents built the QML and the Rust halves in parallel and the QML called thirty QbzLocal members that did not exist. QML resolves lazily, so each would have been a control that renders and does nothing; a static cross-reference caught them before a build ever ran, and they are implemented here. Deliberately logged seams, not half-paths: add-to-playlist, add-to-mixtape and the tag editor have no picker or modal in this port yet, and local favourites are not wired, so those actions log rather than route a local id into the Qobuz API. Plex playback downloads the part instead of range-streaming it, which is Slint's own documented fallback. cc |