Commit Graph
5226 Commits
Author SHA1 Message Date
vicrodh dbc62be90e feat(slint): Blacklist Manager view (list/search/toggle/remove/clear)
cc
2026-06-17 01:51:08 -06:00
vicrodh 67d82efd30 feat(slint): ArtistView overflow menu (create collection / artist scene / blacklist) + hidden banner
cc
2026-06-17 01:31:12 -06:00
vicrodh c90440ab8d feat(slint): featured-aware blacklist filtering on discovery/catalog surfaces; re-filter scene cache (D-FIX-c)
cc
2026-06-17 01:22:08 -06:00
vicrodh d4020b88b5 fix(slint): album-shuffle honors album-primary blacklist fallback; correct stale comment
cc
2026-06-17 01:16:26 -06:00
vicrodh 905de3dcd9 feat(slint): blacklist filter at all play/queue builders; fix AlbumDetail builder (D-FIX-b)
cc
2026-06-17 01:08:55 -06:00
vicrodh 0d724d1457 fix(slint): include composer id in blacklist row stamp (match queue predicate)
cc
2026-06-17 01:01:50 -06:00
vicrodh 6566b507de feat(slint): stamp is-blacklisted on album/playlist/favorites/suggest rows; protect local+Plex; nav-only menu on blacklisted rows
cc
2026-06-17 00:44:47 -06:00
vicrodh 95629713b3 feat(slint): TrackItem.is-blacklisted + TrackRow greyout/inert state
cc
2026-06-17 00:31:47 -06:00
vicrodh 06aba4d403 feat(slint): combined search consumes real blacklist snapshot (gated on enabled)
cc
2026-06-16 22:59:28 -06:00
vicrodh 836dd24901 feat(slint): per-user blacklist lifecycle wrapper, init on online+offline sessions
cc
2026-06-16 22:55:05 -06:00
vicrodh 0bdcf5415e feat(core): featured-aware blacklist helpers (album any-artist, track performer|composer)
cc
2026-06-16 22:26:43 -06:00
vicrodh ef93c86437 feat(app): headless artist-blacklist service extracted from src-tauri
cc
2026-06-16 22:19:36 -06:00
vicrodh a4cba30fcf feat(slint): wire album header backdrop setting
Persist the existing appearance toggle in ui_prefs and use it to enable or disable the album and artist header blur backdrops.

cx
2026-06-16 16:17:36 -06:00
vicrodh a274e602ec feat(slint): use immersive blur in album and artist headers
cx
2026-06-16 15:55:45 -06:00
vicrodh bac0e8ca76 feat(slint): HiFi wizard usability — clickable checkbox rows, queue guardrail, test hint
- Checkboxes are now whole-row clickable (new CheckRow primitive, HTML <label>
  behavior): clicking the text toggles it, not just the box. Applied to the
  welcome accept + the three Configure progress checks.
- "Use my current queue" guards an empty queue: it checks get_all_queue_tracks
  and shows "Your queue is empty — add some tracks first, or press Play test."
  instead of sitting on a dead read-back.
- Welcome step gains a "What to test with" notice recommending one track of each
  bit-depth/sample-rate (16/44.1, 24/44.1, 24/96, 24/192) so users who bring
  their own queue exercise every rate.

