Commit Graph
100 Commits
Author SHA1 Message Date
vicrodh 903acdb044 feat(qbzd): persist + drain ListenBrainz offline queue in scrobble engine
A failed `submit_listen` now writes the listen to the SHARED
`ListenBrainzCache.listen_queue` (daemon-root `cache/listenbrainz_v2.db`, the
same schema the desktop uses) instead of being lost. The scrobble task's loop
becomes a biased `select!` between the CoreEvent bus and a 120s drain interval
(the interval also fires once at task start), so a queue built up while offline
is retried oldest-first and cleared on reconnect, stopping at the first failure
and resuming on the next tick. Mirrors `qbz::scrobble::flush_listenbrainz_queue`:
the rusqlite Connection is opened inside a `spawn_blocking` per op and never held
across an await. The daemon uses its own `qbzd` cache root — it never touches the
desktop's dirs.

cc
2026-07-19 20:17:19 -06:00
vicrodh 09a46b694d feat(qbzd): add Scrobbler setup-TUI screen (Last.fm / ListenBrainz)
Add an eighth setup-TUI section (`qbzd setup`) for scrobbler auth, mirroring
the Account browser-login methodology: the App suspends the alt-screen and runs
the CLI connect flow on the plain terminal, then reloads the screen's settings
snapshot on resume. Last.fm prints an authorize URL and waits for Enter (the CLI
verb owns that); ListenBrainz prompts for a pasted user token, then hands it to
the CLI verb. Both write the canonical ScrobblerSettingsStore, so enable/disable
stays a CLI verb and the screen only shows connection state.

Scrobbler is appended LAST in SCREENS so no existing section index or number-key
jump shifts; the number-key jump range widens 1-7 -> 1-8. The D7 six-screen cap
was already lifted for FB4's Wizard; this is the second owner-sanctioned break.

cc
2026-07-19 20:13:39 -06:00
vicrodh 1c5c44b904 feat(qbzd): scrobble-on-play engine — Last.fm / ListenBrainz (CONSOLE)
The daemon now scrobbles what it plays. A CoreEvent-bus subscriber
(scrobble_engine, spawned in run() next to queue-persist):
- on TrackStarted -> "now playing" to each active provider (LastFm
  update_now_playing / ListenBrainz submit_playing_now);
