Commit Graph
83 Commits
Author SHA1 Message Date
vicrodh ad6c98ff7d feat(queue): Play later everywhere — every context menu and bulk bar (#442)
Extends Play later (end of the manual block) from track rows to every
surface that already offered Play next / Add to queue:

- playback.rs: enqueue_album_later, enqueue_playlist_later (mixed
  sidecar interleave), enqueue_artist_top_later, enqueue_tracks_later,
  enqueue_local_tracks_later — all NATURAL order (each add_track_later
  appends, no reverse), QConnect controller routing via
  play_later_batch_on_peer_if_active, and post-add queue sync.
- local_playlist::enqueue_later_by_id (source-aware rows, offline-only
  stamp preserved), myqbz_play RowMode::PlayLater + BulkEnqueueMode.
- 19 dispatch arms in main.rs (album/playlist/artist/label/mix/
  favorites/local-library/album-page/disc/myqbz dialects).
- 12 context menus + 10 bulk bars in .slint: list-plus icon between
  Play next and Add to queue, popup heights bumped.

The immersive tap-action dropdown is deliberately untouched (it is a
configurable single-tap behavior setting, not a context menu).

km
2026-07-26 21:08:12 -06:00
vicrodh a8919cbadf feat(app): the round's wiring — gamescope, watchdog, geometry, hearts
- Steam PR1/PR3/PR4 wiring: apply_gamescope_quirks at the top of main(),
  tray + miniplayer suppression, gilrs spawn, UiFocusState OSK hook.
- #558 watchdog arm at window-ready; #654 monitor-DPR geometry clamp;
  local-heart routing in the favorite arms; play-later media-action arm;
  sort-flip handlers with direction (#657/Library All pattern); gilrs +
  wayland-data-control deps (#514).

km
2026-07-25 12:05:56 -06:00
vicrodh 3eb93ec4cb Merge branch 'bugfix/internal/discover-reco-follow-state' into chore/internal/smoke-lyrics-discover
# Conflicts:
#	crates/qbz-ui/translations/de/LC_MESSAGES/qbz-ui.po
#	crates/qbz-ui/translations/en/LC_MESSAGES/qbz-ui.po
#	crates/qbz-ui/translations/es/LC_MESSAGES/qbz-ui.po
#	crates/qbz-ui/translations/fr/LC_MESSAGES/qbz-ui.po
#	crates/qbz-ui/translations/ja/LC_MESSAGES/qbz-ui.po
#	crates/qbz-ui/translations/nl/LC_MESSAGES/qbz-ui.po
#	crates/qbz-ui/translations/pt/LC_MESSAGES/qbz-ui.po
#	crates/qbz-ui/translations/ru/LC_MESSAGES/qbz-ui.po
2026-07-22 23:05:16 -06:00
vicrodh 6adb33fcef feat(settings): Recommendations tab in the Blacklist Manager
Manage the reco-scoped Not-interested dismissals: a third tab in
Settings > Blacklist lists dismissed artists (search-filtered like the
other axes, count badge, empty + no-results states) with a per-row undo
that removes the entry from the reco_dismiss store — the artist becomes
eligible for the Recommendations rails again on their next paint. Row
click reuses the existing artist-select navigation. No bulk Clear-All on
this tab (undo is per-row); the modal stays scoped to the blacklist axes.

km
2026-07-22 22:18:59 -06:00
vicrodh b1b64f2dfb feat(ui): translation language setting in the lyrics flyout
New QbzSelect row (Auto (account language) + en es fr de it pt nl ja ru,
the owner-approved list) after the Size row; language names are native
literals, the same convention as the Appearance dropdown. The options
array lives on LyricsState (reseeded from Rust on language change) and
the choice persists per-user in lyrics_prefs.json as translation_language
("auto" or an ISO 639-1 code), sanitized on load (unknown -> auto) and
round-tripped through apply_to_ui/persist_from_ui like the other prefs.
Flyout popup height grows by the new row (and the comment now counts the
lite-fill row it had missed).

km
2026-07-22 19:59:27 -06:00
vicrodh 499f652b71 feat(ui): carry lyrics translation state and render bilingual lines
LyricsLineItem gains translation-text (empty when the doc has none) and
LyricsState the translation props (translation-available, show-translation,
toggle-translation callback) per spec section C. LyricsLinesView renders
the translation under its original line while show-translation is on —
smaller (~72%) and dimmer, static text that follows the line; the karaoke
fill stays keyed on the original timings/text. Secondary surfaces share
the model, so the field flows through untouched.

km
2026-07-22 19:58:35 -06:00
vicrodh 6c6eaf2b36 merge: Most Played Albums (feature/most-played-albums)
Play-count album history store (album_play_history, WAL per ADR-002) hooked
into record_recent, Most Played Albums carousel on Home + For You (default
off), View-all page with search + per-card play count, i18n in all 8
locales.

Conflicts resolved against pre-release: kept the Slint-era Pinned section
alongside the new MostPlayedAlbums everywhere (DiscoverySectionId, prefs,
render kinds, nav, ContentView, HomeActions); AlbumCardItem carries both
is_pinned and plays (populated via card_to_item); .po files union-merged,
msgfmt clean; discover_prefs tests updated (home 15/8 ON, forYou 14/13 ON)
— 10/10 green. Release build green.

km
2026-07-22 17:46:09 -06:00
vicrodh f9a8edcbeb fix(shell): macOS NPB-Small bottom clearance via Rust-set npb-small-extra
The 2px .slint literal proved invisible (owner ask 2026-07-21, iteration
2026-07-22). The extra clearance is now set from Rust at startup
(ShellState.npb-small-extra, default 6px, macOS-only, QBZ_NPB_SMALL_EXTRA
env override) so tuning costs a ~2-minute qbz-bin rebuild instead of a
~2 h qbz-ui one on the 8 GB Mac. Linux stays pixel-identical (extra = 0).

km
2026-07-22 16:49:23 -06:00
vicrodh f18960ba54 feat(library): collapsable multi-artist header on local album detail
Spec 2026-07-19-local-album-grouping-mode (B2). A compilation album (the
Saint Seiya case: 10 distinct track artists) now shows the single artist
line plus a '+N more artists' chevron that expands into the full
track-artist list, each entry clickable into the LocalLibrary Artists tab
(name-based open-artist). Distinct raw track artists in first-appearance
order, computed in apply_album_version into LocalAlbumState.all-artists.
Expansion is view-local and resets on album change; single-artist albums
never see the chevron. Track rows already showed their own artist
(verified, no change needed).

2 msgids ('+{} more artists', 'Show less') x 8 locales; slintcheck green.

km
2026-07-21 14:11:04 -06:00
vicrodh c72e71d410 feat(library): album-identity mode — header dropdown, Settings row, persistence
Spec 2026-07-19-local-album-grouping-mode (A2-A5 + i18n). The Albums tab
gains a third header dropdown ('Albums by folder' / 'Albums by metadata')
and Settings > Local Library a matching 'Album grouping' row — both write
the same pref: LocalLibraryState.albums-id-mode, persisted in
locallibrary_ui.json (default 'folder': compilations and box sets stay
whole; the metadata split stays opt-in for #411's use case).

The group key changes with the mode, so switching reloads the Albums set
(reload_albums) and invalidates the Artists tab (its album cache groups
the same way — a folder-mode compilation cross-lists under every artist
in all_artists, the owner's 'appears in all artists' ask). Threaded
through spawn_albums_load, seed_counts, the Plex quality-hydration
aggregate lookup, and the artists feed; myqbz_builder pins Metadata
(artist-scoped candidates, not the Albums view).

5 msgids x 8 locales (msgfmt clean); slintcheck green.

km
2026-07-21 14:11:04 -06:00
vicrodh d4a4f30e91 feat(label): web-parity header actions, catalog/library toggle, label cards
LabelView parity with the revamped Qobuz web label page (semantics
verified live against play.qobuz.com via Playwright):

- Header: Follow + Shuffle (new play_label_top_shuffled, shuffle-then-
  delegate to play_tracks_ctx) + overflow menu (Play next / Add to queue /
  Share -> play.qobuz.com/label/<id>; no Song.link equivalent for labels).
  No header Play by owner decision (the Popular Tracks play does the same).
- The Popular Tracks section '...' button was dead (fired an unhandled
  'top-tracks-menu' action): now a popup with Play next / Add to queue
  over all cached popular tracks. Section buttons wrapped so the 44px
  primary and 32px secondaries share one vertical centre.
- Catalog/library toggle: the ArtistPage SegmentedTabBar verbatim, floated
  right of the header cluster (visible only with library items; mirrors
  the web's A/B-rolled 'Del catalogo / En la biblioteca'). Library tab
  renders added tracks + albums on the label, backed by the new
  library_by_label session index (AlbumCard.label_id / TrackCard.label_id,
  seeded from the SAME two favorites fetches as library_by_artist).
- More Labels carousel now renders the Library LabelCard (new label-cards
  mode in ArtistCarousel) instead of the legacy generic card.
- Artists carousel images: /label/page top_artists often carry no parseable
  image, leaving placeholders forever. Restored the Tauri fallback wave:
  custom artwork first, then v2/artist/get, fed through the stable
  LabelArtist artwork targets.

i18n: zero new msgids (all reused across the 8 locales). slintcheck green.

km
2026-07-21 12:30:40 -06:00
vicrodh 70d8dc85f2 feat(macos): center traffic lights in the header + NPB-Small +2px
With the custom chrome (titlebar_transparent + fullsize_content_view)
AppKit parks the native traffic lights at the stock ~28pt titlebar height,
visibly above the 42px header controls. macos_chrome.rs measures the close
button's centre in window coordinates and shifts the shared button
container by the delta to the 21pt header centre (custom chrome only,
one-shot after show; known gap: AppKit re-lays the titlebar on fullscreen
roundtrips). Deps: raw-window-handle direct + slint raw-window-handle-06 +
objc2-app-kit NSWindow feature, all macOS-target only.

NPB-Small on macOS gains 2px of bar height (ShellState.npb-small-height,
44px, growing upward from the bottom-anchored bar) so CJK/descender-heavy
meta lines stop clipping at the window's bottom edge. PlayerBarSmall pins
the song card to its old 39px top-anchored geometry: the art keeps its
exact top-relative position and the 2px land as bottom clearance. Linux
geometry is pixel-identical to before. slintcheck stub green.

km
2026-07-21 12:30:19 -06:00
vicrodh b4b1f38cf0 feat(whats-new): render standalone release-note links as clickable
The What's New / release-notes modal renders the GitHub release body through a
custom block model that stripped inline markup and had no link support, so a
markdown link showed as literal `[text](url)`. Now:
- inline links `[text](url)` collapse to just their text (clean attributions),
- a whole-line link becomes a KIND_LINK block rendered as a clickable accent
  link opened in the browser (a new WhatsNewActions.open-url reusing open::that,
  mirroring about.rs).

Adds a `url` field to WhatsNewBlock plus a small markdown-link parser. The 2.0.2
notes use it for the daemon-wiki link. Built + smoked at release (window ready,
ALSA 192kHz bit-perfect, playback OK).

cc
2026-07-20 21:43:02 -06:00
vicrodh 3118abb4a1 feat(ui): per-renderer quality cap row in the cast picker (#638)
CastState gains the fix-4 cap-row surface (device-cap-available/-key/
-options/-index + global-quality-label) and CastActions the
set-device-quality-cap(cap-key, index) callback - property/callback
additions to existing globals only, no app.slint export change. The
connected panel gets one label-left / QbzSelect-trailing row per the
flyout alignment rule (explicit menu-width, popup wider for the long
option labels, no pills), visible only when the renderer has a stable
identity, plus the muted caption that is the literal answer to #638:
Qobuz sells four discrete tiers, there is no 48 kHz option.

The quality-line suffix property now also names the cap at work
(cause 3, 'capped for this device') when no over-cap disclosure takes
precedence. Copy discipline throughout: the cap is the user's own
quality/bandwidth choice - never an implied device incapability
(renderers accept ~anything and downsample internally).

i18n: 8 new msgids in ALL EIGHT locales (en identity-translated),
including the cause-3 tooltip line AudioStamp pre-wired in fix 1.
Unit style matched per catalog (ru Cyrillic units, nl decimal comma).
Validated with the slintcheck stub (full tree + all catalogs).

cc
2026-07-20 15:25:58 -06:00
vicrodh f450975675 feat(ui): truthful limit-quality-to-device row with read-only detected limit
Settings > Audio (#638 fix 3, spec C.6): the "Limit quality to device"
toggle's description now states what the wired setting really does — it
caps the REQUESTED tier, local playback only, never casting. The
conditional "Maximum sample rate" Hz dropdown is replaced by a read-only
"Detected device limit" line ("192 kHz · Hi-Res+") fed by the probe: a Hz
control was the documented root-cause trap (it offered rates the app
cannot request — Qobuz sells four tiers, and no 48 kHz tier exists), and
with real detection there is nothing left to pick. When the probe fell
back to the assumed common rate set, a plain informative caveat below the
row discloses that the cap still applies but may not match the hardware
(owner Decision B: no warning icon, no error styling).

Removes the dead dropdown plumbing from the desktop bridge only:
MAX_SAMPLE_RATES, the SettingsState sample-rates/sample-rate-index
properties, the snapshot fields/derivation/pushes, the "sample-rate"
select arm (and with it a dead Apply::Reinit device teardown for a value
nothing read), and the table test. Deliberately KEPT per the spec's
hazard analysis: every qbz-audio SQLite column/migration/setter/getter
(positional 23-column SELECT), the qbzd CLI/TUI surfaces, the settings-
bundle entries, and all existing .po msgids (msgmerge will mark obsolete
ones; "No limit" is reused elsewhere) — grep hits for the old identifiers
under crates/qbzd/ are expected and correct.

3 new msgids in all 8 locales. Slint tree + catalogs validated with the
slintcheck stub.

cc
2026-07-20 15:09:56 -06:00
vicrodh 534a2a0d5e feat(qbz): cap local playback requests at the detected output-device limit
Wires limit_quality_to_device to a real consumer on the LOCAL output path
(#638 fix 3, spec Phase C). New crate::device_cap module caches the DAC's
detected rate ceiling mapped to a Qobuz tier (spec C.3: >96k -> Hi-Res+ =
no effective cap; 96/88.2k -> Hi-Res; <=48k -> CD 16/44.1, bit depth
included since no 48 kHz tier exists). Detection is the proven read-only
query_dac_capabilities probe; it runs in spawn_blocking on EXPLICIT
triggers only (startup, the Settings toggle, output-device change, backend
change, reset-to-defaults) — never on the playback hot path or poll tick.
A stale cap after a hot-unplug until the next device change is the
accepted trade, same class as the HiFi wizard.

playback::local_playback_quality() returns (tier, QualityLimit): the
preference clamped by the cap, with the request-time cause for the badge
tooltip (tie reports LocalDeviceCap — the more specific constraint,
mirroring the spec's cast tie rule). All LOCAL request sites switch to it:
play_track_resolved, prefetch (fetch + throttle estimate), the gapless
pre-fetch, and the now-playing badge seed (which can now name the device
as the downgrade cause, msgid x8 locales).

PRECEDENCE (owner decision): the device actually playing governs.
playback_quality() stays the PURE preference and the cast path keeps
calling it — cast_service is untouched and the play_audible cast branch
returns before any local resolve, so the local DAC cap is structurally
unreachable from a cast. The two settings stay independent; nothing is
auto-synced. No downsampling anywhere: the cap only changes which tier is
REQUESTED; cached/offline bytes still play as-is.

Settings plumbing: SettingsState gains device-cap-summary/-detected
(property additions to an existing global — no app.slint export change);
build_snapshot fills them from the cache; refresh_device_cap re-probes and
re-pushes on the explicit triggers.

Protected-crate note: no protected file is touched — qbz-audio is only
READ through the dac_capabilities probe; qbz-player, playback_engine.rs,
alsa_direct.rs, alsa_backend.rs and the backends are untouched; no
sample-rate handling changes.

cc
2026-07-20 15:06:31 -06:00
vicrodh 20dc5f4a19 feat(cast): report the MEASURED delivered quality on the cast surfaces (#638)
Fix-1 cast half (spec Phase 1). During cast there is no PlaybackEvent —
the renderer is playing, not our engine — so the delivered quality is now
MEASURED from the bytes about to be served: register_qobuz probes the FLAC
STREAMINFO (shared qbz-models prober) before register_audio moves them,
which reports the truth regardless of which tier (network / L1/L2 cache /
offline store) produced the bytes.

- CastPicker line: measured label+detail via the shared formatter
  ("Hi-Res FLAC", "24-bit / 96 kHz"); quality_label_from_track demoted to
  the fallback for non-FLAC/local casts (F20 closed).
- The now-playing badge stays live during cast: cast_track publishes the
  85e11d28 effective properties plus the fix-1 cause/effective-tier through
  the SAME shared helpers the local poll uses (stream_downgraded /
  classify_limit_cause / delivered_tier_str — no forked arithmetic). The
  cause is the cast request-time resolution: preference today, renderer cap
  when fix 4 lands; NEVER the local DAC cap (precedence rule).
- Over-cap disclosure: origin != network AND measured tier > requested tier
  -> logged at info and surfaced on the picker line as "offline copy — cap
  not applied" / "cached copy — cap not applied" (Decision C: local bytes
  go out as-is, never resampled, never re-fetched — disclosed, not hidden).
  Three new CastState properties (quality-limit-cause, quality-over-cap,
  quality-origin), reset on disconnect; properties on existing globals only,
  no app.slint export change.
- remote_stream.rs now calls the shared prober, keeping its own CD-shaped
  defaults at the call site — behavior byte-identical (F17/F21).
- Two new msgids land in en.po; remaining locales come with the i18n pass.

Validated with the slintcheck stub (full .slint tree + 8 .po catalogs).

cc
2026-07-20 14:45:17 -06:00
vicrodh 6c0228b390 feat(quality): badge reports the DELIVERED quality and names the cause (#638)
Phase A of the #638 fixes (owner decision, reverses the deliberate #590
catalog-max choice back to Tauri behavior): while the delivered stream is
below the track's catalog max, the AudioStamp main line shows the DELIVERED
tier/detail, the amber arrow stays on, and the tooltip gains a third line
naming WHY — Source/Output plus the cause. A downgrade caused by the user's
own setting is the app doing what it was told, so the strings are
informative, never alarming.

Mechanics:
- REQUESTED_QUALITY_ID / REQUESTED_CAUSE atomics seeded once per track
  change beside TRACK_MAX_* (one ui_prefs load per change, never per 450ms
  poll tick), gated to Qobuz sources — local/Plex/ephemeral are not
  governed by the streaming preference and never get a cause line.
- Display-time classification (classify_limit_cause): requested Hi-Res+ or
  delivered below the requested tier's promise -> catalog availability;
  promise met -> the request-time cause (the user's preference today; the
  device/renderer caps of fixes 3/4 reuse the same discriminants).
- The #590 downgrade arithmetic (0.9 rate-family guard + full DSD
  exemption) is extracted UNCHANGED into stream_downgraded so the fix-1
  cast half reuses it instead of forking it.
- Two new NowPlayingState properties (quality-limit-cause int,
  quality-effective-tier string) on the existing global — no app.slint
  export change — reset on track change, mirrored to the miniplayer.
- Causes 2 (local device cap) and 3 (renderer cap) are pre-wired in the
  tooltip chain but unreachable until fixes 3/4 land; their .po entries
  ship with those fixes. The two reachable msgids land in en.po here; the
  remaining locales are populated by the i18n pass.

Validated with the slintcheck stub (full .slint tree + 8 .po catalogs).

cc
2026-07-20 14:37:20 -06:00
vicrodh 9353ed700d feat(settings): Flatpak/Snap sandbox section with copyable permission commands
Tauri parity (the sections did not survive the Slint port): a Settings
section shown only on sandboxed installs holding the one-time permission
commands with copy-to-clipboard buttons. Flatpak: exclusive-DAC D-Bus
reservation override (the user-side fix for #641's exclusive-mode failure
— the Flathub manifest cannot carry the wildcard own-name past its linter,
so the user grants it locally) and NAS/external-drive filesystem access.
Snap: the audio interface connections (qbz-player:alsa/pulseaudio/
pipewire/mpris) plus optional removable-media. New SandboxState global
seeded from qbz_app::diagnostics install_method; 14 msgids across all 8
bundled locales.

km
2026-07-20 13:47:24 -06:00
vicrodh f0ae95b431 refactor(ui): unify the track context menu into one shell-level instance
Popup windows are always inlined by the Slint compiler, so every
`track-menu := TrackContextMenu` site — TrackRow in ~15 repeaters plus
SearchTrackHero — generated its own full copy: 18 structs, ~22k lines of
the 1.9M-line generated module, for one logical menu.

Now a single TrackContextMenu lives in AppShell, fed by the new
TrackMenuState global. Openers (TrackRow right-click / "..." gutter,
SearchTrackHero more button) write every gate input and bump
`open-token`; the shell instance watches the token and shows at the
pre-computed window coordinates (same scroll-safe absolute-position math
as the drag ghost). Gate expressions and dispatch ported 1:1 from the
former per-site instances — local-album view routes play/play-track +
track-menu-action, everything else hits the central media-action handler.
Menu closes on item click and on navigation.

QueueSidebar's queue menu and TrackCard's card menu are different item
sets and stay local (single instances, no multiplication).
Spec: qbz-nix-docs/context-menu-unification/2026-07-17-single-context-menu-spec.md

km
2026-07-20 01:05:30 -06:00
vicrodh 0283a372d7 feat(discover): ArtistGridCard in the Scene view, genres as subtitle
Replace the bespoke SceneArtistCard in ArtistsByLocationView with the
app-wide ArtistGridCard (follow / play / pin / context menu, homologated
with the other artist carousels). The scene model rides the shared
SlimItem (LocationArtistItem deleted): genres land in the subtitle — the
card's second row under the name, as requested. Follow and pin states
seed from the disk-backed caches at map time; mark_artist_followed now
walks the scene model too, so toggles from any surface flip the chips
live. ArtworkTarget::LocationArtist writeback updated for the SlimItem
artwork field; media-action routed through AppShell. Cards at 200x246
(AlbumCard parity).

km
2026-07-19 23:30:54 -06:00
vicrodh c6da484481 fix(ui): highlight/animate the playing row for offline-cache tracks
Local track rows bind the local-library DB row id, but local_queue_track
publishes the Qobuz catalog id for offline copies (so the shared
resolver finds them), so NowPlayingState.track-id never matched the
row: no equalizer bars, no active highlight, and double-click restarted
the track instead of toggling (album view, Tracks tab, folder detail,
kiosk).

Carry the library row id in source_item_id_hint for qobuz_download and
re-publish it as NowPlayingState.local-track-id; TrackPlayCell,
TrackRow and KioskAlbum now match on either id — one alias, no
per-view hacks.

km
2026-07-19 16:08:02 -06:00
vicrodh e81ed911f8 feat(library): ownership-aware playlist cards, Library All genre/sort, live fav/follow sweep
PlaylistCard now reflects ownership: owned playlists show favorite only;
foreign Qobuz playlists show follow/unfollow on the overlay plus follow +
copy in the context menu. Added is-owned/is-following/is-copied to
SearchPlaylistItem, seeded from the favorites owned/followed split and
owner.id == current_user_id elsewhere. Wired the actions id-scoped across
both routing families via shared helpers (copy/follow/favorite), so a card
toggles ITS playlist instead of the open detail.

Library "All" toolbar: genre filter (own "library-all" context over the
mixed feed; albums + tracks carry a genre, non-genre kinds hide when a genre
is selected) and PlaylistView-style sort menu (re-select the active field to
flip asc/desc). Tooltips on the four source toggles.

Live favorite/follow sweep across every card surface that was missed:
album favorite now reaches the Pinned carousel, the Recently Played "view
all" page and the artist "In library" grid; artist follow reaches Home Top
Artists, label-page artists and Pinned; new set_playlist_row_following flips
the follow chip on every playlist card (incl. Pinned) after subscribe.

cc
2026-07-19 02:44:43 -06:00
vicrodh e1bdec5094 fix(library): proper All list rows, NVIDIA seam, close-to-tray crash, Go-to-album
- Rework the "All" list view to the aligned column layout of the mixtape detail
  list: artwork + hover play, title + clickable-artist subtitle, a TYPE column,
  a dedicated SOURCE column (SourceGlyph), the row QualityBadgeFull in a Quality
  column (was the uneven grid badge), favorite/follow indicator, and the per-kind
  context menu — no longer bare and out of tune.
- Fix the NVIDIA "wallpaper join" background seam: replace the float hash2 in
  ambient.wgsl + plasma.wgsl with a bit-exact integer (u32) lattice hash so the
  same cell corner hashes identically from adjacent cells on NVIDIA.
- Fix the close-to-tray "RefCell already mutably borrowed" panic introduced by
  the teardown texture-clear: defer the set via upgrade_in_event_loop so it runs
  after the window adapter releases its borrow.
- Add "Go to album" to the TrackCard context menu and the list-row menu (adds
  album-id to the feed model).

cc
2026-07-18 15:11:15 -06:00
vicrodh f484cc481f fix(library): tray-restore crash, local art/badges, LabelCard, live pins, rich list rows
- Fix the tray-restore wgpu crash (pre-existing, amplified by the app-wide
  background): clear ImmersiveState.shader-texture on RenderingTeardown so the
  first restored frame never hands a cross-instance stale texture to the new
  femtovg canvas.
- Local/Plex feed covers now decode: route the "All" artwork jobs through
  spawn_search_loads (http / local fs / Plex thumb) at all four dispatch sites;
  show the source badge on the feed's album/track cards (AlbumCard maps plain
  "qobuz" to the Qobuz logo).
- New card-sized LabelCard (200x246), used by the feed + the Favorites Labels
  tab (replaces the round FavLabelCard).
- Artist pin badge updates live in the Favorites Artists grid: add is-pinned to
  FavoriteArtistItem, stamp it, bind it in the ArtistGridCard, and flip the
  favorites artist models in set_artist_row_pinned.
- Rich "All" list view: type-aware subtitle, quality badge (album/track),
  source icon (local/Plex), favorite/follow indicator, and a per-kind context
  menu (⋯ button + right-click).

cc
2026-07-18 11:27:28 -06:00
vicrodh 83df91439c feat(library): All mixed feed, TrackCard, artist playlists + library toggle, local favorites
- Library "All": mixed feed (tracks/albums/artists/playlists/labels) sorted by
  date added, reusing the existing cards; new TrackCard; toolbar (search + source
  switches + sort); "All" added to the Library nav surfaces.
- ArtistPage: restored the curated Playlists carousel (parsed but dropped in the
  Slint port); added the catalog/library toggle backed by a per-session
  per-artist index (favorites).
- Local-library favorites layer: new SQLite store (mirrors the pinned service),
  album-level; excludes the Qobuz offline cache at write time. Feeds "All" behind
  the show-local switch.
- i18n: 3 new strings across all 8 locales.

cc
2026-07-18 01:33:23 -06:00
vicrodh 8c2f84b798 feat(shell): per-GPU selector, ambient scene in immersive, glass carousels/viewall
Smoke round 5 (owner feedback). Checkpoint.

- Preferred GPU selector (Settings > Renderer, Linux/wgpu): lists each detected
  GPU by name ("Auto" + e.g. "NVIDIA … (discrete)" / "Intel … (integrated)"),
  persisted (ui_prefs.gpu_power by device name), applied at startup via the wgpu
  adapter PowerPreference (WGPU_POWER_PREF env still wins). Fixes the iGPU running
  hot under the shader background on hybrid laptops — pick the discrete GPU.
  New gpu_adapters() enumerates+dedups adapters; gpu_power_from_prefs /
  _options / _seed_index / _value_for_index.
- Ambient metaball added to the IMMERSIVE scene picker (mode 7, wgpu-only opt-in);
  the non-shader atmosphere stays the fallback for GL/software tiers.
- Glass treatment on the surfaces that were still opaque: carousel < > nav
  buttons (6 carousels), "Filter by category" pill, "Filter by genre" buttons,
  and the PlaylistBrowseView (View-all) header band + category radio row.
- i18n: "Preferred GPU" + description + discrete/integrated + restart toast (8
  locales).

Base: 76ea5a82. Prior checkpoint: a0f5cf2b.

cc
2026-07-17 10:54:45 -06:00
vicrodh a0f5cf2bf5 feat(shell): metaball ambient + translucent bars/panels/controls, drop carousel fades
Smoke rounds 3-4 (owner feedback). Checkpoint during iteration.

Ambient (the big one): rewrote ambient.wgsl from translating gaussian blobs to a
METABALL field (r²/d² summed + smoothstep iso-surface) with 2-octave domain warp
— lobes fuse/stretch/split like an amoeba / lava lamp (the Cider motion that was
missing), faster clock, more contrast.

Surfaces made background-aware (translucent when the dynamic background is on):
- Search results view (root + toolbar), artist Network panel + its tab-row
  header (transparent to avoid double-translucent compounding), JUMP TO bar
  (JumpNavBar bar-bg passed in — can't import ShellState without a cycle).
- Discover toolbars + tab pill + BrowseHeaderTools; SearchResultsView toolbar.
- CircleAction (shared: playlist/album/artist/etc. action buttons) on-surface
  secondary fill goes glass; playlist Sort dropdown; QbzSelect closed control.
- New app-background-bar-alpha knob (0.3, env QBZ_BG_BAR_ALPHA): thin bars go
  MORE transparent than panels (surface-alpha 0.5).
- Content well: frosted tint (0.22) + 1px hairline border → distinct panel.
- Album/Artist header gradient auto-suppressed under the dynamic background
  (they clashed); header text stays light on the dynamic backdrop.
- Discover carousel edge-fade "shadows" hidden when active (Carousel /
  PlaylistCarousel / ArtistCarousel / PinnedCarousel).

Base: 76ea5a82. Prior checkpoint: 4e405338.

cc
2026-07-17 08:08:33 -06:00
vicrodh 4e40533870 feat(shell): dynamic background shows through content area + livelier ambient
Smoke round 2 (owner feedback: content area was an opaque black slab, motion
imperceptible, ambient≈blurred).

- Content-area sweep: 27 content-view roots go transparent when the app
  background is active (was opaque Theme.surface-main covering the background) —
  Home/Discover/Album/Artist/Search-adjacent/Favorites/Playlist/LocalLibrary/
  MyQBZ/managers/etc. Content well (AppShell) transparent too.
- SongCard (NPB classic), search input box, cortinilla panel go translucent so
  they stop reading as opaque slabs over the background.
- ambient.wgsl: much faster + bigger blob travel + stronger domain warp +
  brighter/more saturated so the flow is obviously moving and clearly distinct
  from the blurred-art mode.
- Defaults: dim 0.35, surface-alpha 0.5.

Checkpoint during smoke iteration (owner-requested). Base: 76ea5a82.

cc
2026-07-17 02:42:59 -06:00
vicrodh 76ea5a82bb feat(shell): app-wide dynamic album-art background ("modo Cider")
Opt-in album-art background composited behind the whole shell, wgpu-tier
only. Appearance mode picker: Off | Ambient (GPU shader) | Blurred art.

- ambient.wgsl: new calm, low-energy album-colored scene (mode 7), drifting
  mesh-gradient of the primary/secondary/accent triad, no beat/transient.
  Registered in shader_underlay SHADER_SOURCES[5] + render_frame idx math.
- state.slint: AppearanceState app-background {modes,mode-index,available,
  dim,surface-alpha}; ImmersiveState.app-shader-mode (derived 1->7);
  ShellState.app-background-active (mode>0 && wgpu && has-track).
- visualizer.rs: drain renders shader-mode when immersive OPEN, else
  app-shader-mode (7=ambient) into the same shader-texture sink.
- AppShell: background layer at root z-bottom (Ambient=shader texture,
  Blurred=ImmersiveAtmosphere) + tunable dark scrim; shell chrome surfaces
  go translucent when active; viz-should-run extended to drive the drain.
- Whole-shell translucency: Sidebar/HeaderBar/PlayerBar(Small)/QueueSidebar/
  LyricsSidebar/SidebarNowPlayingDock + AppShell content well/frame/column.
- ui_prefs: persisted `app_background` (off|ambient|blurred), String-key
  pattern; appearance-select("app-background") write-back.
- Live-tunable knobs QBZ_BG_DIM / QBZ_BG_SURFACE_ALPHA (seeded at startup)
  so the look is dialed in one smoke session without a rebuild.
- i18n: 8 locales for the picker + warning; reduce-motion / D4 (no-track ->
  opaque theme) handled via derived active flag.

Design + signed decisions: qbz-nix-docs/specs/2026-07-16-app-wide-dynamic-background.md

cc
2026-07-16 23:50:40 -06:00
vicrodh 8e57864bee fix(shell): #618 window identity/geometry + single-instance present; Plex refresh seed; wire discover/pinned state
Persist the Wayland app_id (set_xdg_app_id) and window geometry/maximized across
tray-hide and miniplayer round-trips so the pinned taskbar icon keeps grouping
and the size/position survive (#618); route the single-instance raise and MPRIS
Raise through a miniplayer-aware present() plus a dedicated D-Bus Present method.
Seed the Local Library Plex refresh button after login (not before, when the
per-user store is unbound). Also the shared wiring these features need in
main.rs / state.slint / app.slint / home.rs / artwork.rs (pinned globals + store
seed + mixed-artwork target, playlist-browse nav, sub-genre reload) and the
bundled i18n strings.

cc
2026-07-16 16:31:51 -06:00
vicrodh 897ba7128c feat(qbz-ui): settings export modal + Developer row (04 §4.2)
Add the desktop "Export settings…" surface for the qbzd daemon epic (T18):
a Developer-settings row that opens a modal with the single default-off
"Include Qobuz login and integration tokens" gate, its warning line, and a
Cancel/Export footer. Confirm() is a Rust callback (wired next). New
SettingsExportState/SettingsExportActions globals, exported from app.slint
so the Rust layer binds them, and the modal mounted in AppShell. New strings
translated across all 8 locales (en es de fr pt ru ja nl).

cc
2026-07-15 07:50:27 -06:00
vicrodh 9e7d574be0 feat(discover): Most Played Albums View-all page with search + play count
Full-page listing opened from the rail's View all (both tabs now wire show-view-all -> HomeActions.open-most-played-albums). New MostPlayedAlbumsView (mirrors RecentAlbumsView) with a search box; AlbumCardItem gains a plays field shown ONLY here (a gated line in AlbumCard). Rust: navigate_most_played_albums loads all_albums(), caches the ranked rows, renders the grid + fires artwork (MostPlayedAlbumsPage target, Qobuz vs Plex/local split); the search box re-filters the cached rows by title/artist and re-renders (mirrors the Qobuz-playlists filter). NavEntry::MostPlayedAlbums + AppShell mount + ContentView variant. Count label in 8 locales. slintcheck green; qbz-crate Rust pending a full build (30GB wall).

cc
2026-07-14 01:21:36 -06:00
vicrodh 1a7853d9c6 feat(discover): Most Played Albums carousel in Home + For You
Wires the rail (top-20 by play count) into both tabs, mirroring the favoriteAlbums pattern: HomeState/ForYouState.most-played-albums DiscoverSection; foryou::most_played_album_cards maps the ranked rows onto AlbumCard; home.rs builds it inline (local, no fetch) and For You rides the favorites branch; HomeMostPlayedAlbum/ForYouMostPlayedAlbum artwork targets + apply arms; render arms in HomeView/ForYouView (self-hide while empty); title in 8 locales. slintcheck green. View-all page (search + play count) is the next increment.

cc
2026-07-14 01:06:54 -06:00
vicrodh 41f8578b78 feat(home): sort control for the Library Albums rail
Adds a compact dropdown in the Library Albums (favoriteAlbums) row header, left of the page chevrons: Recently added (default), First added (reverse), Random (shuffle). Reorders the cached base list in Rust with no re-fetch and re-fires artwork by the new index. Also corrects the stale HomeView comment that claimed the recently-played refresh lives in the toolbar (it is a row-header button). 8 locales updated.

cc
2026-07-13 21:01:48 -06:00
vicrodh a174e06119 feat(home): auto-refresh recently-played rails + a row refresh button
Home's recently-played albums/tracks only populated on a full home load, so
tracks played mid-session never appeared until restart. Re-read the LOCAL
store into the rails when a track is recorded while Home is visible, and on
Home re-mount if it went dirty — event-driven, no polling, cheap (small JSON
read + cached artwork). Also add a manual refresh button in each recently-
played row header next to the page chevrons (moved off the top toolbar, where
it crowded the discovery tab bar).

cc
2026-07-13 13:11:43 -06:00
vicrodh c3523722f4 feat(local-library): add a Plex sync button to refresh media without restarting
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
2026-07-13 04:44:08 -06:00
vicrodh f5a8afc0f2 fix(playlist): reorder via the existing row drag gesture, not a bespoke grip
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
2026-07-13 02:45:10 -06:00
vicrodh 6e58682aa6 fix(playlist): reuse the shared drag ghost for custom-order reorder
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
2026-07-13 01:29:09 -06:00
vicrodh 2103bbd3eb feat(playlist): drag-and-drop reorder for custom-order tracks
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
2026-07-12 20:17:12 -06:00
vicrodh ec8c487564 feat(lyrics): lite-fill display pref — static active-line highlight to cut CPU
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
2026-07-12 19:38:43 -06:00
vicrodh dabf4a88c8 feat(kiosk): Now Playing — queue/history tabs, cover<->lyrics toggle with synced follow
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
2026-07-12 12:43:56 -06:00
vicrodh 6032791be4 feat(kiosk): Now Playing centerpiece + NPB miniplayer/immersive consistency
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
2026-07-10 21:38:57 -06:00
vicrodh f1d965b04e feat(kiosk): live Kiosk<->Desktop toggle in the NPB layout menu
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
2026-07-10 21:17:12 -06:00
vicrodh 222001a9eb fix(queue): wire the Remove all after and Add to playlist row actions
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
2026-07-08 18:38:27 -06:00
vicrodh 5d50158edb feat(qconnect): restore the device-name setting from Tauri
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
2026-07-08 15:26:39 -06:00
vicrodh d3b00b78ef feat(home): Recently Played Albums gets a View-all full page
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
2026-07-08 14:07:16 -06:00
vicrodh 338d20df9b fix(home): render all 13 configurator sections; own recent-albums history
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
2026-07-08 11:46:57 -06:00
vicrodh 0e55f6a24f fix(ui): macOS title bar — restart-to-apply toggle, hide-completely row hidden
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
2026-07-08 10:52:30 -06:00
vicrodh 860f49ca3e feat(favorites): album favorite action is a real toggle everywhere
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
2026-07-08 09:05:02 -06:00