cc
2026-06-16 13:23:12 -06:00
vicrodh f0deed2b44 feat(slint): HiFi wizard test — explicit Prev/Next + clearer queue label
"Use my own music" -> "Use my current queue" (the button resumes the existing
queue, it doesn't open a picker). Adds explicit Prev/Next icon buttons that call
the proven NowPlayingState.previous()/next() transport, so they step through
whatever is playing — the 4 curated test tracks or the user's own queue.

cc
2026-06-16 12:42:35 -06:00
vicrodh 1e5e4d3ce5 feat(slint): HiFi wizard polish — scrollbar, progress checkboxes, FLACDalf, trigger move
From owner feedback:
- The body Flickable now has a visible ListScrollbar (wrapped in a Rectangle so
  it can overlay) — the review step with multiple DAC accordions can be tall.
- Configure step gains Tauri-style progress checkboxes (backup / config files /
  restart) so the user can track what they ran; Next is gated on all three.
- The trigger moves to the end of the BIT-PERFECT group (the wizard's whole
  point is bit-perfect) and now wears the FLACDalf (gandalf) icon, which also
  replaces the modal header icon. gandalf.svg copied into the Slint icon assets.

cc
2026-06-16 12:20:53 -06:00
vicrodh 3db43b4481 chore(slint): drop the dead StepPlaceholder now that all 6 wizard steps are real
cc
2026-06-16 10:49:57 -06:00
vicrodh ca09b35917 feat(slint): HiFi wizard review-and-apply + done steps — config generation (Slice 10)
The review step generates the copy-paste config per selected DAC (read-only —
QBZ never writes these files, Decision 2): an honest info banner, a backup
CommandBlock first, then one collapsible accordion per DAC (Decision 5 multi-DAC)
holding the three snippets ported 1:1 from the Tauri wizard — pipewire
allowed-rates, per-app bit-perfect client rules, and the WirePlumber rule that
pins node.name + rates + disables resample/channelmix — and finally an
init-aware restart CommandBlock. Rates are re-probed at generation time
(query_dac_capabilities); the filename slug is the description (fixes the old
dacShortName regex that only kept two underscore tokens). The done step lists
the config paths.

Controller: checked_dacs -> gen_configs_blocking (off-thread re-probe + template
build via line-join, no format!-brace escaping) -> apply_configs; toggle_config
flips an accordion. gen-configs fires on the select-dacs -> review transition.
2 unit tests (slugify, wireplumber pins node+rates). DacConfigRow state +
gen-configs/toggle-config actions.

cc
2026-06-16 10:47:54 -06:00
vicrodh 97fd942aa0 fix(slint): HiFi wizard test step — audio, bit-perfect read-back, track jump, UX
Fixes from owner testing of the Slice 9 playback test:

- No audio: removed the incomplete reinit_device(node) that routed output to the
  selected DAC (the proper switch is set_output_device -> reload_settings ->
  reinit; doing it half-way broke playback). The test now plays through the
  current output; route QBZ to the DAC under test in Settings > Audio.
- Bit-perfect read-back: dropped the pw-top panel (batch snapshot never reflected
  the live nodes) and enriched the N6 read-back to show the DAC's real
  /proc/asound hw_params — rate + ALSA container format (S32_LE etc.) + channels.
  That container format is the bit-perfect proof. Probe scans for the actively
  playing card, so it's DAC-agnostic (answers the multi-DAC case: one output at
  a time).
- Track jump: clicking a test row now re-sets the queue at that index via the
  working play_tracks path (play_index alone only moved the pointer). Resolved
  tracks are stashed for this.
- "Use my own music" button: starts the read-back + resumes the user's own queue
  for those who don't want the curated tracks.
- Row hover no longer changes the row height (fixed-size glyph slot; the ♪->▶
  swap can't reflow what's below).

cc
2026-06-16 10:42:18 -06:00
vicrodh 7157090d98 fix(slint): read player rate via player.state in the wizard test poll
player.get_sample_rate/get_bit_depth don't exist on Arc<Player>; the accessors
live on player.state (as the QConnect engine already does). Fixes the Slice 9
build.

cc
2026-06-15 21:29:46 -06:00
vicrodh f87f298681 feat(slint): HiFi wizard playback test — self-service with N6 read-back (Slice 9)
Replaces the impossible "watch pw-top" verify step with an in-wizard test. The
overlay modal doesn't grab the window, so playback runs underneath while the
test plays.

start-test resolves the 4 owner-curated tracks by id-hint first, then by
"artist title" search if the id 404s (a pulled license — never raw-id-only),
routes QBZ output to the selected DAC (reinit_device), and plays them via the
existing play_tracks path. A 1.5s Slint Timer drives poll-test while playing:
it reads the rate QBZ REQUESTED (player.get_sample_rate — honest) and the DAC's
REAL negotiated rate (N6 / negotiated_stream_rate over /proc/asound), showing a
green check only when they actually match. Read-only; protected audio path
untouched.

