Commit Graph
88 Commits
Author SHA1 Message Date
vicrodh 25ab1408e5 feat(reco): augment discovery known-artists with reco signal
Add RecoStore::get_known_artist_ids (artists played >threshold OR favorited;
unit-tested) and union it into the network-discovery 'skip artists I already
know' filter. play_history stays the source of the (id, name) known set:
reco_events has no artist names and the schema is frozen to match Tauri, so
full consolidation (plan D2) would regress the name-based MB candidate filter.
This augments the id set with reco's richer plays+favorites signal instead.

cc
2026-06-26 20:03:19 -06:00
vicrodh bd887d755c feat(reco): add forgotten-favorites + genre-backfill engine queries
Two additive RecoStore queries with unit tests (all 8 reco_store tests green):
- get_forgotten_favorite_album_ids(limit, recency_days): favorite albums not
  played within the window. Ported from Tauri db.rs:391-426 but adapted to the
  integer created_at schema -- Tauri's datetime('now',..) string compare never
  matched the integer column, so the recency filter is a numeric cutoff.
- update_genre_for_album(album_id, genre_id): backfills genre_id onto an
  album's still-NULL events. Plays log no genre, so the frontend writes it back
  on album resolution; this is what makes get_top_genres non-empty (Tauri
  defined the backfill fns but never called them, so its top-genres was empty).

cc
2026-06-26 19:51:31 -06:00
vicrodh 22fb4162f7 feat(slint): search cortinilla — main-header + immersive
Intelligent Search (phases 3-4): a frontend-agnostic SearchService in qbz-app (Capa A result cache + Capa B interaction ranking) behind a live header dropdown with skeleton loading, stale-while-revalidate, 220ms debounce, keyboard nav + scroll-into-view, per-section caps, a URL-match artwork guard, and an opt-out Settings toggle. Typing previews in the dropdown; Enter goes to Search > All; local-library results show in the dropdown only.

Immersive search: an in-immersive glass cortinilla (Albums/Artists/Playlists) that switches playback without navigating — a Settings action select (disabled/replace-queue/play-next/add-to-queue), the HUD stays up while searching and never hides while the cursor rests on the chrome, twinned top buttons, and a clear button with always-centered field text.

cc
2026-06-20 17:38:02 -06:00
vicrodh ef93c86437 feat(app): headless artist-blacklist service extracted from src-tauri
cc
2026-06-16 22:19:36 -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 2a37f2ad91 feat(discover): add per-tab section-prefs core store (configurator model)
Frontend-agnostic port of Tauri discovery-v2/sectionPrefs.ts (ADR-006):
the ordered per-tab {id,enabled} model for the Discover configurator
(Home / Editor's Picks / For You). Pure model logic (default_prefs,
migrate with the legacy-array / object / garbage branches, reconcile_list
with drop-unknown + first-wins dedup + append-missing, toggle, move_section
with boundary clamp, reset_tab, is_enabled, enabled_count, enabled_ordered)
plus a per-user SQLite store (dedicated discover_prefs.db, single JSON-blob
column in the Tauri-compatible shape). 10 headless tests green.

Lives in qbz-app::settings::discover_prefs so any frontend (Slint, Tauri)
drives the configurator from one source of truth.

cc
2026-06-15 12:45:14 -06:00
vicrodh 3adb517236 feat(slint): ImmersiveView base - reusable atmosphere + compact player + audio-transport lift
Foundation for the ImmersiveView Slint port. Authored by Codex and accepted by
the owner after live visual iteration; committed here so the panel work builds
on top of it. The Album Reactive foreground was intentionally left out (a prior
attempt was rejected) so it can be rebuilt cleanly on this base.

Audio-transport lift (slice 1):
- qbz-audio: VizFrame/VizSink trait + spawn_visualizer_thread; FFT producer
  lifted off tauri::AppHandle. Protected audio path untouched (seam =
  RingBuffer.snapshot).
- src-tauri: fft_processor gutted to a thin TauriVizSink emitting byte-identical
  viz:* events.
- qbz-app: AppRuntime::with_visualizer + visualizer_tap accessor.
- qbz-slint: SlintVizSink + install() publishing VisualizerState
  bars/spectral/energy/waveform/transient at 30fps, zero IPC.

Immersive base (slice 2a):
- immersive.rs: host-side atmosphere generation from decoded artwork
  (8x8 sample -> upscale -> blur -> saturate/contrast/normalize -> vignette),
  mirroring Tauri texture-loader.ts::generateAtmosphere; plus glow_color.
- ImmersiveAtmosphere.slint: reusable animated atmosphere (4 counter-drifting
  copies via animation-tick); usable static for Album/Artist headers later.