- scrobbles ONCE when the position crosses the threshold from
  qbz_app::scrobble_timing::scrobble_delay_secs (Last.fm's half-or-4-min rule)
  -> LastFm scrobble / ListenBrainz submit_listen, best-effort + logged;
- re-reads the canonical ScrobblerSettingsStore on each track, so `qbzd scrobble
  login/enable/disable` take effect on the next track with no reload signal;
- holds no Arc<AppRuntime>; aborted on shutdown alongside queue-persist.

Album is dropped when empty / "Unknown Album". Pure helpers (due, album_opt)
are unit-tested. The ListenBrainz offline queue (ListenBrainzCache) is a
follow-up — this slice submits best-effort.

Verified: cargo test -p qbzd --bin qbzd -> 278 passed, 0 failed.

cc
2026-07-19 19:22:50 -06:00
vicrodh 17de908225 feat(qbzd): scrobbling auth foundation — Last.fm / ListenBrainz (CONSOLE)
The connect flows, same methodology as `qbzd login`:
- `scrobble login lastfm`: LastFmClient::get_token prints an authorize URL, the
  user approves on last.fm, get_session exchanges it for a session key.
- `scrobble login listenbrainz --token <T>`: ListenBrainzClient::set_token
  validates a pasted user token (like `login --token`).
- `scrobble status` · `scrobble enable|disable <provider>`.

Credentials land in the CANONICAL scrobbler store
(qbz_app::settings::scrobblers::ScrobblerSettingsStore — SQLite at the daemon
data root), the SAME store the desktop uses and the one the settings bundle
export/import already carries, so scrobbler creds travel with `settings export`.
Secrets registered with qbz_log; a running daemon is nudged to reload.

- Cargo.toml: qbz-integrations path dep (already in-tree via qbz-core).
- cli/scrobble.rs: the verbs. main.rs: Scrobble subcommand group.

Verified: cargo test -p qbzd --bin qbzd -> 276 passed, 0 failed.

NOTE: lands CREDENTIALS + auth only. Scrobble-on-play (a daemon task on the
CoreEvent bus: update_now_playing on TrackStarted, scrobble past
scrobble_delay_secs, ListenBrainz submit + offline queue) is the next slice —
it wires the playback driver. The setup-TUI Scrobbler screen is also pending.

cc
2026-07-19 19:11:43 -06:00
vicrodh 620af9d75d feat(qbzd): add lyrics verb + GET /api/lyrics (CONSOLE)
- qbz-core: thin QbzCore::get_lyrics wrapper over QobuzClient::get_lyrics
  (additive; lets qbzd avoid the client() escape hatch).
- api/lyrics.rs: GET /api/lyrics?id=<TRACK_ID|current>; flattens the tagged
  QobuzLyricsDocument union to {track_id, synced, lines:[{time_ms?, text}]}.
  No-lyrics = 200 with empty lines, never 404. Auth-gated.
- cli/lyrics.rs: `qbzd lyrics [TRACK_ID] [--synced] [--json]`; --synced prefixes
  each line with its [mm:ss.cc] LRC timestamp.
- api/mod.rs: P1_ROUTES 24->25; route + test. main.rs: Lyrics subcommand.

Verified: cargo test -p qbzd --bin qbzd -> 276 passed, 0 failed.

cc
2026-07-19 19:04:14 -06:00
vicrodh 7fc292de27 feat(qbzd): add playlist recommendations — POST /api/reco/playlist (CONSOLE)
Enables QBZ's Suggested-Songs engine headlessly and Slint-free. The
artist-vector store is a CACHE the engine builds on demand from MusicBrainz +
Qobuz — no listening history needed, so the personalized (history-based)
recommendations stay out of scope (they would need play-recording plumbing).

- Cargo.toml: qbz-reco path dep (already in-tree via qbz-core; direct dep only
  exposes the ArtistVectorStore name).
- daemon.rs: open ArtistVectorStore::open_at(daemon-data-root) after core init
  and set_artist_vectors — mirrors qbz/src/auth.rs, session-independent,
  best-effort (suggestions still work un-cached if the open fails).
- api/reco.rs: POST /api/reco/playlist {playlist_id} (server seeds the engine
  with the playlist's distinct artists + excludes its tracks) | {artists,
  exclude} | {limit}. generate_playlist_suggestions; auth-gated. Documents the
  single-thread cost (MusicBrainz-resolve is rate-limited).
- cli/reco.rs: `qbzd reco playlist <ID> [--limit --ids --json]`; --ids emits
  suggested track_ids (pipe into `queue add -` / `playlist add`).
- api/mod.rs: P1_ROUTES 23->24; route + test. main.rs: Reco subcommand group.

Verified: cargo test -p qbzd --bin qbzd -> 274 passed, 0 failed.

cc
2026-07-19 16:56:49 -06:00
vicrodh 1a50f4c1b1 feat(qbzd): add discover — Discover rails, no recommendations (CONSOLE)
GET /api/discover?section= exposes the editorial Discover rails so a third
party can replicate the QBZ Discover page WITHOUT the personalized
recommendation engine (per-user reco/play-count stores the daemon never opens):
  index · most-streamed (Qobuz "most played") · new-releases · press-awards ·
  qobuzissims · album-of-the-week · ideal-discography · playlists[?tag] · tags ·
  release-watch[?release_type=artists|labels|awards] · featured[?type]. Optional
  ?genre=ID,ID scopes a section by genre.
- api/discover.rs: one route over get_discover_index / get_discover_albums /
  get_discover_playlists / get_playlist_tags / get_release_watch /
  get_featured_albums; typed serde shapes verbatim; auth-gated.
- cli/discover.rs: `qbzd discover [SECTION] [--genre --tag --release-type
  --type --limit --ids --json]`.
- api/mod.rs: P1_ROUTES 22->23; route + test. main.rs: Discover subcommand.

Verified: cargo test -p qbzd --bin qbzd -> 274 passed, 0 failed (with the
playlist-CRUD commit below it).

cc
2026-07-19 16:56:49 -06:00
vicrodh df4f29a50e feat(qbzd): add playlist CRUD — create/edit/rm/add/remove (CONSOLE P2)
- api/playlist.rs: POST /api/playlist/create, /update, /delete, /tracks/add,
  /tracks/remove. tracks/remove takes PLAIN track ids and the daemon resolves
  them to per-playlist playlist_track_id ROW ids (the row-id trap) via
  get_playlist before remove_tracks_from_playlist — clients never touch row ids.
- cli/playlist.rs: `playlist create/edit/rm/add/remove` (rm requires --yes; add
  reads `-` from stdin; edit --public/--private mutually exclusive).
- api/mod.rs: P1_ROUTES 17->22; routes + test. main.rs: PlaylistCmd variants.

Pending: `cargo check -p qbzd` — box at 7GB with a qbz_ui build running;
compile deferred to avoid an OOM freeze (verified next in a memory window).

cc
2026-07-19 16:32:29 -06:00
vicrodh 4437133c49 feat(qbzd): add art verb — current-track cover art (CONSOLE)
`qbzd art` prints the now-playing track's artwork_url (already carried on
GET /api/now-playing — an unauthenticated Qobuz CDN URL); `qbzd art --save
PATH` downloads it (notification icons, terminal viewers). A pure-ish client
verb: no new route (reads the shipped now-playing). A daemon-served
GET /api/artwork/current (302) is deferred until a daemon-only client needs it.

Verified: cargo test -p qbzd --bin qbzd -> 269 passed, 0 failed.

cc
2026-07-19 16:28:03 -06:00
vicrodh 9ab41fbe8c feat(qbzd): add fav (list/add/remove) + playlist (list/show) (CONSOLE P1)
- api/fav.rs: GET /api/favorites (paged read), POST /api/favorites/add|remove.
  The contract is SINGULAR (track|album|artist) everywhere; the server
  pluralizes for get_favorites, hiding the plural-read/singular-write trap.
  `add {current:true}` favorites the now-playing track.
- api/playlist.rs: GET /api/playlists (collection), GET /api/playlist?id=
  (one playlist, full track list). Reads only in this slice; CRUD deferred.
- cli/fav.rs + cli/playlist.rs reuse browse's generic render/collect_ids
  (made pub(crate)); playlist list has its own id/name/count renderer.
- api/mod.rs: P1_ROUTES 12->17 (+favorites x3, playlists, playlist); routes +
  test. main.rs: Fav/Playlist subcommand groups.

Verified: cargo test -p qbzd --bin qbzd -> 269 passed, 0 failed.

cc
2026-07-19 16:28:03 -06:00
vicrodh ecc47b6d23 feat(qbzd): add shuffle/repeat + queue move/jump/stop-after (CONSOLE)
Completes transport + queue parity from the CLI.

- api/playback.rs: POST /api/playback/shuffle {on|off|toggle} and
  /api/playback/repeat {off|all|one} (set_shuffle / toggle_shuffle /
  set_repeat_mode; no auth — queue-mode toggles, state surfaces in now/status).
- api/queue.rs: POST /api/queue/move {from,to} (move_track); /api/queue/jump
  {index} (play_index + the shipped start ritual, auth-gated — a click-to-play
  row, never a bare cursor move); /api/queue/stop-after {track_id|current|off}
  (set_stop_after / clear_stop_after).