State: reuses the test-* fields from Slice 5; adds a poll-test callback and a
TEST_NODE thread_local (no extra model). 3 unit tests already cover the pure
seed-matching/formatting helpers.

cc
2026-06-15 21:24:52 -06:00
vicrodh 0ad789fc12 feat(slint): HiFi wizard select-dacs step — auto-detect + escape hatch (Slice 7)
Replaces the manual node.name paste with auto-enumeration. Controller
detect_blocking() enumerates via the Slice-0 pw-dump path, projects each sink to
a DacCandidate (looks_like_dac = is_hardware && bus in {usb,pci}), and probes
rates (Slice-2 query_dac_capabilities) for the likely DACs; apply_candidates
pre-selects them and flips has-enumeration off when empty. toggle_dac flips a
VecModel row + recomputes the Next gate; validate_manual is a 1:1 port of the
Tauri validateNodeName/detectDacType (+3 unit tests).

UI step 2: circular SelectionCheckbox rows with a bordered bus label (no pill),
a default star and the probed rate list; a "Can't see your DAC?" disclosure
reveals the two wpctl CommandBlocks + a validated node.name input (shown
unconditionally when nothing enumerated). Next gates on any-dac-selected ||
manual-valid; enumeration fires on the check->dacs transition. Read-only.

cc
2026-06-15 21:16:55 -06:00
vicrodh 9c5febdaa8 feat(slint): HiFi wizard check step — stack health, distro/init override, sandbox (Slice 6)
Wires the check step on top of the Slice-0/2/6 backend: a health verdict
(success/warning WarningBanner — new `success` variant), a per-missing-piece
CommandBlock with copy, and TWO overridable dropdowns — distro (package
manager) and init system (service commands), which are orthogonal so a Gentoo
box on systemd vs OpenRC gets the right restart command.

New `src/dac_wizard.rs` controller: open() runs the probe off-thread
(spawn_blocking → upgrade_in_event_loop), caches it, and recompute() rebuilds
remediations from the cached probe + the current distro/init selections.
Init-aware restart_cmd (systemd/OpenRC/runit/s6/dinit). NixOS special-cased to
a declarative configuration.nix block.

Sandbox honesty: in Flatpak/Snap the host probes are blind, so the check step
shows an info banner + reference setup commands (full stack incl. pipewire-alsa
+ restart) for the chosen distro/init instead of a false health verdict.

main.rs wires on_open/on_set_distro/on_set_init/on_copy_command; the Audio
settings trigger calls DacWizardActions.open().

cc
2026-06-15 21:04:23 -06:00
vicrodh 2b52a3c01c feat(audio): init override metadata + Flatpak/Snap sandbox-aware host detection
InitSystem gains ALL/index/label so the wizard can offer a manual init override
(for sandboxes and for generating commands for another machine).