- ImmersiveView.slint: base overlay (atmosphere + auto-hide Tauri-shaped compact
  player only). Foreground panels are rebuilt on this base.
- AppShell mounts the overlay; ViewMode menu opens Immersive in PlayerBar/Small.

cc
2026-06-13 04:11:53 -06:00
vicrodh 952846b6a0 feat(slint): Last.fm and ListenBrainz scrobblers wired to player and settings
Re-implements the parked scrobblers-wip stash intent on the current tree
(the stash no longer applied; it remains untouched in the stash list) with
three corrections: the Last.fm offline queue uses the SHARED per-user
offline_settings.db scrobble_queue (byte-identical SQL to Tauri - queues
flush from either frontend) instead of a private table; the ListenBrainz
cache opens Tauri's per-user path so LB creds and listen_queue are truly
shared (a Tauri LB sign-in pre-seeds Slint); and the queue flush is driven
by the offline engine's online edge (plus initial flush), not login-only.

New frontend-agnostic qbz_app::settings::scrobblers store (master enable,
Last.fm session/username, LB token/username; tests) and the shared
scrobble-queue API on OfflineModeStore. Player wiring hooks the de-duped
track-change edge inside the QConnect remote gate: now-playing fires on
change (skipped offline), the scrobble arms at min(50%, 240s) with a
generation counter cancelling stale timers, all sources scrobble (Tauri
parity incl. the #360 version-enriched title). Offline or call failure
queues; Last.fm flush honors the 50-batch and 14-day cutoff.

Settings gains an Integrations section (Plex-section idiom): Last.fm
browser-auth connect/finish/disconnect, ListenBrainz token field +
validate, per-service enable toggles. Known gap documented: Last.fm creds
cannot be shared with Tauri (webview localStorage); LB creds and both
queues are shared.

cc
2026-06-10 02:52:17 -06:00
vicrodh c207f232c5 fix(offline): logout reopens the Qobuz gate; auth endpoints exempt from it
Logging out of an offline session left offline_session set and the gate
closed, refusing the very OAuth exchange that could end the offline state
(user-reproduced: sign-in failed with OfflineMode after logout). teardown()
now ends the session-scoped offline state (offline_session and the cached
induced flag reset; the persisted preference reloads on the next
activation) and recomputes, reopening the gate when connectivity allows.
Two regression tests added.

The three login methods (login, login_with_oauth_code, login_with_token)
are exempt from the offline gate: user-initiated sign-in is explicit
intent to reach Qobuz; the gate governs services. Verified none of them
reach gated sub-calls; bundle cold-start stays gated on purpose. New unit
test pins the exemption. The recovery sign-in falls back to the full
browser OAuth flow when the saved token is gone or rejected, and
offline_session is lifted only on success paths.

cc
2026-06-10 01:32:14 -06:00
vicrodh 727c3fb4ba feat(slint): disk-first cached favorites, read-only hearts offline
Hoists Tauri's FavoritesCacheStore into qbz-app::settings::favorites_cache
(same per-user favorites_cache.db filename and schema, file shared across
frontends; 4 store tests). Slint's fav_cache now binds the store per user,
seeds the in-memory id set from disk at session activation (online,
restore, recovery and offline entry - fixing the start-offline cold-hearts
gap Tauri never closed), writes through on every favorite mutation, and
full-replaces the disk mirror on network refreshes. Queue hearts read the
shared cache instead of re-fetching from the network. All track-favorite
mutation paths are gated with a toast while offline (read-only hearts).

Part of offline-mode slice 3; main.rs wiring rides the slice-3 wiring
commit.

cc
2026-06-09 21:00:33 -06:00
vicrodh edae9d2463 feat(app): shared offline-mode engine with connectivity actor and grace gate
New qbz_app::offline_mode (frontend-agnostic, ADR-006): three-state mode
machine (Online / RealOffline session-scoped / InducedOffline persisted),
Tauri-compatible offline_settings.db store with the issue #279
stream_first_track snapshot, and a layered connectivity actor (default-route
signal on Linux, audio liveness window, IP-literal + vendor-diverse strict
probes, asymmetric hysteresis with confirmation burst, captive-portal and
suspend/resume handling).

qbz-qobuz gains the process-wide offline_gate and ApiError::OfflineMode (the
single Qobuz choke point; HTTP funnel wiring lands next).
SubscriptionStateStore gains offline_playback_allowed: binary 30-day-grace
playback gate for the offline cache, never a preview-degradation path.

cc
2026-06-09 19:48:13 -06:00
vicrodh f7619a92fc feat(slint): Plex creds store + Settings auth/PIN UX (port slices 0-2)
Fresh Slint-only Plex integration foundation (Tauri untouched; the Tauri
code is slated for deletion at port end, so creds are not shared):

- slice 0: fix the false Plex docs in local_library.rs
- slice 1: add the qbz-plex dependency to qbz-slint
- slice 2a: qbz-app/settings/plex.rs per-user store (10 fields, idempotent
  4->11 column migration keeping creds, 8 tests) - frontend-agnostic,
  no tauri::State (ADR-006)
- slice 2b: Settings > Local Library Plex section, 1:1 with the complete
  Tauri feature - master toggle + collapse, LAN-only address gate, PIN auth
  (2500ms poll, self-stopping on section-leave/expiry/disconnect),
  manual-token fallback, ping + machine-id capture, multi-section pick
  (default-all), disconnect vs clear-cache; once-per-session background
  refresh (no cache_clear churn on section re-entry)

cc
2026-06-07 21:44:33 -06:00
vicrodh 5b3ba963b0 feat(settings): tray store gains mac_hide_dock + close-to-tray opt-out
Add a mac_hide_dock column (with migration) to the shared tray settings
store for the upcoming macOS close-to-tray Dock-icon toggle, and flip the
close_to_tray default to true (opt-out: closing sends the window to the
tray and keeps the app running).

cc
2026-06-06 01:44:30 -06:00
vicrodh 1072195740 merge: bring v1.2.15 Tauri fixes (shared HTTP client / fd exhaustion, qobuz bundle-token cache + bounded fetch) into slint-mvp
cc
2026-06-05 15:11:48 -06:00
vicrodh 0cefe1ee80 fix(qobuz): cache bundle tokens and bound the fetch to stop startup hangs
Startup blocked on Qobuz's ~7 MB web bundle download with no cache or
timeout, so a slow CDN left the app on a blank screen for 25-30s before
the login screen appeared. Cache the extracted tokens on disk (keyed by
bundle version), apply them instantly on warm starts and refresh in the
background, and bound the cold fetch with a per-request timeout + retry.
The legacy api client shares the same cache. Emits BundleFetchStarted so
the UI can show a connecting state on the first run.

cc
2026-06-05 01:09:54 -06:00
vicrodh f907d9d984 refactor(qconnect): move device_uuid persistence to qbz-app (slice 1)
Relocate the persistent QConnect device-identity resolution out of the
Tauri adapter into the frontend-agnostic qbz-app crate
(qbz-app/src/qconnect_identity.rs) so both the Tauri and a future Slint
frontend resolve the SAME uuid for the same install.

Moved: resolve_qconnect_device_uuid, device_uuid_from_db,
load/persist_device_uuid, qconnect_settings_db_path.

Byte-identical persistence contract preserved: DB path
(<data_dir>/qbz/qconnect_settings.db), the 'device_uuid' key, and the
QBZ_QCONNECT_DEVICE_UUID env override are unchanged, so existing installs
keep their uuid. The Tauri transport.rs::resolve_qconnect_device_uuid is
now a one-line delegate; the device-NAME persistence shares the same DB
file via the qbz-app path helper.

qbz-app gains a uuid dependency (it already had rusqlite + dirs, so no
SQLite dep was added to qconnect-app per R6). The 2 device_uuid tests
move to qbz-app.

cc
2026-06-01 09:03:13 -06:00
vicrodh 7e6e548598 feat(slint): minimal playback wiring + Queue, plus detail/Settings fixes
Playback (minimal, album-focused):
- playback.rs controller: play an album (set queue + start audio),
  play a single track, enqueue, play a queue index, transport
  (play/pause, next, previous, seek, volume)
- the audible step uses Player::play_track (fetch -> download -> play);
  the protected audio backend is untouched
- a 450ms poll loop pushes position/progress/times/playing onto
  NowPlayingState and auto-advances at end of track
- NowPlayingState / QueueState globals; NowPlayingBar bound to real
  state (song card, seek bar, transport, volume); QueueSidebar lists
  the queue and rows play on click
- a played track is recorded into the recently-played store

Detail views:
- album description and artist biography stay with the metadata,
  filling the width to the right of the artwork (not below the header)
- header nav tabs gained a dropdown caret and 2px side padding

Settings > Playback defaults are now opt-out: show context icon,
restore session, resume position and stream-uncached default on;
streaming-only stays opt-in; initial buffer defaults to 2s.

Known gaps: queue/now-playing artwork not yet threaded (Image is not
Send); autoplay-mode preference not consulted by auto-advance; buffer
progress line stays at 0.

cc
2026-05-18 22:16:13 -06:00
vicrodh 7ef6300a22 refactor(qbz-app): add minimal session activation to AppRuntime
Task 2 of the Slint POC readiness audit. AppRuntime now performs
framework-agnostic per-user session activation: activate, activate_at,
activate_offline, deactivate, plus is_session_active, active_user_id,
and a with_session_store accessor.

This is the minimal version per the 2026-05-18 audit correction. It
opens only the session store and does the portable scaffolding (user
paths, directories, last_user_id marker, runtime state machine). It
does not touch Tauri's session_lifecycle, does not initialize the
src-tauri-side per-user stores, and does not run the flat-to-user
migration. The shipping app keeps identical behavior; this is additive.

activate_at takes explicit directories and is the testable core;
activate resolves real per-user paths through UserDataPaths.

cc
2026-05-18 09:26:22 -06:00
vicrodh a213b37807 refactor(qbz-app): add AppRuntime shell facade skeleton
Adds a framework-agnostic composition root for non-Tauri UI shells
(Slint, TUI, headless) as Task 1 of the Slint POC readiness audit
(session-20-slint-poc-readiness-audit).

AppRuntime<A> owns an Arc<QbzCore<A>> plus the runtime state machine,
built with the same recipe as the Tauri CoreBridge but with no Tauri
dependency. Scope is composition and accessors only; per-user store
ownership and session activation are deferred to Task 2.

qbz-app now depends on qbz-core, qbz-player, qbz-audio, and qbz-models.
The Tauri app does not consume this module; it is purely additive.

cc
2026-05-18 07:28:19 -06:00
vicrodh f79c550889 refactor(qbz-app): add playback context
cx
2026-05-17 19:39:14 -06:00
vicrodh 4ba96fe59a refactor(qbz-app): add session store foundation
cx
2026-05-17 19:16:13 -06:00
vicrodh 29b766ae24 fix(graphics): recommend CPU for NVIDIA-only, allow DMA-BUF on hybrid
NVIDIA-only systems no longer get a hardware-acceleration recommendation that visibly stutters under WebKitGTK. The three NVIDIA-only branches (Wayland+GNOME, Wayland, X11) collapse into one CPU-rendering profile, since XWayland does not rescue the stutter either.

NVIDIA + iGPU hybrid now allows DMA-BUF: WebKit composites on the stable iGPU stack, so the instability that affects NVIDIA-only systems does not apply. The recommendation also tells the user to pick the iGPU explicitly in the rendering GPU selector instead of Auto.

cc
2026-05-16 12:22:41 -06:00
vicrodh 1a5dbe07bb refactor(qbz-app): extract boot watchdog decision
cx
2026-05-16 09:04:34 -06:00
vicrodh 3c963cdaf3 refactor(qbz-app): extract graphics autoconfig
cx
2026-05-16 08:50:06 -06:00
vicrodh 2fc9270c89 refactor(qbz-app): add graphics detection
cx
2026-05-16 00:44:16 -06:00
vicrodh a497d61b75 refactor(qbz-app): add user data paths
cx
2026-05-16 00:20:21 -06:00
vicrodh be500956ff refactor(qbz-app): add developer settings store
cx
2026-05-15 23:25:06 -06:00
vicrodh bcdbc69913 refactor(qbz-app): add graphics settings store
cx
2026-05-15 18:52:15 -06:00
vicrodh f8f8bb88d6 refactor(qbz-app): add image cache settings store
cx
2026-05-15 18:18:36 -06:00
vicrodh 26012e98c9 refactor(qbz-app): add download settings store
cx
2026-05-15 17:48:14 -06:00
vicrodh 2e43831800 refactor(qbz-app): add remote control settings store
cx
2026-05-15 14:17:35 -06:00
vicrodh 04d5b45134 docs(qbz-app): keep app comments frontend agnostic
cx
2026-05-15 00:12:22 -06:00
vicrodh d1082317f5 refactor(qbz-app): add ui preference stores
cx
2026-05-15 00:04:36 -06:00
vicrodh 61d5715a22 refactor(qbz-app): add preference stores
cx
2026-05-14 19:48:54 -06:00
vicrodh 800447d33c refactor(qbz-app): add settings stores
cx
2026-05-14 17:08:45 -06:00
vicrodh dc74db6401 test(qbz-app): cover runtime state transitions 2026-05-14 11:54:16 -06:00
vicrodh d16ef003a5 refactor(qbz-app): move runtime core 2026-05-14 11:45:59 -06:00
vicrodh 4f008cd219 refactor(qbz-app): scaffold app crate 2026-05-14 11:40:19 -06:00