- cli/mode.rs: `shuffle`, `repeat`. cli/queue.rs: `queue move/jump/stop-after`
  (1-based positions translated to 0-based at the boundary, like `queue remove`).
- api/mod.rs: P1_ROUTES 7->12; routes + test. main.rs: subcommands.

Verified: cargo test -p qbzd --bin qbzd -> 264 passed, 0 failed.

cc
2026-07-19 15:36:28 -06:00
vicrodh d819d94ab5 feat(qbzd): add radio (seed-and-go) + resolve (CONSOLE)
- api/radio.rs: POST /api/radio {artist_id|track_id|album_id} — qobuz mode
  (get_radio_artist/track/album, zero local state). Reuses the new shared
  play::start_resolved for the queue-replace + cold-start ritual (protected
  audio untouched). Smart local-pool mode deferred (needs RadioDb::open_at
  plumbing through QbzCore — a shared-core change).
- api/play.rs: extract pub(crate) start_resolved (shared by play + radio).
- cli/radio.rs: `qbzd radio artist:ID | track:ID | album:ID`.
- cli/resolve.rs: `qbzd resolve <URL>` -> kind:ID token; pure client, no
  daemon and no network (qbz_qobuz::link_resolver).
- api/mod.rs: P1_ROUTES 6->7 (+POST /api/radio); route + test. main.rs:
  Radio + Resolve subcommands.

Verified: cargo test -p qbzd --bin qbzd -> 264 passed, 0 failed.

cc
2026-07-19 15:06:38 -06:00
vicrodh ba2f626208 feat(qbzd): add browse reads — album/artist/similar/suggest (CONSOLE P2)
qbzd can now BROWSE the catalog from the CLI: open an album, an artist page /
top-tracks / releases grid, find similar artists or albums, and pull For-You
suggestions — reads that feed the composition pipeline (`--ids | queue add -`).

- api/browse.rs: GET /api/album (get_album + optional get_album_suggest);
  GET /api/artist?view=page|top|albums (get_artist_page / get_artist_tracks /
  get_releases_grid); GET /api/similar?artist=|album= (get_similar_artists /
  get_album_suggest); GET /api/suggest?seed= (get_dynamic_suggest — seeds from
  the current queue when omitted). Typed serde shapes verbatim; auth-gated;
  blacklist fail-open.
- cli/browse.rs: `album`, `artist`, `similar`, `suggest`; a generic items/tracks
  walk drives the human list and --ids; `--seed -` reads ids from stdin.
- api/mod.rs: P1_ROUTES 2->6 (+album/artist/similar/suggest); routes + test.
  main.rs: four subcommands.

Verified: cargo test -p qbzd --bin qbzd -> 262 passed, 0 failed.

cc
2026-07-19 15:02:19 -06:00
vicrodh f2f65b0fbf feat(qbzd): add play <content> + POST /api/play (CONSOLE P1 row 23)
qbzd can now START playback of a track / album / playlist / artist / Qobuz
URL from the CLI — the receiver becomes a source. Server-side materialization
and the shipped cold-start ritual; the protected audio path is untouched.

- api/play.rs: resolve selector (a URL via link_resolver wins over id fields),
  fetch tracks (get_album / get_playlist / get_artist_tracks / get_tracks_batch),
  map via the now-shared queue::track_to_queue_track, stamp album/artist/
  playlist provenance, then set_queue -> play_track_resolved -> save_session_now
  (never a bare cursor move). Auth-gated; 404 on unknown id / empty resolution;
  503 on start failure.
- cli/play.rs: `qbzd play [album:ID|track:ID|artist:ID|playlist:ID|URL|N]`;
  bare `play` delegates to transport::play (one resume implementation). Local
  exit 2 on a malformed selector.
- api/queue.rs: track_to_queue_track -> pub(crate); api/playback.rs:
  resolve_quality -> pub(crate) (both now shared with play).
- api/mod.rs: P1_ROUTES += POST /api/play; route + test. main.rs: Play gains
  an optional content arg (bare = resume, unchanged).

Verified: cargo test -p qbzd --bin qbzd -> 256 passed, 0 failed.

cc
2026-07-19 14:54:56 -06:00
vicrodh 23d6cc71ec feat(qbzd): add search verb + GET /api/search (CONSOLE P1 row 19)
First slice of Proposal A (CONSOLE): qbzd can originate discovery — search
Qobuz by name from the CLI, not only receive via QConnect or take track
ids by hand.

- api/search.rs: GET /api/search?q=&type=&limit=&offset=. Typed searches
  return SearchResultsPage<T> verbatim under a per-category key; type=all
  fans out to the four typed searches. Auth-gated (needs_auth); blacklist
  fail-open by design (documented GUI-parity delta).
- api/mod.rs: new P1_ROUTES table + p1_route_table test — the shipped-caller
  HARD RULE (§3.1.4) applied to the content-verb door; route registered.
- cli/search.rs: `qbzd search <q> [--type all|albums|tracks|artists|
  playlists] [--limit N] [--offset N] [--ids] [--json]`. --ids emits ids
  one-per-line (pipes into `queue add -`); frozen exit-code table.

Verified: `cargo check -p qbzd` clean (no warnings); `cargo test -p qbzd
--bin qbzd` → 248 passed, 0 failed (10 new + 238 existing). New files are
standard-rustfmt; existing files carry only the minimal logical edits.

cc
2026-07-19 14:20:25 -06:00
vicrodh 075582920a fix(qobuz): 403 circuit breaker + prefetch backoff; header tooltips
Issue #637: a Qobuz outage + forced re-login left a user's account
transiently 403'd, and the no-backoff prefetch retry storm escalated it
into a per-IP edge/WAF block (the "error decoding response body" lines are
that non-JSON WAF body hitting a bare .json()). The bundle bump b019->b034
is exonerated: extraction yields byte-identical app_id/secret/private_key.

