Ports the Tauri qconnect/transport.rs config layer to Slint: resolve_transport_config
(env overrides -> qws/createToken auto-discovery -> WsTransportConfig with require_jwt
+ 60s idle-retry + default channels) and fetch_qconnect_transport_credentials (POST
/qws/createToken with X-App-Id / X-User-Auth-Token), plus the device-identity helpers
(uuid via shared qbz_app::qconnect_identity, hostname, friendly name, persisted
device-name in qconnect_settings.db) and the hex-channel parsers.
Frontend difference handled: the Qobuz client is reached via runtime.core().client()
(Arc<RwLock<Option<QobuzClient>>>, may be None before API init) and is gated/ok_or'd,
unlike Tauri's always-present AppState.client. Uses qbz_qobuz::endpoints::build_url
(same base) instead of the Tauri-local one.
Deps added: qbz-qobuz (build_url), qconnect-transport-ws (WsTransportConfig /
NativeWsTransport). allow(dead_code) until the service wires it. cargo check
-p qbz-slint green (only the pre-existing unrelated rename_folder warning).
cc
Phase S steps 9-10. Makes qbz-Slint a QConnect renderer: SlintRendererEngine
implements qconnect_app::QconnectRendererEngine over runtime.core(), so the Slint
frontend inherits the shared echo/cursor/materialize/shuffle orchestration in
qconnect_app::renderer instead of re-deriving it.
- Deps added to qbz-slint: qconnect-app, explicit qbz-player, futures-util, uuid.
- 16 trait forwards through runtime.core() (CoreError mapped to String) +
current_output_format reading player().state.
- start_track_stream + the HTTP feeder (probe FLAC -> play_streaming_dynamic,
fallback download -> play_data) ported from the Tauri CoreBridge impl. The
protected bit-perfect seams are only called, never modified; the probe-derived
sample_rate/channels/bit_depth flow straight through.
- TLS deviation: the crates-workspace reqwest ships rustls-tls (not native-tls),
so .use_native_tls() is dropped. TLS is transport-only; audio bytes identical.
allow(dead_code) until the Slint QConnect service + event sink wire it (next
Phase S step). Verified: cargo check -p qbz-slint green (only a pre-existing
unrelated rename_folder dead-code warning).
cc
- offline.rs: constructs + owns a per-user OfflineCacheState (same index.db +
library.db paths as Tauri, so the offline store is shared across frontends);
activated on login/restore, torn down on logout (hooks in auth.rs).
- playback.rs: play_audible + the gapless trigger now route through QbzCore's
play_track_resolved / fetch_for_gapless_resolved, so an offline-cached track
resolves from disk (decrypt -> play_data) before hitting the network. The
unlock-animation sink is None for now (padlock lands with the Slice 2 UI).
qbz-slint compiles clean. Offline PLAYBACK now works in Slint; the "Make
offline" trigger UI + per-row status + the padlock are next (Slice 2).
cc
Album-card hover overlay now shows the full release date as "MMM D, YYYY"
(e.g. "Nov 6, 2025") instead of the year alone. New dates::release_label
helper keeps the month/day/year order fixed but localizes the month via
chrono format_localized (the app ships 5 languages; current_locale() is the
single hook to thread the real UI language once Slint wires translations —
English for now). Applied to the Discover and Favorites album mappings.
Also fixes Favorites > Albums cards, which hardcoded an empty genre and a
year-only label: they now read album.genre and the full date, matching the
Discover cards 1:1. The data was already in the getUserFavorites response
(qbz-models Album parses genre + release_date_original) — no backend change.
cc
Wires qbz-library into the Slint app (new library_db module) so the
local, Tauri-shared organization data is read/written from the same
per-user library.db (<data_dir>/qbz/users/<id>/library.db). The DB is
opened per-operation on a blocking thread (rusqlite Connection is
!Send).
- Header collage: the playlist cover is now a 4-up collage of the
first tracks' covers (Tauri parity); falls back to the server
image, then the first track, then a placeholder.
- Custom artwork: owner-only buttons on the cover set/clear a custom
image. Set copies the picked file into <cache_dir>/qbz/artwork and
stores the path in playlist_settings.custom_artwork_path (shared
with Tauri); it's loaded directly from disk and overrides the
collage.
- Edit description: the edit modal gains a description field
(prefilled), saved through update_playlist alongside the name.
cc
Connects the previously-inert track context-menu items now that the
backends exist:
- Add to library -> add_favorite("track").
- Go to album / Go to artist -> resolve the track (get_track) to its
album / performer and navigate there.
- Create radio -> Qobuz track radio (new get_radio_track core wrapper
over the existing qbz-qobuz endpoint + playback::play_track_radio).
- Share Qobuz link -> https://www.qobuz.com/track/{id} to clipboard
(new arboard dep + share module).
- Share Song.link -> Odesli (api.song.link) pageUrl resolution to
clipboard.
- Add to playlist -> a global picker modal (PlaylistPickerModal +
PlaylistPickerState/Actions + playlist_picker controller) listing
the user's playlists; picking one calls add_tracks_to_playlist.
Add to mixtape / Make available offline / Track info stay unwired
(no backend yet).
cc
Two unrelated things landing together.
Sticky scroll fix:
- The natural-top binding on both jump-nav and the sidebar reads
from `body-row.absolute-position.y` (a live, scroll-aware
lookup). The y formula was `max(natural-top + viewport-y, ...)`
— adding viewport-y on top of an already scroll-aware value
double-counted the scroll. During mid-scroll the bar drifted
ahead of the body row, opening a visible gap and briefly
overlapping the header.
- Drops `+ viewport-y` from both formulas. natural-top alone IS
the viewport-relative y the elements want to occupy; the
max() clamp just keeps them from going above the top.
- Initial position and sticky position are unchanged; only the
transition is fixed.
Artwork right-click menu (artist portrait + album cover):
- Both headers get a TouchArea on the artwork that opens a
PopupWindow + ContextMenu on right-click, positioned at the
cursor. Mirrors Tauri's `oncontextmenu` flow on the artist
portrait and album cover.
- Menu items match Tauri 1:1: Add image / cover, Change + Remove
(when a custom image is registered), divider, Open in
browser, Save as.
- New `crate::custom_artwork` JSON-backed store at
`<data-dir>/qbz/custom_artwork.json` keyed by artist name +
album id (mirror of the Tauri library's
`v2_library_set/remove_custom_*` persistence).
- ArtistState / AlbumState expose `artwork-url` (network URL for
Open / Save) + `has-custom-image` / `has-custom-cover` (drives
the Add vs Change/Remove menu rendering), both populated from
Rust at apply_artist / apply_album time.
- ArtworkActions global + main.rs handlers:
* open-in-browser → `open::that(url)`.
* save-as → `rfd::AsyncFileDialog::save_file()` + reqwest
download + tokio::fs::write.
* add-custom → `rfd::AsyncFileDialog::pick_file()` then
`custom_artwork::set_*` + flip the has-custom flag.
* remove-custom → `custom_artwork::remove_*` + reset flag.
- rfd added with xdg-portal + tokio features. external-link and
image-plus SVGs ported from Lucide.
Custom-image RENDERING (using the picked file in place of the
network artwork) is deferred — the store persists the choice and
the menu reflects it, but the displayed image still comes from
Qobuz. Next pass.
cc
Ports the minimum of src-tauri/src/reco_store needed to honour the
"skip artists I already listen to" gate in the discovery pipeline.
play_history (`crates/qbz-slint/src/play_history.rs`):
- Tiny SQLite store at <data-dir>/qbz/play_history.db with WAL
(ADR-002), holding play_events (artist_id + occurred_at, one row
per audible track start) and artist_names (id -> name upsert).
Lazy open, errors collapsed to log::warn so a missing DB just
means an empty filter — same default Tauri lands on a fresh
profile.
- record_play(artist_id, name) wired into playback::record_recent
so every audible track start increments the per-artist count.
- known_artists(threshold) returns the (HashSet<u64>,
HashSet<String>) pair Tauri's filter expects — qobuz ids for
suppressing validated matches, normalized names for suppressing
MB candidates upstream.
qbz-core::musicbrainz_discover_artists:
- New `known_artists` closure parameter, called once per
invocation. Threshold matches Tauri (>2 plays).
- Filter applied at both stages: MB candidates dropped when their
normalized name is in known_names; Qobuz matches dropped when
their id is in known_ids (mirrors local_known_qobuz_ids check
in v2_get_discovery_artists).
The Tauri reco_store has many other features (favorites, top
genres, home seeds) the Slint shell does not use yet — those stay
out of this commit; only the play-count surface needed by the
sidebar's "You may also like" filter is ported.
cc
Wires the artist controller to call QbzCore's MusicBrainz orchestration
and feed the Origin section of the network sidebar, plus typed click
callbacks for every section (logged-only until the target views land).
Controller (`crates/qbz-slint/src/artist.rs`):
- reset_network_sidebar() — clears sidebar MB state on artist change.
- load_mb_metadata(runtime, name) — checks the client's enabled flag,
resolves name -> mbid via musicbrainz_resolve_artist, fetches
metadata via musicbrainz_get_artist_metadata, maps into MbOrigin
(is_person derived from artist_type, dates formatted as "1976",
"May 1976" or "May 14, 1976" matching Tauri's en-US output).
- apply_mb_metadata() / apply_mb_unavailable() — push into
NetworkSidebarState on the event loop.
main.rs:
- navigate_artist now resets the sidebar before loading and spawns
the MB enrichment in parallel with artwork. Origin loading flag
flips to true before the resolve, back to false on success or
on MB-unavailable.
- NetworkSidebarActions click callbacks (location, label, artist,
musician, discovery dismiss) wired to log() with full payload —
placeholder until ArtistsByLocation/LabelReleases/MusicianPage land
in Slint.
ArtistPageView.slint:
- Sidebar body becomes a Flickable + VerticalLayout (so the upcoming
Labels / Similar / Relationships / Discovery sections can overflow
cleanly).
- Origin section renders three optional rows — Born/Founded,
Born in/Founded in, Died/Disbanded — with the same uppercased
label + right-aligned value pattern Tauri uses. The location row
is clickable when the location precision is finer than country
or a city is present; click emits NetworkSidebarActions.location-
clicked with the artist's mbid.
- Section is gated by NetworkSidebarState.mb-available, so a
disabled MB integration or an unmatched artist hides Origin
entirely (next phases fill Labels/Similar with Qobuz data that
does not depend on MB).
Adds qbz-integrations as a direct dep of qbz-slint so the controller
can match on ArtistType / LocationPrecision without going through a
re-export shim.
cc
Moves off Skia back to femtovg and bumps Slint to 1.16: that release
carries the femtovg text-rendering fixes (slint-ui/slint #5177 pixel-
grid alignment and #11335 0.5px tearing / inconsistent glyph spacing).
Skia never received that treatment and rendered text slightly worse.
cc
femtovg's text rendering looks coarse next to native Qt/KDE. The
slint dependency now uses default-features = false with renderer-skia
(plus backend-winit, renderer-software fallback, accessibility,
compat-1-2). Pulls skia-safe — first build is slower and the binary
grows ~6 MB, both expected and accepted.
cc
Close the Recently Played Tracks tech debt: a JSON-backed play-history
store at the shared QBZ data path, surfaced as a slim-card section on
Discover Home (it hides while empty).
- recently.rs: load() reads the store newest-first; record() prepends a
track, dedupes by id and caps the list — the playback session calls
record() when a track starts
- home.rs threads a `recent` list into HomeData; HomeView renders a
"Recently Played" GridSection; artwork pipeline gains an
ArtworkTarget::Recent
The data path is complete end to end; only record() awaits the
playback session.
cc
Introduce the Settings page, reachable from the Discover gear and the
header menu. Two sub-sections backed by the existing UI-agnostic stores.
- Audio panel: backend, output device and sample-rate dropdowns;
toggles for exclusive mode, DAC passthrough, PipeWire force
bit-perfect, gapless, normalization and quality fallback
- Playback panel: autoplay mode, show context icon, remember session
and resume position
- settings.rs controller owns AudioSettingsStore + PlaybackPreferencesStore
at the shared global path; persists changes and applies audio ones to
the live Player (reload_settings, plus reinit_device for routing-
critical changes); a backend switch re-enumerates output devices
- QbzToggle (switch) and QbzSelect (PopupWindow dropdown) primitives,
SettingRow layout helper, chevron-down icon
- SettingsState global, ContentView::settings, settings-bool /
settings-select callbacks wired through the shell
cc
The MVP no longer requires re-authenticating on every launch. On
startup it loads the saved OAuth token via qbz-credentials, validates
it against Qobuz, and — if valid — activates the session and goes
straight to the Discover/Home view. A rejected token is cleared and
the login screen is shown instead. Successful sign-ins now persist the
token to the shared encrypted store.
- auth.rs: save_oauth_token after login, restore_saved_session at
startup.
- main.rs: enter_shell helper shared by the login and restore paths.
cc
Cover art now goes through the QBZ image cache instead of being
downloaded directly every time, matching the Tauri behavior.
- Moves ImageCacheService / ImageCacheStats into the qbz-cache crate
(LRU disk cache, MD5-keyed, SQLite index). src-tauri/image_cache.rs
re-exports it and keeps the Tauri managed-state wrapper, so the Tauri
app stays behavior-identical and both shells share one implementation
and one on-disk cache (~/.cache/qbz/images).
- qbz-slint artwork.rs checks the cache first, downloads and stores on
a miss, and trims the cache to 200MB on startup.
cc
Corrections from testing feedback:
- Remove the greeting — the current QBZ discovery view has no greeting,
only the tab selector. Drops HomeState.greeting and the chrono dep.
- Quality badge is now the icon-only badge (hi-res.svg, or cd.svg in a
framed square), not a numeric text pill. AlbumCardItem carries a
quality tier instead of a quality string.
- Tighten the album card: deterministic 274px height, title and artist
in a 2px-spaced stack — removes the large gap between them.
- SectionRow no longer scrolls vertically (vertical-scrollbar-policy
always-off, height pinned to the card height).
- Artwork is downscaled to 264px on decode instead of holding ~600px
source textures.
cc
Discover cards now show real Qobuz cover art. Artwork is fetched and
decoded off the UI thread, capped at 16 concurrent downloads, and each
result is applied to its own card row on the Slint event loop — so a
cover arriving never resets a section.
- artwork.rs: download + decode worker and per-row apply.
- AlbumCardItem gains an `artwork` image field; AlbumCard overlays the
cover, falling back to the elevated placeholder until it resolves.
- main.rs: after the discover sections render, artwork jobs are spawned
for every card with an image URL.
qbz-slint now depends on reqwest and image.
cc
After sign-in the app loads the real Qobuz discover index and renders
the Home view: a time-based greeting with the user's name, the tab
pill (Home / Editor's Picks / For You), and discover sections — New
Releases, Press Accolades, Ideal Discography, Qobuzissimes, Albums of
the Week — each a horizontally scrolling row of album cards.
- state.slint: AlbumCardItem / DiscoverSection structs, HomeState global.
- discover/: AlbumCard (artwork area, award ribbon, title, artist,
quality badge), SectionRow, HomeView.
- home.rs: discover-index fetch and mapping (award-ribbon priority,
quality string), greeting, and event-loop model application. Unit
tests for ribbon, quality, and greeting logic.
- AppShell content region is now the HomeView.
Album artwork images load in the next increment (artwork pipeline);
cards currently show the elevated placeholder. No fixtures — all data
is the live discover index.
cc
Wires real authentication. "Sign in via Browser" now runs the actual
Qobuz OAuth flow with no Tauri and no WebView: opens the system
browser, captures the authorization code on a one-shot localhost
listener, exchanges it via QbzCore's Qobuz client, and activates a
real per-user session through AppRuntime.
- adapter.rs: SlintAdapter implements FrontendAdapter.
- auth.rs: system-browser OAuth, mirrors v2_start_system_browser_oauth
without Tauri. Includes a query-param parse test.
- main.rs: tokio runtime; sign-in / offline callbacks run real async
work and navigate to the shell on success.
- Sidebar: placeholder playlist fixtures removed.
qbz-slint now depends on qbz-app, qbz-core, and qbz-models.
cc
M1 progress. Adds the foundation token layer (palette, semantic
colors, spacing, radius, typography) bound to the QBZ dark theme,
three primitives (QbzCheckbox, QbzPrimaryButton, QbzTextLink), and
the LoginScreen feature component.
The login screen mirrors the Svelte LoginView: centered card, logo,
QBZ wordmark, Qobuz ToS checkbox, "Sign in via Browser" button,
system-browser and offline links, and the legal disclaimer. Colors
are lifted verbatim from src/app.css; strings use @tr() markers.
Rust side: typed AppCommand enum; UI callbacks dispatch through it.
M2 wires the command handler to AppRuntime.
cc
First increment of M1 of the Slint functional POC. Adds the qbz-slint
workspace member: Slint 1.x dependency, build.rs compiling ui/app.slint,
a minimal AppWindow, and the binary entry point.
Verifies the Slint toolchain builds in this environment before the
foundation layer (tokens, shell, theme, i18n, typed commands) lands.
Private slint-poc branch only (ADR-007).
cc