Follow-ups to #591/#592. The streaming initial-buffer wait blocked the audio thread up to 60s even when the play had been superseded by a newer track, which fed the stop/restart-loop feel; play_generation moved onto SharedState and threaded through PlayStreaming so a superseded wait exits in ~50ms (the 60s timeout stays for genuine slow networks). And the non-streaming Play path gained the same legacy-CPAL fallback the streaming path got, instead of dead-ending on backend-init failure (macOS keeps surfacing the error). Control flow only; sample path untouched.
cc
Fetching new Plex music required restarting QBZ. Add a sync button in the Local Library header (gated on plex-available, with a busy spinner) that triggers the existing Plex re-sync (new plex_auth::sync_now over the persisted selected libraries) and refreshes the view. Strings in all 8 locales. Resolves#573.
cc
Trackpad PixelDelta events arrive divided by the HiDPI scale factor, making trackpad scrolling far slower than other apps on scaled Wayland desktops. In FlickableData::scroll_delta, distinguish line-based wheel deltas (always exact multiples of 60) from pixel-precise trackpad deltas and apply a 2.75x factor to the latter only; wheel and fling scrolling are unchanged. Resolves#588.
cc
Two stacked defects broke playback under sandboxes (Snap/Flatpak/minimal .deb) in 2.0.1: PipeWireBackend::is_available() probed only `pactl info`, which those images don't ship, so PipeWire reported unavailable even though it was running (native pw-dump enumeration succeeded in the same log). And the streaming stream-init path had no legacy fallback (unlike init_device), so a failure dead-ended into a stuck spinner until the 45s watchdog. is_available() now checks the pipewire-0 socket + pw-dump (both time-bounded, fail-fast) with pactl kept as a last fallback; the StartStreaming path falls back to legacy CPAL on non-macOS. Sample path untouched (no gain/resample/rate change). Addresses #591/#592.
cc
The header sits on the always-dark art atmosphere, but its text used Theme.text-* which is dark on a light theme = unreadable. Route the header title/artist/meta/description through hdr-strong/hdr-body colors that go white over the gradient and fall back to the theme colors when the gradient is off; below-divider text is untouched. The atmosphere also faded out above the divider on description-bearing headers — raise its height to 440px and push the fade start to 74% so it reaches the header/track-list divider. Addresses #491/#519.
cc
The prior reorder reused only the DragGhost but re-implemented the drag gesture with its own grip + absolute-position math, which placed the ghost several px off the cursor. Remove the grip entirely and drive reorder off the SAME row-body drag that already adds a track to a sidebar playlist: it captures the source index on start, the shared DragGhost (positioned by the Rust DragActions path) follows the cursor exactly, the drop slot is read from DragState.pointer-y against the ListView's absolute-position (no row double-count), and releasing WITHIN the list reorders while releasing on a sidebar playlist still adds. Reverted the sidebar drop suppression + removed DragState.reorder.
cc
Follow-up to #590: playback now downgrades to the streaming-quality setting but the badge still showed the catalog max with no indicator. The poll loop already received the delivered stream's sample-rate/bit-depth in PlaybackEvent but dropped them; publish them plus a quality-downgraded flag (effective < catalog max, DSD-exempt on depth) and the true effective detail string. AudioStamp shows an amber down-arrow glyph when downgraded and a Source/Output hover tooltip; mirrored to the miniplayer. Strings translated in all 8 locales.
cc
The bespoke reorder ghost read a row's absolute-position inside a ListView, which double-counts the row's own offset, so it only sat at the cursor for row 0 and flew off for the rest. Drive the app-wide DragState directly from the reorder gesture so the shared DragGhost (positioned in AppShell at the proven window coords) renders the reorder ghost; a new DragState.reorder flag keeps the sidebar from claiming it as an add-to-playlist drop and keeps the Rust DragActions path out of it. Removed the buggy local ghost. Reorder logic + persistence unchanged. (state.slint also carries the quality-badge fields for the next commit.)
cc
The Settings streaming-quality dropdown persisted the pref but playback used a hardcoded UltraHiRes const, so the setting did nothing. Resolve the pref (via a new streaming_quality_for_key mapping to the Quality format ids) at playback time for the normal, prefetch and gapless paths. Resolves#590.
cc
QBZ let the machine suspend mid-playback. Take an org.freedesktop.login1 Inhibit (sleep:idle, block) fd while the MPRIS status is Playing and drop it on pause/stop, hooked to the same play-state channel MPRIS already uses. Linux-only, all failures log-and-continue. Resolves#522.
cc
Under Snap's mediated session bus, requesting the org.freedesktop.ReserveDevice name is denied and the error aborted ALSA Direct stream creation, so there was no audio. Fall back to the existing Degraded state (proceed without the reservation) on acquire failure, reusing the same non-fatal path the card-index / no-bus cases already take. Audio sample path untouched. Resolves#534.
cc
It flickers on the wgpu tier; the reporter confirmed QBZ_RENDERER=gl fixes it. Added the uhd-600 markers to WEAK_GPU_MARKERS so it auto-selects the stable GL renderer. Resolves#578.
cc
The live-drag thumb reverted to the round-tripped bound value on release; because that value echoes back asynchronously the thumb snapped to the stale position then to the fresh one (a rubberband bounce). The thumb now tracks drag-fraction, which stays put on release and re-syncs to the external value only when it changes on its own (MPRIS / keyboard) while not dragging.
cc
The de-quantization (0.1% scale + f32-bits atomic) removed the stepping in the VALUE, but the thumb still derived from the round-tripped bound value, so it felt snappy/laggy during a drag. Give QbzSlider/QbzVerticalSlider a live drag fraction: while held, the thumb tracks the pointer position directly (continuous, no round-trip); on release it reverts to the bound value. The emitted value is still the nearest step to the pointer (round), now imperceptible at the 0.1% scale.
cc
Adds a prominent 'bug reports need logs' banner and a required pre-flight checklist (attach logs or explain why not, searched duplicates, included repro steps). Without logs most reports stall on needs-info.
cc
Volume snapped between 1% steps from two integer quantizers. UI: the volume sliders now use a 0..1000 scale so the shared int slider's round() lands on 0.1% (imperceptible). Runtime: the player volume atomic widened from integer-percent AtomicU64 to f32-bits AtomicU32 (same idiom as normalization_gain) so a continuous value no longer re-snaps on track restart / device switch / gapless. Bit-perfect untouched: 1.0f32 round-trips its bits, no gain multiply or ramp added to the protected ALSA-direct / DoP / JACK paths.
cc
Adds a dedicated drag HANDLE (grip) in the reorder gutter with the queue's drag gesture (6px threshold, absolute-position - root.y correction, three end paths incl. the changed-pressed safety net), so the row-body drag-to-sidebar stays intact. PlaylistView keeps component-local reorder state (slot math folds in the ListView viewport-y), a drop indicator and a floating ghost; commit fires PlaylistActions.reorder-track(from,to). Rust reuses the arbitrary-index persist path (repo::reorder for local, persist_custom for Qobuz) the arrows already back. Edge auto-scroll during drag is a follow-up TODO. Resolves#589.
cc
Lightweight kiosk shell — profile boot (windowed default, fullscreen opt-in),
immersive mount, Now Playing redesign, Search All dashboard, lyrics lite-fill,
plus the owned-playlists Library fix.
cc
load_favorites built the Playlists Library sub-tab only from locally-hearted ids and the Following list excludes owner.id == uid, so the user's own playlists appeared nowhere. Seed the Library list with owned playlists first, then hearted-but-not-owned (dedup). Fixes desktop and kiosk.
cc
The per-word karaoke sweep animates a clip rect ~60x/s, driving continuous full-window repaints (~4% CPU). Lite fill highlights the active line whole (no per-frame animation) so the surface only repaints on line changes. New toggle in the lyrics controls flyout, persisted via lyrics_prefs; default off (smooth karaoke). Kiosk / weak-GPU tiers force it on via ShellState.reduce-motion.
cc
The cover is image-fit:contain filling the flex art area (no fragile parent-geometry math), so it is large and scales with the window. Toggle and meta rows get vertical-stretch:0 so only the art area takes the slack (nested layouts stretch greedily) — kills the dead space and the ~55px cover. Title toned to 20px, quality stamp floats on the transport row instead of owning a line. Cover<->Lyrics toggle; lyrics fill the panel width.
cc
In kiosk the layout-flyout would carry a single Desktop mode row that the fixed-height popup floats high above the button, so the layout button toggles straight to Desktop mode instead. Lyrics/Queue toggles are hidden (their panels aren't mounted in the kiosk shell; the queue lives in Now Playing).
cc
All is no longer a flat album grid: a polymorphic most-popular hero (artist/album/track) plus Artists/Albums/Tracks(6)/Playlists previews, each with a View all that jumps to that tab. Reads SearchState verbatim, mirroring the desktop hierarchy.
cc
Left panel toggles cover/synced lyrics (reuses LyricsLinesView, gated on status==2 to dodge the geometry recursion footgun); right panel is a Queue/History list (Flickable+VerticalLayout so rows top-anchor). The lyrics-sync engine bailed unless a desktop/immersive surface was open, so a kiosk-lyrics-follow flag on ShellState widens lyrics_surface_open().
cc
The immersive overlay hangs inside AppShell, so the kiosk shell mounts its own copy. The transport bar is hidden on Now Playing (redundant there) and the content height no longer subtracts it then, so the NavRail stays pinned. Footer Search tile removed — the back-bar carries a persistent search field.
cc
Forcing fullscreen at kiosk boot trapped the user with no way out (Esc/F11 don't leave it, the kiosk shell has no titlebar control). Fullscreen is now opt-in for a real appliance image; the persisted-kiosk-profile case opens in a normal window.
cc
KioskLocalLibrary (tabs Albums/Artists/Folders/Tracks, reading
LocalLibraryState, driving LocalLibraryActions; open-album routes to the
local-album detail) and KioskMyQBZ (Mixtapes/Collections tabs over
MyQbzState, MyQbzActions.open-card). Both reuse the shared kiosk cards/grid
+ ListView for tracks. Wired onto the local-library / mixtapes / collections
routes. With this every kiosk content view is the light version. Validated
via slintcheck. (v1: no album-window / tracks-load-more hooks yet.)
cc
- Add the forward nav button beside back (NavState.can-forward/request-forward).
- The kiosk back bar owns the window chrome (no HeaderBar): macOS leaves the
~78px traffic-light inset, Linux frameless draws the custom WindowControls
on the right. OS-agnostic (the kiosk is not restricted to any platform).
- Hide ALL NPB view modes not compatible with kiosk (New/Classic/Small/Large
+ Miniplayer + Immersive) while in the kiosk profile; only the profile
toggle remains.
- Track lists (Library/Search) use ListView so only the visible band mounts
— the plain VerticalLayout spiked CPU on entry, which a Pi cannot afford.
cc
KioskNowPlaying: a 50/50 full screen (big cover + title/artist + AudioStamp
+ SeekBar + the reused 7-control TransportControls on the left, the Up-Next
queue on the right), reading NowPlayingState/QueueState and calling
QueueState.panel-opened() on init. New ContentView.now-playing route; the
NavRail Now Playing tile navigates there instead of opening immersive. Also
hide the Miniplayer + Immersive items from the NPB view menu while in the
kiosk profile (todos o ninguno) so it is not a second-window/overlay mix.
Validated via slintcheck.
cc
Add a "Kiosk mode" / "Desktop mode" item to both PlayerBarSmall and
PlayerBar view menus (label from the new ShellState.kiosk-profile flag).
It fires media-action("npb-view","","toggle-profile"); Rust flips the
persisted ui_prefs.profile and swaps the shell in place via set_screen
(KioskShell/AppShell share state, so no restart). The toggle keys off the
current screen, not kiosk_profile_active(), so a QBZ_PROFILE env override
doesnt pin it. ShellState.kiosk-profile is seeded at window setup.
cc
NavRail grows to 8 tiles (Discover, Library, Local Library, MyQBZ, Search,
Now Playing, Visualizer, Settings) routing through the same header-menu
routes. Also center each tile's icon: a fixed-size Image does not center
itself in a VerticalLayout, so wrap it in a centering HorizontalLayout.
cc
Replace the reused AlbumPageView/ArtistPageView on the kiosk album/artist
routes with Pi-light detail: KioskAlbum (cover + title + tappable artist +
Play/Shuffle + number/title/duration track list, reading AlbumState) and
KioskArtist (round portrait + Play/Radio + discography as KioskCard shelves
per release section, skipping the \"other\" bucket, reading ArtistState). Both
bind the shared state so covers push in. Validated via slintcheck.
cc
Layout-based attempts (child stretch, mount stretch, explicit HL width) all
left the tiles collapsed to icon width packed on the left. Position the 6
tiles by absolute x + explicit width derived from root.width instead; the
rail width comes from the KioskShell VerticalLayout, so the tiles now span
the panel evenly.
cc
Replace the heavy reused SearchResultsView and FavoritesView on the kiosk
search/favorites routes with Pi-light versions built from the shared kiosk
cards: KioskAlbumGrid (wrapping grid of KioskCard), KioskArtistCard (round),
KioskTrackRow. KioskSearch tabs All/Albums/Tracks/Artists/Playlists reading
SearchState; KioskLibrary tabs Tracks/Albums/Artists/Playlists/Labels reading
FavoritesState. Both bind the shared models so the artwork pipeline pushes
covers in with no request hook (album covers windowed = first ~60 for now).
Validated via slintcheck.
cc
The tiles still collapsed to the bottom-left even with per-tile
horizontal-stretch: a HorizontalLayout nested in the rail's VerticalLayout
was taking its preferred (content) width, not the rail width. Drop the
wrapper, make the tile row an explicit width: root.width child with the
hairline as an overlay, and the 6 tiles now spread evenly across the panel.
cc
Replace the heavy reused HomeView on the kiosk home route with a Pi-light
Discover: KioskCard (square art + quality badge + title + artist, tap-to-
open, no hover/context-menu chrome) in horizontal shelves, reading the SAME
DiscoverState model so the artwork pipeline pushes covers in with no request
hook. Tab pill (Home/Editor's Picks/For You). First of the mockup-matching
lightweight views; also guards single-key hotkeys while typing in the kiosk
search field. Validated via slintcheck.
cc
The tiles collapsed to icon width, packed bottom-left with truncated labels
(a horizontal-stretch set only on the child component root is not honored by
the parent layout). Set horizontal-stretch at each mount site + fill the rail
height so the 6 tiles distribute evenly as full-height touch targets.
cc
The P0 kiosk block read a creating_mini local that only exists inside
select_slint_backend(); at the main-window setup site it is out of scope.
Call crate::miniplayer::is_creating_mini() directly (same source the backend
hook uses) so the qbz bin compiles. Confirmed: the kiosk shell builds
(release) and launches.
cc
Skip fullscreen-at-boot when QBZ_KIOSK_NO_FULLSCREEN is set so the kiosk
layout can be previewed in a normal window (the kiosk shell exposes no
titlebar control to leave fullscreen). reduce-motion still applies.
cc
The kiosk drops the HeaderBar (where the desktop search box lives), so add a
search LineEdit to the back bar: Enter submits through SearchActions.submit
(the same seam the header uses) and lands on the results view. This makes
Search functional end-to-end in kiosk (query -> SearchResultsView -> tap a
result -> the album/artist detail views mounted in P2). Validated via the
slintcheck stub.
cc
Extend the KioskShell content mount to every ContentView route AppShell
hosts (discover-browse, recent-albums, local-album, artist-releases,
musician, label(-releases), award(-albums), location, purchases, purchase-
album, playlist(-manager), mixtapes, collections, mixtape-detail,
discography-builder, offline/blacklist managers, local-library, mix,
playlist) plus the offline placeholder + qobuz-view-blocked gate — a
verbatim copy of AppShell so navigation reaches the same real views by
construction. Discover, Search, and the album/artist detail views tapped
from them are functional. Validated through the Slint compiler (slintcheck
stub). ShellContent DRY extraction still deferred.
cc
Add AppScreen.kiosk mounting a KioskShell sibling of AppShell in the same
AppWindow: a thin back bar + persistent PlayerBarSmall + a bottom NavRail of
6 tiles replace the HeaderBar/Sidebar, sharing every *State global verbatim.
The NavRail routes through the SAME NavState routes the desktop uses
(header-menu-navigate / request-settings / ContentView.search). Content
reuses the real Home/Album/Artist/Favorites/Search/Settings views (verbatim
subset of AppShell mounts) with the arithmetic content-height pattern to
avoid the layout-collapse footgun. Rust lands the Kiosk screen post-login
when the kiosk profile is active. Validated through the Slint 1.16.1
compiler (slintcheck stub); remaining routes + touch-interaction remap are
P2/P3.
cc
Add a kiosk startup profile for small-panel touch appliances (Pi/SBC): the
main window boots fullscreen and forces reduce-motion regardless of renderer
tier. Resolution is env-first (QBZ_PROFILE=kiosk, so the kiosk image sets it
in the autostart) with a persisted ui_prefs.profile fallback; the miniplayer
window is never affected. No .slint touched. The kiosk image still pins
QBZ_RENDERER=gl and the XS ui_scale preset via env (separate knobs).
cc
Remove boot_watchdog, graphics_detection, and settings::{download,legal,
library,image_cache} (only src-tauri consumed them). Split
graphics_autoconfig: keep Environment/detect_environment/detect_gpu_name
(used by the Slint diagnostics panel), drop the WebKit Recommendation half.
cc
release-linux had save-if:false deferring cache writes to build-slint.yml,
which no longer runs — so the amd64 `slint-linux` dep cache was never created
and every release/snap amd64 build ran fully cold (no dep cache; qbz_ui
recompiles regardless). release-linux-aarch64 already saves (save-if:true),
which is why only the aarch64 cache existed. Save on main so pre-flights warm
it and tag runs restore it.
cc
The publish step failed the whole run with "login_data is empty" when
SNAPCRAFT_STORE_CREDENTIALS is unset, even though both binaries compiled and
the amd64 snap packed fine. Move the secret to job level and gate the publish
step on it: with no secret the snap is still built and uploaded as an artifact,
publish is skipped (green run) instead of failing. Applies to both arches.
cc
Both arches now publish from CI: the existing amd64 track plus a new
build-binary-arm64 (native ubuntu-24.04-arm, free for public repos, reuses
release-linux-aarch64's cargo cache) -> snap-arm64 that dumps the prebuilt
aarch64 binary with packaging/snap-slint/snapcraft-arm64.yaml. This CI upload
is the sole publish path; Launchpad / "Build from GitHub" is not used.
cc
New rule: the release codename stays fixed across a minor line and only changes
at the next minor (2.1.x). 2.0.1 therefore keeps "Rebuild 破 (You Can (Not)
Advance)"; the About-modal literal is unchanged.
cc
The About modal and website render the release's TAG_DETAILS; for the 2.0.1
point release, preserve the full 2.0.0 story and prepend a terse 2.0.1 section
(one line per fix) plus the 2.0.1 compare link.
cc
Workspace + lockfiles, the dead-Tauri consistency version fields, the metainfo
<release> entry, and the release docs (TAG_DETAILS.md / CHANGELOG.md).
The About-modal codename and the TAG_DETAILS h1 are left as __CODENAME__ /
__SUBTITLE__ placeholders on purpose — the owner names the release before the
tag (2.0.0 was "Rebuild 破 (You Can (Not) Advance)").
cc
The per-track context menu in the Queue sidebar had two placeholder items
with no click handler: "Remove all after" and "Add to playlist". Wire both
end to end.
- QueueState gains remove-all-after(int) and add-to-playlist(int), resolved
from the page-local row index like remove-upcoming (search-filter aware).
- Core/player gain remove_upcoming_after(upcoming_index): truncates the
upcoming list in PLAY order (shuffle-aware, reuses remove_upcoming_track),
clears the stop-after marker if it lands in the removed range, and emits a
single QueueUpdated. The pre-existing remove_after (absolute-index, not
shuffle-aware) was dead code and stays only for its unit coverage.
- Add to playlist reuses the save-as-playlist picker handoff seeded with the
single row's track id.
cc
The NPB Large dock idled at ~2.5% CPU paused: the visualizer gates on
visibility only, so the FFT producer kept re-transforming the stale
ring buffer at 30fps and the drain timer kept churning the 28 bar
models. Producer now parks when enabled-but-paused (same bounded
200ms park_timeout shape as the existing disabled-park; the atomic is
a lone Relaxed store, submit path untouched); the drain early-returns
when not playing and unparks the producer on the resume edge, so
fresh bars land in <100ms. The paused flag is stored at every
playing-state flip: toggle, track meta seed, the poll loop dirty-push
(catch-all for MPRIS/tray/hotkeys/QConnect within one 450ms tick),
the cast poll while casting, stop-after, queue end and the
unavailable-skip halts. Bars freeze at their last values while
paused; immersive freezes too — intended. Legacy Tauri tree's tap
literal gained the field to keep compiling.
cc
Tauri let the user rename how QBZ announces itself to Qobuz Connect;
the Slint port lost the UI entirely while keeping the persistence and
the announce path (same device_name key in qconnect_settings.db, read
by connect() -> bootstrap_remote_presence). Restored 1:1: a text row
in Settings > Playback next to the QConnect startup select (LineEdit
mirroring the ListenBrainz token row, placeholder = the resolved
default 'Qbz - {hostname}'), a new settings-string callback channel
through the settings chain, and a live-service setter so a rename
takes effect on the next connection without an app restart — exactly
Tauri's semantics (no live re-announce; the row description says so).
Trim-only validation like Tauri; empty clears the override. Two new
msgids across the 8 locales.
cc
The rail's 'View all' opens a dedicated page listing the recently
played albums (the dedicated album history from the #567 fix, cap 24).
Built on the AwardAlbumsView skeleton stripped to essentials: fixed
header + windowed AlbumCollectionView wired per the new sampler
contract, scroll-restore, shared scrollbar. Works offline (local
history — the AppShell arm deliberately skips the offline gate) with
mixed-source rows routing through the existing open-album funnel
(qobuz/plex/local) and source-split artwork dispatch mirroring the
rail's. Full nav integration: NavEntry::RecentAlbums round-trips
through last_nav so view persistence restores it (a local JSON read —
cannot crash-loop), ADR-004 back button via the standard record +
navigate + flags shape. Zero new msgids — title, empty state and the
View-all link all reuse existing strings across the 8 locales.
cc
Third and final hit of the 'Recursion detected' panic class
(i-slint-core properties.rs:583), this time crashing DiscoverBrowse on
startup view-restore (and crash-looping via persistence). Root cause
symbolized from the field backtrace: every .slint 'changed x =>'
compiles to a ChangeTracker whose watched expression evaluates
SYNCHRONOUSLY at component instantiation, and conditional/repeated
components instantiate from ensure_updated INSIDE whatever property
evaluation triggered them — so a windowing band that reads layout
results (viewport-y, flick.height, self.width, absolute-position) can
re-enter a property mid-evaluation. Timing-sensitive: needs the model
filling in the same pass that instantiates the grid, which startup
view-restore hits deterministically.
Class-kill, uniform on every windowed surface (owner mandate — no
survivor exceptions): band state is now plain properties written ONLY
by an 80ms sampler Timer (event-loop context, nothing on the property
stack) in the shared AlbumGrid, the horizontal Carousel and the search
grid. No changed handlers, no init-time reads; the ±1-viewport margin
absorbs the sampling cadence; consumers' declarative wiring unchanged
(inputs are polled, never observed — contract documented in-file).
Startup crash-chain watchdog ('perro policía'): a counter file armed
before risky init and cleared by the same liveness signals as the
renderer sentinel. One pre-liveness death -> next boot resets exactly
ui_prefs last_nav/last_view (the field-verified surgical bypass) with
a recovery toast; two -> additionally bypasses the session-persist
queue restore for that boot, and the exit save keeps the preserved
snapshot instead of clobbering it with the empty queue. Recovers this
incident and the #558 loop automatically. One msgid x8.
cc
ArtistCard grew for the Recommendations subtitle line, and Search
cards fill that same slot ('N albums') plus the Follow chip — but the
two Search carousel instantiations kept the 220px default, clipping
the chip. Pass card-height 256px, the same value Recommendations
sizes the identical stack with. ArtistCard itself untouched.
cc
Two contributors in one week sent solid PRs against the retired
Tauri/Svelte tree (#561, #564) because nothing in the repo says it is
dead. Three zero-risk notices, no code touched: a pull_request_target
bot that leaves one sticky comment when a PR touches src//src-tauri/
(target on purpose — fork PRs cannot be commented under plain
pull_request; the PR code is never checked out), sentinel READMEs at
both tree roots (what GitHub renders when browsing), and a PR template
checkbox. The tree deletion + local reference worktree is the 2.0.2
follow-up (owner hold: no dependency risk allowed before 2.0.1).
cc
Two Home bugs, one dropped port. #566: four configurator sections
(favoriteAlbums 'Library Albums', qobuzMixes, releaseWatch, topArtists)
were offered and persisted but had no HomeView render arm and no data
in load_home — enabling them produced an invisible row. All four were
genuine Tauri-Home features (DiscoveryView.svelte arms :632/:653/:660/
:666, loaded unconditionally), so this is the full port, not a trim:
own HomeState fields (separate lifecycle from ForYouState), shared
foryou fetch pipelines (favorite_album_cards / fetch_release_watch /
top_artist_cards extracted pub(crate), For You behavior untouched),
Home-scoped artwork targets, and HomeView arms mirroring ForYouView.
load_home joins the three fetches concurrently with the index — zero
added latency. A HOME_RENDERABLE guard next to descriptors_for is now
the single source of truth: an enabled id without a render arm can
never emit an invisible descriptor again.
#567: 'Recently Played Albums' starved because albums derived from the
24-track history (long albums collapse to ~4 distinct). recently.rs
now keeps a separate album history (dedupe by id at record time, own
24 cap) with additive migration from the legacy bare-array format;
tracks keep their cap; load()/load_albums() signatures unchanged.
Zero new msgids (all three titles reuse For You's). qbz-app tests
10/10; slintcheck green.
cc
Extends the #556/#565 fix (one malformed item in a JSON array nuked
the whole list via from_value::<Vec<T>>().unwrap_or_default()) from
favorites to every remaining silent-blank site found by audit: the
search parse_page (EVERY search tab — scalars now parse separately,
items per-item), For You fav-albums/fav-artists rails, the daily-mix
favorite_tracks source, and qbz-qobuz get_dynamic_suggest. The helper
moves to qbz-models::lenient (the one crate all consumers reach) with
unit tests; favorites re-uses it via alias, local copy deleted. The
~30 endpoints whose parse errors PROPAGATE stay as-is — honest errors
stay honest. New parse_page test covers a poisoned item surviving.
cc
qbz-core tests did not compile since the Album model gained these
fields; the test-only helper never got them. Found while adding the
poisoned-item parse_page test.
cc
PR #569 follow-through with the owner-corrected scope: on macOS BOTH
titlebar modes work (native bar, or traffic lights integrated in the
QBZ header) — what's broken is hot-applying the toggle: the overlay
window attributes are fixed at creation, but the .slint chrome
(no-frame, header drag+inset, Purchases placement) flipped reactively,
hiding the buttons one way and overlapping them the other.
Split pref from applied state: the toggle edits use-system-title-bar
(persisted), the chrome reads a new system-title-bar-active seeded at
startup; the handler mirrors pref->active live on Linux only (behavior
byte-identical there), macOS keeps the session value + the existing
restart toast. The hide-title-bar row is macOS-hidden (that part of
the PR was right); the system-bar toggle stays.
cc
Follow-through on 6a0c3d5e (state-visible hearts): the card-level
album 'favorite' arm was still add-only — a filled heart re-added and
toasted 'Added to favorites'. It now toggles, mirroring the header
arm: fav_cache-keyed, optimistic set_album_row_favorite + rollback on
error, cache written on success only, reco logging on ADD only (the
Capa B search-interaction record is now gated to adds, like tracks).
AlbumCard's menu item goes state-conditional (Remove from Library +
filled heart), and the now-playing '+' flyout album entry becomes
state-honest through a new NowPlayingState.album-favorite (seeded per
track in refresh_now_playing_meta, flipped live by
set_album_row_favorite — which now also syncs the open album page's
header heart, closing the stale-header undo loop). Favorites' own
intercepted unfavorite (fade + row removal) is untouched, both view
modes. One msgid x8 locales.
cc
Port of PR #564 by @afonsojramos (his fix targeted the retired Svelte
tree; the live Slint app had the same bug, wider): AlbumCardItem
carried is-favorite and AlbumCard rendered the filled heart, but only
the Favorites tab ever populated it — artist discography, album
suggestions, search, home, discover, foryou and reco cards all showed
empty hearts for favorited albums. The six mapping funnels now seed
the flag from fav_cache, and a new set_album_row_favorite() flips it
by album id across every visible card model (mirroring the tracks'
set_row_favorite), wired to the card heart and header toggle with
optimistic flip + rollback, and to the Favorites unfavorite.
Album favorite analysis and Svelte reference: afonsojramos (PR #564).
cc
Restores the Tauri-era Qobuz Connect startup setting, now in
Settings > Playback per owner request. Reuses the shared
qconnect-app::startup module (QconnectStartupMode +
compute_effective_startup, unit-tested) and Tauri's exact
qconnect_settings.db keys (startup_mode / last_known_state), so an
upgrading user's choice survives. last_known_state is written at the
manual toggle's success points only — internal teardowns (D5 offline
force-disconnect, bootstrap failure cleanup) never clobber the
remembered state, mirroring Tauri's command layer. Startup auto-connect
runs post-enter_shell (online sessions only, once per process) through
the same connect() path as the toggle, on Tauri's bounded retry
schedule, bailing silently under offline mode instead of toasting per
retry. Select row mirrors the quality-retries row; 5 msgids x8 locales.
cc
Ports PR #561 by @pbaart into the live gettext system (the PR
translated the retired Svelte catalog): full nl.po (1481 msgids, 13
plurals, msgfmt-clean, zero untranslated), 262 strings taken directly
from the contribution via the legacy-English bridge and the rest
translated using the contributor's terminology as the style guide
(nummer/afspeellijst/wachtrij/bibliotheek, informal je). qbz-i18n
registration (2-form plural, resolve_auto nl_NL->nl, 21/21 tests) and
the language dropdown at index 8 (Nederlands) across ui_prefs,
reseed_i18n_labels and state.slint — same shape as the ru/ja commits.
Dutch translation credit: pbaart (PR #561).
cc
Owner-corrected UX: the compact view keeps exactly TWO buttons — the
screen must serve fast. Upload moves from the advanced row to the
always-visible footer (with its uploaded-URL row un-gated so the link
shows in compact view too); level/search/refresh/auto-tail, the
diagnostics-bundle copy, open-file and clear stay behind [+]. Reverts
the open-expanded default from fe43c698.
cc