- ForbiddenBreaker: after repeated 403s the hot streaming/favorites paths
  short-circuit without touching the network, with an exponential cooldown
  and a half-open probe; success resets it.
- get_stream_url: typed 401/403 + body preview; abort the quality fallback
  loop on 403/401/breaker-open instead of firing all five qualities.
- CMAF session/start + file/url: feed the breaker, map 403.
- get_favorites + qws/createToken: check status before .json() so a 403
  surfaces as a 403, not a misleading "error decoding response body".
- Prefetch: per-track failure cooldown (negative cache) so a failed track
  is not re-hammered every queue tick.
- Audible path: a 403/back-off no longer counts as an "unavailable" skip;
  stop cleanly with a toast.
- QBZ_FORCE_403 test hook to smoke the back-off path on a healthy account.
- Header action-button tooltips for AlbumView/ArtistView/PlaylistView,
  translated across all 8 locales.

cc
2026-07-19 10:01:37 -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 89d08c27b8 feat(playback): artist-card play — Popular tracks with studio-discography fallback
The artist grid/overlay play button emitted media-action("artist", id,
"play") but no Rust arm handled it, so it was a no-op. Add play_artist:
play the artist's Popular (top) tracks, and when the artist has none, fall
back to playing their studio discography — the "album" + EP/single buckets
in the page's section order, deduped by album id, excluding compilations,
live, and "other". Fetches the artist page once and decides from it; a
single unavailable album is skipped without a toast or aborting the rest.

cc
2026-07-18 23:38:20 -06:00
vicrodh e9d235f33e fix(renderer): reuse one wgpu instance/device across Wayland surface recreations
On Wayland every tray hide->show destroys the winit window and re-runs
the femtovg-wgpu set_surface, which with WGPUConfiguration::Automatic
created a brand-new VkInstance + VkDevice on each restore. The NVIDIA
driver segfaults on that churn: a null call inside libnvidia-glcore,
reached via libvulkan from set_surface before the first render
(reproduced under gdb, close-to-tray -> restore with playback starting
mid-restore; it crashed even while paused, ruling out the shader
drain path).

Create the wgpu instance/adapter/device/queue ONCE at startup and hand
it to Slint as WGPUConfiguration::Manual (Linux+Wayland only), so a
restore only re-creates the surface/swapchain on the SAME device. The
Automatic configuration stays as the fallback on other platforms and
on creation failure.

km
2026-07-18 20:43:06 -06:00
vicrodh 5ebe418805 fix(kiosk): dismiss on-screen keyboard on accept and tap-outside
The OSK opened on kiosk search/inputs but never closed: focus stayed on
the search field. Clear focus in the field's `accepted` handler and add a
full-bleed background TouchArea that clears focus on any tap outside the
input, so the on-screen keyboard dismisses.

cc
2026-07-18 19:50:27 -06:00
vicrodh a08d91eeef fix(kiosk): touch-scroll Settings via raw Flickable + ListScrollbar
The std ScrollView does not intercept a touch-drag that starts over the
dense SettingRow controls, so Settings did not finger-scroll on the Pi
kiosk. Replace it with a raw Flickable (intercepts the drag after a few
px) plus a ListScrollbar overlay for the desktop bar; scroll-position
restore now keys on the Flickable's own height.

cc
2026-07-18 19:50:27 -06:00
vicrodh 3b7b5a43a4 fix(library+tray): tray-restore crash via hide-clear; richer/cleaner All list rows
- Tray restore no longer crashes (RefCell panic on close / segfault on restore):
  clear ImmersiveState.shader-texture in tray::hide_window BEFORE the surface is
  destroyed (UI thread, no rendering-notifier RefCell borrow), instead of a
  deferred clear in the RenderingTeardown notifier that ran too late when the
  drain was parked (paused). The 30 fps drain repopulates it after restore.