Sandbox awareness: detect_sandbox() (/.flatpak-info, $SNAP). Inside a sandbox
/etc/os-release is the RUNTIME's (Flatpak freedesktop-sdk) or the snap base's,
not the user's distro, and /run + /proc/1 reflect the sandbox — so detect_distro
now reads the host-exposed os-release first (Flatpak /run/host/os-release, Snap
/var/lib/snapd/hostfs/etc/os-release, then /etc), and detect_init returns
Unknown in a sandbox (the host init is invisible; the wizard's override decides).

cc
2026-06-15 20:41:52 -06:00
vicrodh 93da676fd7 feat(audio): runtime init-system detection + antiX/Artix/NixOS distros (wizard)
Decouple service commands from the distro: the init system is detected at
RUNTIME (detect_init: /run/systemd/system, /run/openrc, /run/runit, /run/s6-rc,
/run/dinitctl, then /proc/1/comm), because it is orthogonal to the distro —
Gentoo runs OpenRC *or* systemd, Debian runs systemd or sysVinit/runit (antiX).
So install commands key off the distro (package manager) and service/restart
commands key off the detected init.

Adds the systemd-free derivatives Antix (Debian) and Artix (Arch) — matched
BEFORE their parent family since their ID_LIKE would otherwise misclassify them
and emit systemd commands — plus NixOS (declarative). InitSystem enum +
parse_init_from_comm. 6 health tests (incl. systemd-free-beats-parent + init
classification).

cc
2026-06-15 20:31:28 -06:00
vicrodh b266fb2ed2 feat(audio): audio_stack_health probes + distro detection (Slice 6 backend)
`qbz-audio::health` for the wizard's check step: audio_stack_health() probes
wireplumber (systemctl OR `pgrep` fallback so non-systemd inits like
Gentoo/OpenRC and Void/runit don't read as down), pw-dump, the ALSA->PipeWire
bridge PCM (`aplay -L` `^pipewire$` — needed for CPAL PLAYBACK, not just
enumeration), pactl, and any-sink presence. detect_distro() classifies
/etc/os-release (ID then ID_LIKE; strips single AND double quotes — Gentoo
single-quotes its value). Distro enum carries the dropdown order + labels.

Pure parse_distro + 4 unit tests (Ubuntu, Pop/EndeavourOS via ID_LIKE,
fedora/arch/suse/gentoo+single-quote/void/other, index round-trip). Read-only.

cc
2026-06-15 20:13:39 -06:00
vicrodh 9bb392c262 feat(slint): DAC wizard shell — 6-step modal + state + trigger (Slice 5)
DacWizardState/DacWizardActions/DacCandidateRow in state.slint (re-exported in
app.slint). DacWizardModal: scrim + 620px card + dot/label stepper rail +
Flickable body with if-step blocks + footer Back/Close/primary. Navigation is
inline state mutation (no Rust yet); the Actions callbacks (copy-command/
run-detect/query-caps/start-test) are declared for Slices 6-9. welcome step is
real (disclaimer + accept checkbox gating Start); steps 1-5 are titled
placeholders. Mounted in AppShell; "Open Wizard" 160x34 trigger added to Audio
settings, gated on PipeWire.

cc
2026-06-15 20:09:35 -06:00
vicrodh 5e57db7a0c feat(slint): CommandBlock + WarningBanner primitives (HiFi wizard Slice 4)
CommandBlock: surface-elevated copy-paste card (Radius.sm, no pills) with an
optional caption, a monospace wrapping command body and a copy button that
flips to a green check for 2s (Timer). Read-only — the copy() callback is wired
to the clipboard by the consumer; nothing is executed.

WarningBanner: info/warning/error variants (tone drives icon + 10%/30% alpha
bg/border, matching the existing inline banners). Adds lucide copy.svg.

cc
2026-06-15 20:09:35 -06:00
vicrodh f08943ad27 feat(audio): headless query_dac_capabilities for the Slint wizard (Slice 2)
Extract the DAC capability assembly out of the Tauri-only
`v2_query_dac_capabilities` into `qbz-audio::dac_capabilities` so the Slint
wizard can probe a DAC's real supported sample rates + pretty description
without a Tauri command. The detection itself already lived in this crate
(`PipeWireBackend::get_sink_supported_rates` -> /proc/asound, with the
`get_device_supported_rates` ALSA-direct fallback); this just assembles the DTO
frontend-agnostically and benefits from the Slice-0 pw-dump-robust enumeration
for the description.

Formats stay nominal (S16/S24/F32) and are documented as NOT probed — honest
port of the prior behavior, not a fake measurement. Pure `assemble` helper +
3 unit tests. Read-only.

cc
2026-06-15 19:38:51 -06:00
vicrodh 1151c96e09 feat(audio): N6 DAC negotiated-rate probe via /proc/asound hw_params (Slice 8b)
The playback test needs the rate the DAC is ACTUALLY clocked at, not the rate
QBZ requested (`player.state.get_sample_rate()` only proves "QBZ asked for X").
Ground truth on Linux is `/proc/asound/cardN/pcm*p/sub0/hw_params` (`rate:` line
while a stream is open, `closed` when idle). The card number is resolved from
the PipeWire node.name via `pw-dump` (robust — no pactl/pipewire-pulse).

New `qbz-audio::dac_probe`: `NegotiatedRate { sample_rate, format, channels }`,
pure `parse_hw_params` + `parse_alsa_card_for_node` helpers, and the composed
`negotiated_stream_rate(node_name)`. Read-only — never opens/reconfigures a
stream, protected bit-perfect path untouched. 4 unit tests (active 24/192
fixture, closed/empty/no-rate, card resolution from int and string props).

cc
2026-06-15 19:36:00 -06:00
vicrodh 381eafdf21 feat(audio): native pw-dump enumeration with pactl fallback (HiFi wizard Slice 0)
Enumerate PipeWire sinks via `pw-dump` (the daemon's native socket) as the
primary path, falling back to the existing `pactl` parser only when pw-dump is
absent or empty. pw-dump needs only `pipewire-bin` — not `pipewire-pulse` (the
Pulse server pactl talks to) nor `pipewire-alsa` (the bridge PCM CPAL relies
on), which are commonly missing on PipeWire-only Ubuntu and produced the empty
device list. As a bonus it yields the exact `alsa_output.*` node.name that the
HiFi wizard otherwise asks the user to paste by hand.

Discovery code only: `create_output_stream` and the protected sample-rate
passthrough path are untouched. max_sample_rate stays None from pw-dump because
its EnumFormat reports the currently negotiated rate, not the device maximum —
the /proc/asound capability probe remains the honest source.

Pure `parse_pw_dump_sinks` helper + 2 unit tests (fixture: USB DAC + PCI card +
filtered-out capture source; garbage/empty JSON).

cc
2026-06-15 19:31:37 -06:00
vicrodh 30bd959bf7 feat(album): multi-disc "Disc N" headers with per-disc context menu
Album and local-album track lists render a "Disc N" header above the
first track of each disc when the album spans more than one disc
(media_number-driven; single-disc albums render flat), matching Tauri.
Each header carries a context menu (new — Tauri has none) with Play all /
Shuffle / Add to queue / Play next scoped to that disc's tracks only; the
whole-album header buttons are unchanged.

The menu lives in a DiscHeaderMenu wrapper because Slint rejects a
PopupWindow placed directly under a for/if delegate — wrapping it makes
the popup a non-root child (same pattern as TrackRow + TrackContextMenu).
Every TrackItem constructor gains disc-header-number: 0; artist
top-tracks (a flat cross-album list) default the disc to 1.

cc
2026-06-15 19:19:21 -06:00
vicrodh 2e337f6039 feat(discover): polish Qobuz Playlists cards with contain covers and category filter
Single-cover Discover playlist cards now letterbox the cover with
image-fit: contain over its dominant colour (computed host-side from the
decoded cover), matching Tauri, and show the first tag as an accent
category subtag. Add a multi-select category filter dropdown (sm toolbar
variant, no pills per ADR-008) that re-filters the cached playlists row
client-side; an empty selection shows everything. Raise the playlists
cap from 18 to 100 so the filter operates on a full set.

cc
2026-06-15 19:19:01 -06:00
vicrodh 46f455629e feat(slint/discover): DailyQ/WeeklyQ primary track_to_analysed path
Bring the Slint mix generation to Tauri parity: build a track_to_analysed
payload from up to 9 spread-sampled seeds (pick_spread -> get_track ->
{track_id,artist_id,genre_id,label_id}, drop artist_id==0) for the PRIMARY
/dynamic/suggest algorithm, limit = max(1, 50 - analysed), and fall back to
the empty-analysis call when the primary returns nothing. The listened-seed
build is factored into mix_listened_seed_ids (recents + favorites, ~120,
Qobuz-only) as a seam the reco-store swap (b3) replaces. Logs seed/analysed/
result counts so an empty mix is diagnosable.

cc
2026-06-15 16:47:30 -06:00
vicrodh f3982c12ed fix(slint/discover): rename reserved 'row' property in ConfigRowView
Slint reserves 'row' as a GridLayout attached property, so the configurator
modal's 'in property <ConfigRow> row;' failed to compile ("Cannot override
property 'row'"). Rename it to 'entry' — the declaration, the seven
root.row.* accesses, and the for-binding pass-through.

cc
2026-06-15 16:47:30 -06:00
vicrodh 314ed8e6ba feat(reco): shared headless reco-store module (qbz-app)
Cleanroom port of the Tauri reco-store into qbz-app::settings::reco_store
(frontend-agnostic, no tauri::State, ADR-006) so the Slint frontend can seed
DailyQ/WeeklyQ, the WeeklyQ 7-day window, and top-genres. Opens the SAME
per-user reco/events.db that Tauri uses (cross-frontend history share); ports
the reco_events schema + indexes, log_play/favorite_event, get_recent and
get_favorite track ids, a windowed get_recent_track_ids_since, get_top_genres,
get_home_seeds, and the train() scorer verbatim. 6 headless tests green.
Tauri's own reco_store is left as-is (rewiring it is a deferred cleanup).

cc
2026-06-15 16:00:41 -06:00
vicrodh ee83734fba feat(discover): add get_dynamic_suggest_full carrying track_to_analysed
Tauri's DailyQ/WeeklyQ /dynamic/suggest seeds a track_to_analysed payload
(~9 resolved {track_id,artist_id,genre_id,label_id} tuples) for the PRIMARY
mix algorithm; the empty-analysis call is only its fallback. The Slint port
only ever sent the empty-analysis path. Add a TrackToAnalyse model
(qbz-models) and a get_dynamic_suggest_full method on the qobuz client and
qbz-core that carries the payload; the existing get_dynamic_suggest now
delegates with an empty analysis (behaviour unchanged). Plumbing only — the
mix.rs build of the analysis tuples + the two-call fallback lands next (a2).

cc
2026-06-15 15:50:41 -06:00
vicrodh 76406aa746 fix(slint/discover): seed DailyQ/WeeklyQ with Qobuz-only track ids
DailyQ/WeeklyQ build their /dynamic/suggest seed from recently-played track
ids. Recently-played now includes local and Plex tracks whose ids are not
Qobuz catalog ids — numeric Plex rating keys even passed the u64 parse, so
they reached Qobuz and the mix request failed. Filter the seed to Qobuz (and
legacy pre-source) tracks before parsing, and skip the request when no Qobuz
seeds remain. Local tracks stay visible in the Recent rows.

cc
2026-06-15 13:58:39 -06:00
vicrodh 5fd3456a47 fix(slint/discover): give the configurator section list an explicit height
The section list used vertical-stretch inside a panel sized to
preferred-height; a stretch child there collapses to ~0 (a Flickable's
intrinsic preferred-height is ~0 — the documented Slint trap), so the list
was near-invisible. Size it to its content capped at the window minus the
modal chrome; the Flickable scrolls when the rows exceed it.

cc
2026-06-15 13:57:53 -06:00
vicrodh 2035cd125e fix(slint/discover): keep Qobuzissimes in the Home section cache
The configurator offers Qobuzissimes on the Home tab (in DEFAULT_PREFS.home,
off by default), but Slice 2d had removed it from the Home cache, so enabling
it rendered nothing (descriptors_for found no backing data and dropped it).
Visibility is now governed by the prefs (off by default), so keep the data in
the Home cache pool; enabling Qobuzissimes on Home now renders.

cc
2026-06-15 13:57:53 -06:00
vicrodh 3c4f36e65c fix(slint/discover): show skeleton during initial Home load, not placeholders
The prefs-driven Home skeleton gated on home-sections/editor-sections being
empty, but the descriptor list already carries the non-album rows at seed
time, so the gate never fired and the user saw empty-state placeholders
during the initial fetch instead of the skeleton. Gate the skeleton on
HomeState.loading alone, and suppress the Recently-Played placeholders while
loading, so only the skeleton shows during the fetch (1:1 Tauri).

cc
2026-06-15 13:57:53 -06:00
vicrodh aa5224abe9 feat(slint/discover): section configurator modal and gear trigger
Add the "Customize" modal that drives the Slice 5 prefs: reorder + show/hide the
active tab's Discover sections, live-persisted (no Save button).

- DiscoverConfigModal.slint: scrim + centered panel (CreatePlaylistModal idiom),
  title + close-X, an amber triangle-alert perf-warning banner, an "{} of {}
  enabled" count, a scrollable section list, and a Reset-to-defaults footer
  pinned left. ConfigRowView = [QbzCheckbox] [label] [up] [down]: the row tap and
  the ADR-008 checkbox both fire toggle-section; the 28px ghost ReorderButtons
  fire move-section and dim at the list boundary (index 0 / last). Section labels
  come from DiscoverState.config-rows (resolved in Rust); chrome strings are @tr
  literals.
- HomeView.slint: bind the previously-dead gear to
  DiscoverActions.open-configurator() (opens for the active tab).
- AppShell.slint: mount DiscoverConfigModal after the other modals so it sits on
  top of them (ADR-009 z-stack by declaration order).

cc
2026-06-15 13:40:46 -06:00
vicrodh a06f06e9d8 feat(slint/discover): prefs-driven render loop for Home and For You
Replace the fixed Discover layouts with the prefs-driven descriptor repeaters
that read DiscoverState's ordered ENABLED section lists, so the configurator's
visibility + reorder choices drive what renders.

- HomeView.slint: drop `for section in HomeState.sections` + the 5 hardcoded
  typed blocks; render one repeater over the active tab's descriptor list
  (editor-sections when active-tab == "editorPicks", else home-sections). The
  single-root delegate dispatches on desc.id: generic album carousels bind the
  embedded desc.section; qobuzPlaylists / continueListening / recentlyPlayedAlbums
  / mostStreamed(slim) bind HomeState fields and keep their length gates +
  placeholders. `if forYou: ForYouView` stays a static sibling.
- ForYouView.slint: replace the fixed if-sequence with a descriptor repeater
  over foryou-sections; each arm reads the typed ForYouState field by id and
  keeps its own length gate (qobuzMixes excepted — always rendered). The
  skeleton stays a static sibling. essentialsByGenre has no arm (Slice-2c-blocked;
  Rust drops it from the list).
- home.rs: select_tab writes DiscoverState.active-tab (single source of truth,
  alongside the kept HomeState.active-tab pill bindings) and re-renders the
  active tab from the cache via the descriptor lists; apply_home pushes the
  descriptor lists once the cache is populated.
- main.rs: the initial discover load fires album-section artwork via the
  descriptor-targeted jobs returned by select_tab (DiscoverSectionAlbum), spawned
  on the UI thread; slim-grid / recent-album / playlist covers still target the
  HomeState fields the fixed arms render.

Build-time verifications to confirm (documented in the .slint comments): the
for-over-ternary-model form compiles, and an empty delegate does not double the
outer 40px spacing (Rust already drops data-empty album ids — policy b).

cc
2026-06-15 13:40:46 -06:00
vicrodh 0bb0f73699 feat(slint/discover): DiscoverState global + prefs controller + load/persist
Add the Slice 5 foundation for the Discover section configurator: a single
DiscoverState/DiscoverActions global pair owning both the prefs-driven render
descriptor lists and the modal payload, plus a thin frontend controller over
the headless qbz_app discover_prefs store.

- state.slint: SectionDescriptor + ConfigRow structs and the DiscoverState /
  DiscoverActions globals (active-tab lives here; render loop + modal read it).
- app.slint: re-export the new globals/structs so the Rust layer can bind them.
- discover_prefs.rs: per-user store lifecycle (init_for_user/teardown mirror
  fav_cache), in-memory authoritative prefs, descriptor + config-row push
  helpers, id->kind/label maps, and toggle/move/reset/open/close handlers
  (mutate -> persist -> re-push -> re-render, re-firing artwork for the active
  Home/Editor tab). Labels resolve in Rust (English today) since Slint @tr needs
  a literal key and the gettext pipeline is unwired.
- home.rs: add the SectionData.id key; build the Home/Editor ordered descriptor
  lists from the cached section data (album ids embed their DiscoverSection,
  fixed ids carry an empty section); rerender_active_tab + descriptor artwork
  jobs. Empty album sections are dropped (policy b); placeholder-bearing Home
  sections are kept and gated in the view.
- artwork.rs: DiscoverSectionAlbum target so covers paint on the descriptor
  list (DiscoverState.home-sections / editor-sections) rather than HomeState.
- main.rs/auth.rs: mod + seed at both shell entries + per-user init/teardown at
  every session-activation point + the 5 DiscoverActions callback registrations.

Descriptor lists are pushed but not yet rendered (the views still draw the old
way); no behavior change until the render loop lands.

cc
2026-06-15 13:40:46 -06:00
vicrodh ab330d85de fix(slint/discover): open playlist on body-click in Search and Label
The new ("playlist","play") media-action arm gave these cards a body-click
action for the first time. Without body-opens they defaulted to play, so a
playlist card body-click played instead of opening (and before the arm
existed, did nothing at all). Set body-opens:true on the Search carousel,
Search grid, and Label carousel so body-click opens the playlist while the
overlay button plays — matching the album-card convention and Tauri.

cc
2026-06-15 13:07:40 -06:00
vicrodh c2e5fce150 style(slint/discover): enlarge Spotlight hero avatar to 140px
The Spotlight hero portrait was 96px; enlarge to 140px (border-radius
70px to stay circular) for parity with Tauri. Layout-safe: the inner
Image is 100% image-fit:cover, the fallback glyph re-centers, the hero
HorizontalLayout has no fixed height so it grows, and the page Flickable
absorbs the vertical growth.

cc
2026-06-15 12:58:17 -06:00
vicrodh 6d33b94cf7 fix(slint/discover): similarAlbums title shows "Similar to {seed}"
The More From Your Library / similarAlbums row was hardcoded "More From
Your Library"; Tauri titles it "Similar to {seed}" where seed is the album
the suggestions are seeded from (discovery.similarTo).

Thread a seed_title through apply_more_from_library: the common case passes
the most-recent album title (recents_seed_title), the no-recent-history
fallback passes the first favorite album title. Empty seed falls back to
"More From Your Library" so the row is never titleless. Title stays a Rust
data string (rendered raw at Carousel.slint), matching the other For You
section titles.

cc
2026-06-15 12:57:58 -06:00
vicrodh 26acebc977 fix(slint/discover): drop qobuzissimes from Home default tab
Qobuzissimes was pushed into the Home sections vec unconditionally, so
it always rendered on the Home tab — but Tauri's home default has it OFF.
Editor's Picks keeps it (push_section_ref, correct). Remove the Home push;
borrow-safe (containers.qobuzissims is now consumed only by the editor
push_section_ref).

cc
2026-06-15 12:56:56 -06:00
vicrodh 613a3f009b feat(slint/discover): favoriteAlbums row in For You
Add the missing For You "Favorite Albums" carousel (Tauri's
favoriteAlbums), the user's favorited albums capped at 18 in favorite
order, self-hiding when empty. Reuses the album fav-fetch already done in
albums_branch — no new endpoint.

- state.slint: ForYouState.favorite-albums: DiscoverSection.
- foryou.rs: build_favorite_albums (cap 18, unfiltered) + apply_favorite_albums
  (mirrors apply_rediscover); wired into albums_branch right after fav_albums
  resolves, painting in the same layer as rediscover/radio.
- artwork.rs: ForYouFavoriteAlbum { index } variant + apply arm.
- ForYouView.slint: mount the Carousel after Your Top Artists, before More
  From Your Library, gated on favorite-albums.albums.length > 0.

Titled "Favorite Albums" (not Tauri's literal "More From Your Library")
to avoid the label collision with similarAlbums.

cc
2026-06-15 12:56:33 -06:00