- All list view: right-click opens the context menu AT THE CURSOR (window
  pointer minus the row's absolute position); the whole row is right-click
  sensitive (per-cell handlers), while the ⋯ button keeps anchoring the menu to
  itself. Hide the source column + grid source badges when show-local is off
  (all-Qobuz feed → no per-row Qobuz mark).
- Remove the now-unused FavArtistCard / FavLabelCard components (the Favorites
  Artists/Labels grids use ArtistGridCard / LabelCard).

cc
2026-07-18 17:41:39 -06:00
vicrodh 80986a02b4 fix(kiosk): immersive-exit returns to the pre-immersive shell; native-touch diagnostics
- Immersive exit now returns to the shell (kiosk or desktop) the user came
  from, and only drops fullscreen when it was NOT fullscreen before opening —
  so the kiosk appliance (which boots fullscreen outside immersive) is no longer
  dumped back to the windowed desktop shell on exit. Captured on the open edge
  in the fullscreen guard.
- Add a WindowEvent::Touch arm: a one-shot log confirming whether the compositor
  delivers native touch (RaspiOS/labwc mouse-emulation withholds wl_touch, which
  is why swipe/drag never works — a compositor-side fix, not an app bug), plus
  touch-point mirroring to ShellState so hover-revealed chrome reacts under
  native touch.

cc
2026-07-18 15:38:14 -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 8e0802768f fix(library): smoke-test fixes for All feed + local favorites + artist toggle
- All feed clicks were dead: the Albums/Artists/Playlists/Labels branch didn't
  exclude the "all" tab, so an empty Flickable overlaid the grid and swallowed
  every press. Exclude "all" from that branch.
- Local favorites never bound on the online session paths: init_for_user was
  only called in the offline shell. Bind it in login + restore, teardown in
  logout — so the heart flips and local items reach the "All" feed.
- Add the show-local source switch + the grid/list ViewToggle to the All
  toolbar; implement the compact list view.
- ArtistPage "In library" rows rendered blank: add ArtistLibraryTrack/Album
  artwork targets + dispatch cover jobs in navigate_artist.
- Move the catalog/library toggle into the artist header, floated right.
- Glass under the dynamic background: ToggleButton / ExpandableSearch /
  SegmentedTabBar go translucent when app-background-active.
- Library > Artists grid now uses the ArtistGridCard (flat + A-Z grouped).

cc
2026-07-18 02:59:12 -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 e84d1035f8 fix(cast): seek by fraction of the cast track duration
The seekbar derived the absolute position from the local core's playback
duration, which is 0/stale while casting (the local backend is stopped),
so fraction * duration collapsed to ~0 and every drag restarted the
track. Resolve the real duration from the cast track's catalog metadata
instead (new seek_fraction_if_cast). Fixes seek on both Chromecast and
DLNA.

cc
2026-07-17 21:22:36 -06:00
vicrodh 142142e317 fix(cast): clear residual DLNA transport state on connect
A renderer left mid-playback by a previous session re-requests its
orphaned CurrentURI against the fresh media server (new path token ->
403 loop) and reports a stale PLAYING position. That poisons
cast_max_position and trips the premature-STOPPED end-detection guard
into auto-advancing the first cast track. Issue a best-effort AVTransport
Stop right after connect so the transport starts clean.

cc
2026-07-17 21:22:35 -06:00
vicrodh 1638da05dc chore(deps): bump getrandom to 0.4 in qbz-cast
getrandom::getrandom() was renamed to getrandom::fill() in 0.3; adopt fill() at the cast token call site. Consolidates onto the getrandom 0.4.2 that tempfile and rand already pull in.

cc
2026-07-17 20:20:51 -06:00
vicrodh b8ee89b85c chore(deps): bump tempfile to 3.27.0
cc
2026-07-17 20:19:31 -06:00
vicrodh c15c71449a chore(deps): bump actions/github-script from 7 to 9
cc
2026-07-17 20:14:33 -06:00
vicrodh 6620dd769b chore(deps): bump actions/upload-artifact from 4 to 7
cc
2026-07-17 20:14:25 -06:00
vicrodh 0692048833 chore(deps): bump actions/download-artifact from 4 to 8
cc
2026-07-17 20:14:18 -06:00
vicrodh 0f829ab62f chore(deps): bump actions/checkout from 4 to 7
cc
2026-07-17 20:14:06 -06:00
vicrodh 1f608cddc9 fix(qobuz): back off between bundle-extraction retries
The 3 bundle-extraction attempts fired back-to-back, so a brief network hiccup
(DNS blip, dropped connection, captive-portal redirect) failed all of them in a
few ms and the retries were effectively useless — surfacing as "Failed to
extract bundle tokens: Network error" at sign-in. Add a short growing delay
(600ms, 1200ms) between attempts so a transient failure has time to clear.

cc
2026-07-17 19:36:49 -06:00
vicrodh 3b0d8e7641 polish(shader): soften ambient metaball edges
The metaball iso-surface + a strong high-frequency domain-warp octave produced
hard, clashing edges between the color lobes (visible over the translucent UI).
Gentle the high-freq warp (lower freq + amplitude), widen the iso-surface
smoothstep so lobes melt into the base with a long gradient instead of a hard
rim, ease the bright/dark contrast, and add a touch more dither against banding
— the flowing motion stays, the ugly edges go.

cc
2026-07-17 19:36:49 -06:00
vicrodh 1ee2a6c963 fix(label)+ui: label top-track artist/album from real endpoint + album column
- label.rs parse_top_track: the /label/page top_tracks carry the main artist in
  a track-level `artists` array (roles=main-artist), not performer/artist (null
  here), and the album title in `album.title`. Read both (with legacy fallbacks)
  so label Popular Tracks show the artist name + album (discussion #631).
  TopTrack gains an `album` field, threaded to the row.
- Artist + Label Popular Tracks: show-album: true so the album-title column
  renders (owner: album required in all PopularTracks lists).

Note: "Add to Mixtape" is already present in the artist/label Popular-Tracks
multi-select bar (cassette icon, tooltip) and the per-track "..." menu
(mixtape-action = qobuz-row) — no code change needed there.

Base: 6c41f58f.

cc
2026-07-17 16:28:24 -06:00
vicrodh 6c41f58f44 fix(shader)+feat(ui): nvidia shader tiling fix + scrollbar/mixtape/track-listing batch
NVIDIA shader fix (root-caused via diagnostic bisection on the discrete GPU):
- ambient.wgsl + plasma.wgsl: replace the sin-based hash `fract(sin(dot(p,k))*N)`
  with the sin-free Dave Hoskins hash. On NVIDIA, as noise coords grow with
  time, sin's argument gets huge and range-reduction precision diverges from
  Intel's, degenerating the noise into big blocks. Confirmed via a solid-color
  clear (clean) then a uniform-passthrough (clean) — isolating it to the shader
  hash, not the texture handoff.
- ImmersiveView: the 3 immersive-exit paths (X button, in-menu exit, ESC) kept
  the drain alive only for the Large NPB dock, freezing the shader texture on
  the last immersive frame when the app-wide Ambient background was on. Now they
  also keep it alive for Ambient so it renders the ambient scene after exit.

UI batch:
- Auto-hide scrollbar (Discover's ListScrollbar auto-hide + pointer hit-test)
  on AlbumView, ArtistView, PlaylistView, PlaylistManagerView.
- #631: artist Popular Tracks now show the album cover thumbnail (thread
  artwork_url through TrackData -> track_data_to_item + a new
  ArtworkTarget::ArtistTopTrack decode job, mirroring LabelTopTrack); label
  Popular Tracks fall back to the album's main artist for the name+id. Quality
  badge already rendered by TrackRow — reused, not reinvented.
- #446: "Add to Mixtape/Collection" bulk button in the Favorites/Artist/Label/
  Mix selection bars, mirroring the existing add-to-mixtape path (track items
  auto-restrict the picker to mixtapes, not collections).

slintcheck + Opus Rust review green (no compile blockers). Base: 8c2f84b7.

cc
2026-07-17 14:51:18 -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 d8127356d3 fix(playback): recover stuck-load watchdog and seed transport state on restore
Two playback fixes:
- The 45s loading watchdog only cleared the spinner, so a load that hung
  on a network stall (not errored) left the track dead at 0:00 with no
  recovery — the legacy fallback only reacts to a setup *error*, never a
  hang. Re-start the stuck track up to twice, then skip to the next
  playable track. The counter is keyed per-track and resets the moment
  real audio plays.
- NowPlayingState shuffle/repeat were only set on a manual toggle, so the
  NPB Large shuffle indicator stayed dark after a session restore. Seed
  both from persisted state in refresh_now_playing_meta.

cc
2026-07-16 20:38:04 -06:00
vicrodh c4ddca0223 fix(input): gate hotkeys while text inputs are focused
A use-site `changed has-focus` on a std-widgets LineEdit/TextEdit never
fires — has-focus is an alias into the inner TextInput (slint#7747) — so
typing a letter that doubles as a shortcut ran the action instead of
inserting the character. Sweep every text-input surface (search, import,
create/edit playlist & folder, tag editor, colour picker, settings
fields, QConnect dev modal, kiosk shell) and drive
UiFocusState.text-input-focused from a `guard-focused` probe property
that the change tracker does observe.

cc
2026-07-16 20:37:57 -06:00
vicrodh 134eddc1ae fix(share): resolve Song.link/Album.link via Deezer ISRC/UPC (#514)
The 2.0 port kept only the Odesli-by-URL path, which rejects Qobuz URLs
(400 could_not_resolve_entity), so album.link/Song.link never resolved
while the raw Qobuz link still copied fine. Restore the Tauri behaviour:
look the track up on Deezer by ISRC (album by UPC, with a leading-zero
retry) and build song.link/d/{id} / album.link/d/{id} from the match.

cc
2026-07-16 20:37:51 -06:00
vicrodh 33f91adcca Merge branch 'bugfix/debug-batch-2026-07-15' into pre-release
2.0.2 batch: audio #508 + streaming fixes, #618 window identity/geometry +
single-instance, sub-genre filter, Qobuz Playlists View-all, Plex refresh,
searchable theme dropdown, ArtistGridCard app-wide, Pinned Home/For You section,
and shell UI polish (sidebar margins, queue count, macOS corner).

cc
2026-07-16 16:32:31 -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 288e982b44 fix(ui): sidebar dock margins, queue count footer, macOS NPB-small corner
Homologate the large-dock cover's side margins (16px each vs the visible content
bezel) and add a 4px bottom gap by trimming the FFT band 2px top/bottom. Move the
queue count out of the "Up Next" header into a fixed footer ("Showing X of Y
tracks", dropping the redundant "left"). On macOS, inset the flush-left NPB-small
art so the native rounded window corner stops clipping it.

cc
2026-07-16 16:31:39 -06:00
vicrodh f26d3e5e1d feat(settings): searchable theme dropdown
Enable the searchable QbzSelect on the theme picker (36 themes) — the search box
the migration deferred. Rides the #616 theme-filter (icon swap + list narrowing,
already on pre-release); the search filters within the active All/Dark/Light
facet.

cc
2026-07-16 16:31:39 -06:00
vicrodh 7138dafbdb feat(home): Pinned section — per-user store, mixed carousel, pin affordances
Users pin albums/artists/playlists to a configurable "Pinned" section on Home
and For You. New per-user SQLite pinned-items store (headless in qbz-app +
lifecycle wrapper), a mixed PinnedCarousel dispatching Album/ArtistGrid/Playlist
cards by kind, a rotated pin glyph (hover-only) on those cards, and Pin/Unpin
from the playlist header and the album/artist context menus. Local/Plex albums
resolve to their Local Library page (id-format routing) and render their real
covers via the mixed-source artwork dispatcher.

cc
2026-07-16 16:30:30 -06:00
vicrodh d87ae8a65f feat(discover): ArtistGridCard for mixed carousels, used app-wide
New 200x246 artist card (framed round portrait, follow/play/more hover overlay,
centered wrap-capable name with an optional reco subtitle) matching the album/
playlist card footprint. ArtistCarousel renders it by default across the app
(search "Most popular" hero included); the legacy 160x220 ArtistCard stays for
the label/award carousels whose artist actions don't apply.

cc
2026-07-16 16:30:30 -06:00
vicrodh 64920a76e4 feat(discover): Qobuz Playlists "View all" page + shared header tools
Port the Tauri playlist "View all": a new PlaylistBrowseView with the category
tag bar, search filter, genre filter and grid/list toggle, extracted into a
shared BrowseHeaderTools and a reusable PlaylistListRow. Restores the "Popular
albums" see-all and aligns PlaylistCarousel's "View all" affordance with the
standard carousels.

cc
2026-07-16 16:30:12 -06:00
vicrodh 6aa314b8ef fix(discover): sub-genre filter sends raw ids to the server (Tauri parity)
/discover/index honors sub-genre ids server-side, so send the raw selection
(parent or sub-genre, exactly as toggled) instead of mapping to the top-level
ancestor and narrowing client-side by genre.path — the previous hybrid widened
sub-genre selections back to their parent and dropped albums tagged only at top
level. Removes the client-side narrowing and its now-dead helpers; keeps a
diagnostic log of the outgoing genre_ids.

cc
2026-07-16 16:30:12 -06:00
vicrodh f778874fdd fix(audio): #508 ALSA-exclusive + streaming regression; flatpak ReserveDevice1 own-name
Store the CMAF duration at command-accept so the seek bar and progress can't
freeze at 0:00, backfill it on the Resume rebuild, and clear + surface the
zombie stream-create-failure path. Degrade every non-priority device-reservation
contention error (only a higher-priority holder stays fatal) so a sandboxed
D-Bus RequestName denial can't block bit-perfect playback. Honor the
stream_buffer_seconds floor in the dynamic CMAF path and wire the network
throttle (record_segment_bandwidth / record_underrun / prefetch cap) to tame the
#591 underrun loop. Grant --own-name=org.freedesktop.ReserveDevice1.* in the
repo flatpak manifest so WirePlumber releases the card cleanly.

cc
2026-07-16 16:29:58 -06:00
Victor RH 6e0d788259 Merge pull request #616 from afonsojramos/fix/theme-filter-icon-and-list
fix(settings): make the theme-list filter actually work
2026-07-15 21:55:22 -06:00
vicrodh 5ae5d8e651 docs(readme): add Headless Daemon (qbzd) feature section linking the wiki manual
cc
2026-07-15 15:51:40 -06:00
vicrodh b4bff581ae feat(qbzd): wide sidebar, wrapped descriptions and fixed control column in setup TUI
cc
2026-07-15 12:15:32 -06:00
vicrodh e6cb5114a9 fix(qbzd): posture voice on the TUI network screen and pinned boot note
cc
2026-07-15 11:41:29 -06:00
vicrodh e6cc082669 feat(qbzd): default control-plane and login binds to 0.0.0.0 for LAN-first posture
cc
2026-07-15 11:32:44 -06:00
vicrodh 2e9e6a35ac fix(qbzd): honest OSC 52 copy semantics, copy-all file artifact and wizard polish
cc
2026-07-15 11:09:17 -06:00
vicrodh 90412328be feat(qbzd): hifi wizard section with copyable config blocks in setup TUI
cc
2026-07-15 10:49:38 -06:00
vicrodh ff1dbf7425 feat(qbzd): frames layout for setup TUI — sidebar nav, content frame, header and breadcrumb
cc
2026-07-15 10:10:09 -06:00
vicrodh 927b5aa762 fix(qbzd): footer state mapping test, layout parity and help-tint precision in themed TUI
cc
2026-07-15 08:47:15 -06:00
vicrodh 74dcc60b18 feat(qbzd): themed setup TUI with section borders and accent palette
cc
2026-07-15 08:37:56 -06:00
vicrodh 6b444f9748 fix(qbzd): family-aware bind fallback, URL-first output and IP-validated callback host
cc
2026-07-15 08:17:39 -06:00
vicrodh 7799017c69 feat(qbzd): auto-detect LAN callback host from SSH sessions for login
cc
2026-07-15 08:08:49 -06:00
vicrodh 42b9e39992 feat(qbz): wire settings export modal to the bundle engine (04 §4.2)
SettingsExportActions.confirm() -> settings::export_settings: read the
include-auth gate, close the modal, then off the event loop build the bundle
via qbz_app::settings::bundle::export(ExportSource::Desktop, ..) (no new
export logic), open a native rfd save dialog seeded with
qbz-settings-YYYYMMDD.qbzb, write it 0600 via write_bundle_file, and toast
the "qbzd settings import <file>" step. Errors surface a toast; cancel is a
no-op.

cc
2026-07-15 07:50:27 -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 d658a501ad fix(qbzd): store I/O failures in settings set exit 1 not 2
cc
2026-07-14 22:04:11 -06:00
vicrodh 722a0207ec fix(qbzd): latch network.online from real network-class outcomes
cc
2026-07-14 21:59:50 -06:00
vicrodh 47081b7666 fix(qbzd): drop qconnect control handle before device release in shutdown
cc
2026-07-14 21:54:55 -06:00
vicrodh ece9b1ebc6 fix(qbzd): confirmed-kill escalation in acceptance script stop path
cc
2026-07-14 21:41:41 -06:00
vicrodh f48fc6ceb8 test(qbzd): P0 acceptance script for exit codes, status contract and roundtrip
cc
2026-07-14 21:31:19 -06:00
vicrodh 601188a2c0 ci: build qbzd on ubuntu-22.04 with slint-free and glibc-floor gates
cc
2026-07-14 21:12:21 -06:00
vicrodh 39eb12fc45 feat(packaging): ship qbzd binary and user unit in deb/rpm and tarball
cc
2026-07-14 21:02:34 -06:00
vicrodh 80a20f4322 fix(qbzd): land setup on menu when credentials exist, use multiplication sign in size warning
cc
2026-07-14 20:56:22 -06:00
vicrodh a1fbbafc79 feat(qbzd): ratatui setup TUI, six screens with explicit-save semantics
Implements owner requirement 2 (03-setup-tui.md): `qbzd setup`, a six-screen
raspi-config-grade configurator over the daemon's real stores — zero new
persistence. ratatui 0.30 (IV5: unifies on the lock's unicode-width 0.2.2;
0.29 forbidden). A synchronous event loop on a blocking thread; disk/HTTP only
at screen entry, `r`, save and the immediate actions, on workers with a spinner
(no I/O on keystrokes). Non-tty exits 2 with the verbatim copy; alt-screen +
panic-hook restore.

Screens: Account (T5 login engine; inline masked token paste, suspend-and-run
browser login, logout confirm), Audio (the §3.2.3 constraint matrix + 7
cascades verified 1:1 vs the desktop code, device picker re-deriving the
§3.2.2 grouping/BP-badge rules, DSD guard), Playback (ask->fallback rule,
infinite-radio read-only), QConnect (startup/name/volume_mode, live preview),
Network (whole-file qbzd.toml rewrite preserving every key), Import/Export
(pure renderer of the T12 bundle plan; interactive device re-pick; default-off
auth gate). Saves reuse T11 write_one + reload/status classification (§4.3);
dirty marker + Save/Discard/Stay. All user-facing strings centralized.

cc
2026-07-14 20:42:42 -06:00
vicrodh 8021393925 fix(qbzd): mask secret values in import summary and distinguish reload refusal
cc
2026-07-14 19:58:31 -06:00
vicrodh 0b7cd6c5e3 feat(qbz-app): settings bundle engine with importer-side classification
Export/import CLI on qbzd; requirements 4 and 5 complete with zero desktop builds.

cc
2026-07-14 19:44:01 -06:00
vicrodh a864aa8d55 feat(qbzd): settings reload endpoint and settings/qconnect/config CLI verbs
Route table complete: 17 P0 routes, CI-counted.

cc
2026-07-14 19:02:10 -06:00
vicrodh 6fa0428127 fix(qbzd): pin player volume to 100 percent at connect in locked volume mode
cc
2026-07-14 18:27:43 -06:00
vicrodh adccb98e76 feat(qbzd): report tick, daemon device identity, volume modes and queue persistence
cc
2026-07-14 18:14:52 -06:00
vicrodh 196f167873 feat(qbzd): copy QConnect glue with TODO(converge) markers and daemon trait impls
Real-volume join report and reconnect credential re-resolution are daemon-copy
fixes; desktop call sites untouched (OD2).

cc
2026-07-14 17:47:33 -06:00
vicrodh 05e2216acc fix(qbzd): strict queue payload parsing, history in queue response, honest race comment
cc
2026-07-14 17:19:44 -06:00
vicrodh 92c49e648b feat(qbzd): queue routes and CLI verbs with server-side track materialization
cc
2026-07-14 17:05:15 -06:00
vicrodh 393a6e01a7 fix(qbzd): serialize volume canonically in JSON responses
cc
2026-07-14 16:49:20 -06:00
vicrodh e700084f0b feat(qbzd): playback routes and transport CLI verbs with full advance ritual
cc
2026-07-14 16:35:21 -06:00
vicrodh dd63b18ae9 fix(qbzd): treat empty [server] token as unset
cc
2026-07-14 16:10:34 -06:00
vicrodh 286cb51957 feat(qbzd): tiny_http control plane, Origin shield + opt-in token, ping/info/status and CLI diagnostics
cc
2026-07-14 15:59:17 -06:00
vicrodh 9597573a61 fix(qbzd): bind login nonce into the redirect path instead of relying on state echo
cc
2026-07-14 15:26:56 -06:00
vicrodh 66c9acfa86 feat(qbzd): login via one-shot nonce-bound listener, paste and token paths
Port the desktop system-browser OAuth (crates/qbz/src/auth.rs) into the daemon
CLI with three daemon-specific changes: the one-shot listener binds an ephemeral
port (never the control-API port, D6), a CSPRNG state nonce binds the callback,
and the command only validates + persists the token into the daemon root then
best-effort nudges a running daemon. Adds validate_token / login_browser /
login_paste / login_with_token_arg / nudge_reload plus logout; no email+password
surface anywhere. cli/copy.rs carries the verbatim 02 §2.2 timeout, success and
logout strings.

cc
2026-07-14 15:13:07 -06:00
vicrodh 8534a02f0d test(qbz-app): pin duration==0 end-detection edge in playback driver
cc
2026-07-14 14:22:02 -06:00
vicrodh 297bbad326 feat(qbz-app): headless playback driver with pure plan_tick and thin IO shell
cc
2026-07-14 14:09:22 -06:00
vicrodh 4d0f52138a feat(qbzd): headless runtime bring-up with daemon-root stores and credential restore
Adds additive _at credential fns (crypto/format untouched), DaemonAdapter
broadcast bus, numbered boot per 01 §8.1 incl. the #521 shutdown pair.

cc
2026-07-14 13:40:49 -06:00
vicrodh 18a10d14d9 fix(qbzd): warn on config-dir create/chmod failure instead of silent discard
cc
2026-07-14 13:18:30 -06:00
vicrodh d911f43856 feat(qbzd): qbzd.toml with unknown-key warning, flock instance lock, daemon_prefs store
cc
2026-07-14 13:10:08 -06:00
vicrodh 02c2dba68d feat(qbzd): scaffold slint-free qbzd crate with clap verb tree and CI gate
IV6 build peak: 795 MB max RSS (summed across the cargo-capped build's
process tree via /proc VmRSS polling; /usr/bin/time -v is not installed
on this box). aws-lc-rs in graph: 0 (single rustls provider: ring
v0.17.14 throughout).

cc
2026-07-14 12:53:09 -06:00
vicrodh 90d9d1c66a fix(themes): Catppuccin Latte is a light theme, not dark
PR #612 added the three Catppuccin flavors but grouped all of them under ThemeCategory::Dark. Latte is the LIGHT flavor (its palette already builds light via bg_is_light); move it to the Light category arm so the theme picker groups it correctly. Frappé/Macchiato stay Dark.

cc
2026-07-14 11:19:32 -06:00
Victor RH 771f1a29fb Merge pull request #612 from TerminalTilt/add-catppuccin-themes
feat(themes): add Catppuccin Latte, Frappé, and Macchiato
2026-07-14 11:18:42 -06:00