Commit Graph
1465 Commits
Author SHA1 Message Date
vicrodh 6283da7c70 feat(offline_cache): add maintenance module for bulk delete and redownload selection
Pure logic, no Tauri state. remove_album_cached_tracks bundles the
SQLite delete with on-disk CMAF cleanup. select_redownload_targets
enforces the rule that re-download skips in-flight Downloading tracks
and optionally restricts to Failed only.

cc
2026-05-06 07:29:40 -06:00
vicrodh 60daa7e1c6 fix(offline_cache): use Queued for reset_track_for_redownload, drop redundant Pending
The previous commit added a Pending variant to OfflineCacheStatus, but
the existing Queued variant already represents 'freshly inserted,
awaiting downloader pickup' (insert_track writes status='queued').
Reusing Queued keeps reset_track_for_redownload semantically aligned
with insert_track and preserves the in-flight count SQL that already
matches WHERE status='downloading' OR status='queued'.

cc
2026-05-06 07:24:50 -06:00
vicrodh c47a5558ad feat(offline_cache): add get_album_tracks and reset_track_for_redownload
get_album_tracks returns rows of any status (needed for re-download
which targets ready+failed but skips downloading). reset_track_for_redownload
clears progress and error so the downloader can restart cleanly.

Also adds OfflineCacheStatus::Pending (serializes/parses as 'pending') so
reset_track_for_redownload has a target state distinct from Queued and
Downloading. The frontend type union (per plan) already includes 'pending'.

Factors out row_to_cached_track_info as a private free function so
get_track, get_all_tracks, and the new get_album_tracks share the same
15-column row decoder.

cc
2026-05-06 07:21:15 -06:00
vicrodh f6eaa4ba6b fix(offline_cache): propagate row decode errors in delete_album_tracks
The previous filter_map(.ok()) silently dropped per-row decode errors,
which would have caused the returned ids vec to be truncated while the
rows were still counted in the byte total and physically deleted. P2.2
and P2.3 will use the returned ids to clean filesystem artifacts; a
silent truncation would leak files. Promote row errors to the function
boundary instead.

cc
2026-05-06 07:16:30 -06:00
vicrodh def0b11439 feat(offline_cache): add delete_album_tracks to OfflineCacheDb
Single-transaction batch delete keyed by album_id. Returns the deleted
track_ids and the total bytes freed so callers can clean filesystem
artifacts and report freed space.

cc
2026-05-06 07:11:36 -06:00
vicrodh a714db937a refactor(offline_cache): lift downloader functions out of legacy_compat
Moves spawn_track_cache_download and try_cmaf_offline_download from
commands_v2/legacy_compat.rs to offline_cache/downloader.rs as
pub(crate) functions. No behavior change. Prepares the V2-clean path
for the upcoming offline cache manager commands.

cc
2026-05-05 22:33:34 -06:00
vicrodh 075e32b680 fix(theme): WCAG-aware btn-primary-text token across all themes
Frontend (62 components, ~64 rules):
- Replace hardcoded `color: white` / `#fff` with `var(--btn-primary-text)`
  in every rule that paints text on `var(--accent-primary)`. Covers in-rule
  cases (BulkActionBar, GenreFilterButton, KeybindingsSettings, settings
  views, login, modal primary buttons, wizard steps, etc.) plus descendant
  selectors (`.parent.selected .child`).
- Semi-transparent rgba(255,255,255,X) over selected accent rows now use
  `color-mix(in srgb, var(--btn-primary-text) X%, transparent)` so the mute
  level inherits the per-theme token color.

Theme tokens in app.css (--btn-primary-text fixes):
- rose-pine-dawn: #faf4ed → #575279 (was the bg color, ratio 2.60 FAIL;
  now 5.71 AA against coral accent)
- kurosaki:       #fbf9f2 → #262424 (was the bg color, ratio 1.76 FAIL;
  now 8.33 AA against gold accent)
- snow-storm:     #eceff4 → #2e3440 (was the bg color, ratio 3.50 AA-Large;
  now 6.13 AA against blue accent)
- stratego:       #0a0a0a → #ffffff (visual choice; 4.14 AA-Large is
  acceptable for button labels and matches palette intent)

Auto-theme generator (src-tauri/src/auto_theme/generator.rs):
- pick_btn_text_for_accent_set(): WCAG-aware FG selection that holds
  across accent / hover / active states. Replaces the broken
  `luminance < 0.5` threshold which produced 2.0:1 ratios on mid-luminance
  accents (Catppuccin lavender, Dracula purple, rose-pine coral).
- ensure_text_contrast_target(text, bg, is_dark, target): generalize the
  iterative contrast helper to accept a target ratio. Apply to
  text_secondary (4.5) and text_muted (3.0).
- Add --btn-danger-text and --btn-warning-text tokens via
  pick_btn_text_for_bg for future status-button consumers.

Misc fixes:
- SettingsView.svelte: extract initialTitlebarMode to const, avoids the
  Svelte 5 state_referenced_locally warning.
- QueuePanel.svelte: wrap .stop-after-marker in :global() (the class is
  forwarded to a Svelte component child, not a native element) and fix
  var(--accent) typo to the correct var(--accent-primary).

Tests: 13 generator tests pass (8 prior + 5 new). svelte-check 0 errors,
31 warnings (down from 33).

cc
2026-05-05 16:43:45 -06:00
vicrodh 1be46f1a24 Merge branch 'feature/internal/titlebar-mode-selector' into pre-release
Adds the 4-value titlebar mode selector (qbz | system | plasma | hidden)
replacing the three mutually-conflicting boolean toggles. Plasma mode
delivers KDE-native SSD via Xwayland on Wayland sessions; system mode
now also mounts a stripped strip below OS chrome when the user has
search-in-titlebar or any titlebar-nav item enabled.

cc
2026-05-05 09:46:18 -06:00
vicrodh 5369425d91 chore(deps): bump mdns-sd from 0.11.5 to 0.19.1 in /src-tauri
Bumped only for src-tauri's qconnect-discovery path. qbz-cast (chromecast/
DLNA) stays on 0.11 because mdns-sd 0.19 changed the address API
(HashSet<IpAddr> → HashSet<ScopedIp>) and adapting qbz-cast is real
work outside dependabot scope. Workspace now carries both versions
in the graph; both compile and operate independently.

cc
2026-05-05 09:37:59 -06:00
vicrodh ba98697da7 chore(deps): bump sha2 from 0.10.9 to 0.11.0 in /src-tauri
cc
2026-05-05 09:17:25 -06:00
vicrodh f38141c034 chore(deps): bump notify-rust from 4.16.0 to 4.16.1 in /src-tauri
cc
2026-05-05 08:21:47 -06:00
vicrodh 4ca0faf378 chore(deps): bump tokio from 1.50.0 to 1.52.1 in /src-tauri
cc
2026-05-05 08:05:50 -06:00
vicrodh 71b2d18967 chore(deps): bump axum from 0.8.8 to 0.8.9 in /src-tauri
cc
2026-05-05 07:58:16 -06:00
vicrodh 2794860bc0 feat(main): set GDK_BACKEND=x11 when titlebar_mode is Plasma
Reads the persisted titlebar mode at startup via WindowSettingsStore::new_readonly()
then composes the X11-forcing condition with the existing graphics force_x11
setting. Plasma mode now produces real KWin SSD on Wayland sessions without
requiring users to set the env var externally.

cc
2026-05-03 15:16:23 -06:00
vicrodh 4215b075a2 feat(lib): wire titlebar_mode into window builder, register new v2 commands
Window builder now derives decorations from TitlebarMode::wants_decorations()
which returns true for System and Plasma. Transparency gate now requires
mode=Qbz so plasma/system/hidden modes do not build a transparent window
that would leak through KDE/system chrome. The v2 command registry is
updated to expose the new mode commands and drops the legacy boolean
setter.

cc
2026-05-03 15:11:34 -06:00
vicrodh e444d3594d feat(commands-v2): add v2 titlebar-mode commands, drop v2_set_use_system_titlebar
The new commands cover get, set, and platform availability. The legacy
boolean setter is removed because the mode enum is now authoritative.

cc
2026-05-03 15:03:50 -06:00
vicrodh dae22574a6 feat(window-settings): add available_titlebar_modes and is_sandboxed helpers
Detection of KDE+Wayland for Plasma-mode availability. The is_sandboxed
helper drives the frontend caveat hint about Flatpak/Snap.

cc
2026-05-03 14:50:04 -06:00
vicrodh 045236f716 feat(window-settings): add TitlebarMode enum and titlebar_mode column
Adds a 4-value mode enum (Qbz | System | Plasma | Hidden) to the existing
WindowSettings store, with column migration that infers mode from the
legacy use_system_titlebar flag. Setter is write-through so the legacy
column stays consistent for any remaining readers.

cc
2026-05-03 14:44:41 -06:00
vicrodh 63745a480f fix(qconnect): trigger startup auto-connect after OAuth restore, not during setup
The auto-connect spawn was firing at the end of the Tauri setup hook,
before the frontend invoked runtime_bootstrap. At that point the API
client is not yet initialized (bundle tokens not extracted, no auth),
so service.connect always failed silently and the lifecycle stayed Off
even when startup_mode was On or Remember-last-on.

Move the trigger into v2_runtime_bootstrap, right after the OAuth token
restore + session activation + CoreBridge auth complete. The CLI
override (--enable-qconnect / --disable-qconnect) is exposed via a
managed QconnectCliOverride wrapper so bootstrap can read it.

cc
2026-05-01 15:58:14 -06:00
vicrodh a6fb1cd018 feat(qconnect): write-through last_known_state on connect/disconnect
When startup_mode == RememberLast, persist the latest on/off state so
the next launch can restore it. No-op for Off/On modes.

cc
2026-05-01 13:10:39 -06:00
vicrodh 7bbe68d5b0 feat(qconnect): register v2_qconnect_get/set_startup_mode in invoke handler
cc
2026-05-01 13:08:57 -06:00
vicrodh 398b4ea807 feat(qconnect): add v2_qconnect_get/set_startup_mode commands
String-typed wire shape for predictable serde behavior across the
Tauri invoke boundary. Validates against QconnectStartupMode::from_str.

cc
2026-05-01 12:39:43 -06:00
vicrodh d5ae31aa1b fix(qconnect): move startup auto-connect to end of setup hook
The auto-connect spawn was running on a worker thread before CoreBridge
inner init completed, risking undefined behavior in service.connect.
Moving it to the end of setup ensures all init runs first.

cc
2026-05-01 12:37:48 -06:00
vicrodh 6f38362b9f feat(qconnect): apply startup_mode + CLI override during app setup
Spawns a background auto-connect task during the Tauri setup hook when
the effective startup decision says so. Failures log a warn and leave
lifecycle in Off; no infinite reconnect loop on first-time failure.

cc
2026-05-01 12:30:32 -06:00
vicrodh 27cb2ab6cc feat(qconnect): parse --enable-qconnect / --disable-qconnect CLI flags
Volatile per-launch override. Both flags together → enable wins
deterministically with a warn log. The override is threaded into
lib::run() and consumed in the next task.

cc
2026-05-01 12:20:35 -06:00
vicrodh 40851ad58e feat(qconnect): add startup_mode + last_known_state persistence helpers
Reuses qconnect_settings.db with WAL pragma. Fail-open on any error so
a corrupt DB falls back to Off and never blocks app startup.

cc
2026-05-01 12:16:28 -06:00
vicrodh e0fd0584fa chore(release): bump version to 1.2.10 + tag notes
Hotfix on top of 1.2.9 to address the gapless-play regression
introduced by dee987c1a's tightened isGaplessTransition gate combined
with a long-standing race in the player's transition-swap atomics.

Bumped:
- src-tauri/Cargo.toml + Cargo.lock
- src-tauri/tauri.conf.json
- package.json + package-lock.json
- snapcraft.yaml (version + source-tag) — packaging/snap/snapcraft.yaml
  follows automatically via symlink
- packaging/flatpak/com.blitzfc.qbz.metainfo.xml (new release entry)

cc
2026-04-29 00:19:32 -06:00
vicrodh f17a1733f7 chore(release): bump version to 1.2.9 + tag notes
Quiet Polish: QConnect cleanup (#316) and a long round of Linux
tray polish. Sticky-header gap fix (no more padding-top on scroll
containers), Edit folder shortcut from the playlist sidebar (#364),
shift-range + Ctrl/Cmd+A multi-select across the remaining views,
opt-in resume of seek position (#317), and Qobuz subtitle/edition
propagated through queue / MPRIS / scrobbling (#360).

Bumped:
- src-tauri/Cargo.toml + Cargo.lock
- src-tauri/tauri.conf.json
- package.json + package-lock.json
- snapcraft.yaml (version + source-tag)
- packaging/flatpak/com.blitzfc.qbz.metainfo.xml (new release entry)

packaging/snap/snapcraft.yaml is a symlink to the root snapcraft.yaml
so it tracks automatically.

cc
2026-04-28 20:29:59 -06:00
vicrodh fc402506bf chore(icons): regenerate icon.icns from current logo
The bundled icon.icns was a 1024x1024 PNG masquerading as an .icns
container (placeholder, since no proper libicns tooling is installed
on the build machine). Regenerated from the current icon.svg so the
artwork at least matches the rest of the branding. A real .icns
container with multiple Apple-spec sizes still needs to be produced
by a macOS contributor.

cc
2026-04-28 20:29:52 -06:00
vicrodh 17730e0233 fix(tray): icon variant picker with Color option and mono labels
Three issues with the 1.2.9-pre tray icon override:

  - The mono PNGs filled the entire canvas, so the tray icon read as
    larger than its panel neighbours (Plasma + GNOME).
  - The dropdown labels were inverted: picking "Light icon" produced
    the dark glyph and vice-versa, because the constants were named
    after the panel they suit ("light panel" -> dark glyph), not after
    the glyph colour the user expects to see.
  - There was no way to pick the colour vinyl logo even on desktops
    where a monochromatic glyph clashes with the rest of the tray.

Rebuild the mono pixmaps (22/32/44/64) from icon-symbolic.svg with a
2px content inset; render a new colour set from icon.svg at the same
sizes; rename the Rust constants to glyph-colour names and introduce
an IconVariant enum so the UI label, the persisted value, and the
file selection all line up.

Dropdown options now read Auto / Mono light / Mono dark / Color in
all five locales. normalize_tray_icon_theme on the Rust side and
normaliseTrayTheme in SettingsView migrate the inverted "light" /
"dark" values stored by 1.2.9-pre to the user-intent equivalents.

cc
2026-04-28 20:29:49 -06:00
vicrodh ee713e3f0c feat(tray): persisted icon theme override with live update on Linux
Auto-detected dark vs light tray glyph fails on GNOME, whose top bar
is permanently dark even when the system color scheme is light, so
the dark glyph disappears against it. Add a persisted user override
("auto" / "light" / "dark") that the SNI tray respects at startup
and on the fly.

Backend pieces:
- TraySettings gains tray_icon_theme: String with "auto" default and
  a normalize_tray_icon_theme() helper that clamps unknown values
  back to "auto". SQLite migration ALTERs the table to add the
  column when an older DB is opened.
- tray_linux_ksni::decode_tray_icons takes an optional override and
  resolve_dark_tray() consults it before falling back to the
  existing GNOME/GTK/KDE auto-detection. init() forwards it.
- LinuxTrayHandle gains set_icon_theme(theme); a new
  TrayUpdate::SetIconTheme variant flows through the dedicated
  worker thread so re-decoding plus handle.update() happen outside
  the tokio runtime (same panic-avoidance pattern used for
  tooltip updates).
- v2_set_tray_icon_theme persists the value, mirrors it to the
  global startup store, and pushes it to the live tray handle so
  the icon flips without restart.

cc
2026-04-28 19:02:46 -06:00
vicrodh 7714d83212 fix(tray): apply tooltip updates from a dedicated thread
ksni 0.3.4 with the "blocking" feature wraps every Handle::update in
Runtime::block_on, which panics when called from inside an existing
tokio runtime — exactly where Tauri commands and our async polling
loop run. The panic was being swallowed by the runtime, so set_track
and set_playing looked like no-ops and the SNI tooltip kept rendering
the static "QBZ / Music Player / Nothing playing" fallback.

Move all updates onto a dedicated std::thread that owns the ksni
blocking handle and processes mutations from a std::sync::mpsc
channel, fully outside any tokio context.

cc
2026-04-28 15:59:25 -06:00
vicrodh 789b0262b6 feat(tray): live tooltip and scroll/middle-click controls on Linux
Replicate the Plasma media plasmoid tray behavior on Linux SNI panels:

- Tooltip now shows live track title, "by Artist", album line, and
  "Middle-click to play/pause" + "Scroll to adjust volume" hints.
- Middle-click (SecondaryActivate) emits tray:play_pause instead of
  toggling the window — the window toggle stays on left-click.
- Mouse wheel (Scroll) emits tray:volume_delta with normalised ticks
  (~120 SNI units per notch -> +/-5% volume per tick), wired to the
  existing handleVolumeChange path on the frontend so QConnect remote
  renderers and the local player both respond.

Architecture: ksni 0.3.4's blocking Handle is now stored as
LinuxTrayHandle in Tauri state. v2_set_media_metadata pushes track
fields into the tooltip, and the playback poll loop mirrors the
play/pause flag so the hint flips while the panel popup is open.

cc
2026-04-28 15:29:04 -06:00
vicrodh 27295dea98 fix(commands_v2): write-lock and debug-log v2_queue_consume_stop_after_if
Per code review on commit 97b5bd79:
- consume_stop_after_if mutates inner state on match (clears the
  marker), so use write().await for semantic consistency with the
  other mutating commands. The inner Mutex made read() work today
  but the intent is mutation.
- Demote the per-call log to debug — this fires on every natural
  track-end and would spam info-level logs during normal listening.

Refs #356.

cc
2026-04-28 13:52:23 -06:00
vicrodh 97b5bd79c7 feat(commands_v2): add four queue commands for stop-after and remove-after
v2_queue_set_stop_after / v2_queue_clear_stop_after / v2_queue_consume_stop_after_if
/ v2_queue_remove_after — thin wrappers over QueueManager via CoreBridge. The
consume command is what the frontend's natural-end handler calls before
advancing; manual skip paths do not call it.

Refs #356.

cc
2026-04-28 13:47:56 -06:00
vicrodh 2ebb874f43 fix(icons): improve Linux tray display and refresh assets (#362)
Returning an empty `icon_name` in `tray_linux_ksni.rs` forces SNI
panels (e.g., KDE Plasma) to render the explicit `IconPixmap` data.
This prevents panels from incorrectly resolving the full-color app icon
from the system theme instead of the intended monochrome tray glyph.

Also updates `src-tauri/icons/icon-symbolic.svg` and regenerates
all `tray-dark-*.png` and `tray-light-*.png` assets.
Adds `static/logo.svg` as the main application logo.
2026-04-28 07:37:48 -06:00
vicrodh f533660a43 fix(tray): rebuild Linux tray icon as monochrome multi-size pixmaps (#362)
The previous tray-light.png / tray-dark.png were 128x128 RGBA glyphs with a
black radial background. GNOME's StatusNotifierItem path expects monochrome
silhouettes around 22px base size and was rendering ours as the "three dots"
fallback.

Regenerate from icons/icon-symbolic.svg using Inkscape with a corrected
square viewBox (149 1 944 944, centred on the path bbox) and emit four
sizes per theme (22/32/44/64). decode_tray_icons() now returns Vec<Icon>
so ksni can offer the panel the best match for its bar height and HiDPI.

cc
2026-04-27 22:52:01 -06:00
vicrodh 5bfb748876 feat(session): opt-in resume playback position on session restore (#317)
Adds a separate "Resume playback position" toggle in Settings >
Playback. Default is OFF — opening qbz the next morning lands on a
clean 0:00 of the saved track, which is the common preference. Users
who do want bookmark-style resume turn the toggle on.

Independent of "Restore session on startup" in the UI (top-level
toggle, not a sub-setting). The seek only applies when both are on:
without persistSession there's no saved track to resume to, so the
flag is inert. Keeping the toggles separate matches the Settings UX
elsewhere and lets users flip persistence on without inheriting the
old #317 behavior we never had.

Backend
-------
- New `PlaybackPreferences::resume_playback_position: bool` (default
  false). Migration adds the column to the existing
  `playback_preferences.db` and the default row gets 0; existing
  installs upgrade transparently.
- Setter `set_resume_playback_position(bool)` on both
  `PlaybackPreferencesStore` and the wrapping `State`.
- Tauri command `v2_set_resume_playback_position` registered in
  lib.rs invoke handler. Get-side already reads the new field via
  the existing `v2_get_playback_preferences`.

Frontend
--------
- `playbackPreferencesStore.ts` — new field on the type and cache,
  new `setResumePlaybackPosition()` exported.
- `SettingsView.svelte` — separate toggle row right below
  "Restore session on startup" (matches the existing Settings rhythm).
- `playerStore.ts` — `pendingSessionRestore` extended with optional
  `positionSecs`. When present, after the restored track loads, the
  position is queued through the existing `pendingSeekPosition` /
  `flushPendingSeek` path so the seek runs against an already-loaded
  stream and respects the in-flight guard. Skip the queue when the
  saved position is at or near 0 (the load already starts there).
- `routes/+page.svelte` — at session restore, reads
  `getCachedPreferences().resume_playback_position` and forwards
  `session.current_position_secs` only when the user opted in. The
  field has been persisted in the schema since v1.2.5 — what changes
  is whether we read it back.

i18n
----
- New keys in 5 locales: `settings.playback.resumePlaybackPosition` /
  `…Desc`.
- `persistSessionDesc` updated everywhere — it used to claim the
  feature restored playback position, which was always misleading
  (the code restored the track but paused at 0:00). The text now
  matches what the toggle does on its own: queue + view, paused at
  0:00. Resume-to-position becomes the explicit ask of the new toggle.

Tests
-----
- Backend: 196 tests pass. Frontend: 0 TS errors.

cc
2026-04-27 21:29:29 -06:00
vicrodh 6ff7aab61e feat(track): show Qobuz subtitle/edition (version) in core renderers (#360)
Qobuz returns a separate `version` field for tracks belonging to remix
albums, reissues, and deluxe editions (e.g. "Player's Ball Mix",
"Nine Inch Noize Version", "Remastered 2024"). The mobile app shows it
parenthesized after the title so remix albums are distinguishable from
originals; qbz only showed the bare title, making remix albums
impossible to navigate (issue #360).

Backend
-------
- Add `version: Option<String>` to qbz_models::Track (the V2 model
  consumed by qbz-qobuz/qbz-library) and to api::models::Track (the
  legacy struct still used by some V2 command paths).
- Add `version: Option<String>` to qbz_models::QueueTrack so the field
  travels through the local queue, persistence, QConnect remote queue,
  and frontend snapshots without being lost in conversion.
- Propagate `version` in every Track→QueueTrack converter:
  qconnect::model_track_to_core_queue_track,
  link_resolver::track_to_queue_track_from_api,
  mixtape::enqueue::track_to_queue_track_from_api and the inline
  builders. Plex/local builders set version=None (no edition concept
  outside Qobuz).
- V2QueueTrack (the wire shape exposed to the frontend) gains the
  field with serde default so older clients keep deserializing.

Frontend
--------
- New `formatTrackTitle(track)` helper in src/lib/utils/trackTitle.ts:
  returns `title` when version is empty, else `title (version)`.
- Add `version?: string | null` to QobuzTrack, Track (UI), PlaylistTrack,
  DisplayTrack, BackendQueueTrack, QueueTrack (UI), and PlayingTrack
  so the field flows from the wire to the renderer.
- Propagate `version` through the four buildQueueTrackFrom* builders
  in trackActions.ts and through the QobuzAlbum→AlbumDetail adapter.
- Use formatTrackTitle in the high-traffic renderers:
  - NowPlayingBar (currentTrack derived in routes/+page.svelte)
  - QueuePanel: now-playing label, upcoming-track rows, history rows
  - VirtualizedTrackList: default getTrackTitle accessor
  - AlbumDetailView: TrackRow title binding
  - PlaylistDetailView: TrackRow title binding
  - SearchView: popular-track card label and the two track-list
    rows (Tracks tab + cross-search "All" view)

Tests
-----
- 196 lib tests pass (no behavior change for the no-version path).
- npm run check: 0 errors.

Follow-up: more views still pass `{track.title}` directly (HomeView,
ArtistDetailView, FavoritesView, ForYouTab, MixtapeCollectionDetailView,
LocalLibraryView, the various Suggest/Top/Label views, the immersive
panels, the miniplayer surface, and the Track*Modal stack). They all
render UI Track-shaped objects that now carry version, so each is a
mechanical "import formatTrackTitle + replace {track.title} with
{formatTrackTitle(track)}". Doing them in a follow-up commit to keep
this one reviewable.

cc
2026-04-27 18:45:17 -06:00
vicrodh 532ec89095 refactor(qconnect): extract QconnectServiceState to service.rs
The final extraction of Alcance A. Move the public service facade
(QconnectServiceState struct + impl + Default impl, plus the spawned
event-loop task, the renderer/queue snapshot queries, the controller-
handoff helpers, send_command/send_renderer_report, the deferred
renderer-join plumbing, and the bootstrap_remote_presence pending-
action machinery) from mod.rs into a dedicated service submodule.

Items moved:
- struct QconnectRuntime, QconnectServiceInner
- pub struct QconnectServiceState (re-exported via `pub use`)
- impl QconnectServiceState (~50 methods) and impl Default
- bootstrap_remote_presence, deferred_renderer_join,
  clear_pending_if_matches
- queue_payload_track_preview, update_lifecycle_state_if_running,
  emit_qconnect_diagnostic (private helpers used only by the service)
- Constants: JOIN_SESSION_REASON_CONTROLLER_REQUEST,
  QCONNECT_PLAY_TRACK_HANDOFF_WAIT_MS,
  QCONNECT_PLAY_TRACK_HANDOFF_POLL_MS

All methods on QconnectServiceState are pub(super) so the commands
submodule (which was already extracted) can dispatch through them.
The Default impl uses plain `fn default()` (visibility qualifiers are
not permitted in trait impls).

mod.rs now contains only the cross-submodule glue (~140 lines):
shared constants (PLAYING_STATE_*, AUDIO_QUALITY_*, BUFFER_STATE_OK,
DEFAULT_QCONNECT_CHANNEL_COUNT, QCONNECT_REMOTE_QUEUE_SOURCE), the
QconnectRemoteSyncState accumulator, qconnect_now_ms,
qconnect_repeat_mode_from_loop_mode, normalize_volume_to_fraction,
model_track_to_core_queue_track, and the module declarations.

session.rs and commands.rs imports re-pathed to point at submodules
directly (super::transport::*, super::queue_resolution::*,
super::session::*, super::service::*) instead of going through
re-exports in mod.rs.

mod.rs: 1820 → 142 lines (-92%). service.rs: 1689 lines.

Final layout:

  src-tauri/src/qconnect/
  ├── mod.rs              ( 142)  module glue
  ├── types.rs            ( 273)  wire types, payloads
  ├── session.rs          ( 374)  topology, renderer snapshots
  ├── queue_resolution.rs ( 481)  cursors, shuffle order
  ├── corebridge.rs       ( 566)  apply_renderer_command, materialize
  ├── track_loading.rs    ( 337)  streaming, FLAC probe, dedup
  ├── event_sink.rs       ( 461)  QconnectEventSink + dispatch
  ├── transport.rs        ( 386)  credentials, device identity
  ├── service.rs          (1689)  QconnectServiceState facade
  ├── commands.rs         ( 849)  Tauri invoke handlers
  └── tests.rs            (1044)  37 unit tests

From 6371 lines in 1 monolith → 6602 lines across 11 focused files,
each with a single clear responsibility. mod.rs went 6371 → 142 lines
(-98%). All 37 tests pass. Alcance A complete; the codebase is ready
for unified-binary Phase 0 to resume.

cc
2026-04-27 18:00:43 -06:00
vicrodh 49f56d563e refactor(qconnect): extract test module into qconnect/tests.rs
Move the 37 unit tests (1044 lines, all #[cfg(test)] inside the old
mod tests {} block in mod.rs) into a dedicated tests.rs submodule.
mod.rs keeps only the declaration `#[cfg(test)] mod tests;`.

Test imports are unchanged — they still use `super::*` paths into the
focused submodules established in steps 2-9, which is exactly the
intended layout.

This concludes Alcance A of the qconnect refactor. Full layout:

  src-tauri/src/qconnect/
  ├── mod.rs              (1820 lines) — QconnectServiceState impl,
  │                                       glue, internal structs
  ├── types.rs            ( 273 lines) — wire types, request payloads
  ├── session.rs          ( 374 lines) — topology, renderer snapshots
  ├── queue_resolution.rs ( 481 lines) — cursors, indices, shuffle order
  ├── corebridge.rs       ( 566 lines) — apply_renderer_command,
  │                                       materialize, projection
  ├── track_loading.rs    ( 337 lines) — streaming, FLAC probe, dedup
  ├── event_sink.rs       ( 461 lines) — QconnectEventSink + dispatch
  ├── transport.rs        ( 386 lines) — credentials, device identity
  ├── commands.rs         ( 849 lines) — Tauri invoke handlers + report
  │                                       helpers
  └── tests.rs            (1044 lines) — 37 unit tests

From 6371 lines in 1 monolith → 7591 lines across 10 focused files
with clear responsibility boundaries. mod.rs went 6371 → 1820 (-71%).
Each submodule is independently navigable; visibility is constrained
via pub(super) so re-exports stay intentional. All 37 tests pass.

cc
2026-04-27 17:30:08 -06:00
vicrodh 7bd5562203 refactor(qconnect): extract Tauri commands to commands.rs
Move all 30 #[tauri::command] handlers (registered in lib.rs's
invoke_handler!) plus the renderer-report helpers they own into a
dedicated commands submodule:

Tauri commands (re-exported via `pub use commands::*` so lib.rs and
external callers keep their import paths unchanged):
- v2_qconnect_connect, _disconnect, _status, _send_command
- v2_qconnect_evaluate_queue_admission,
  _send_command_with_admission, _join_session, _set_player_state
- v2_qconnect_toggle_play_if_remote, _play_track_if_remote,
  _skip_next_if_remote, _skip_previous_if_remote,
  _set_volume_if_remote, _mute_if_remote, _stop_if_remote,
  _toggle_shuffle_if_remote, _cycle_repeat_if_remote,
  _set_autoplay_mode_if_remote, _autoplay_load_tracks_if_remote
- v2_qconnect_set_active_renderer, _set_volume, _set_loop_mode,
  _mute_volume, _set_max_audio_quality, _ask_for_renderer_state
- v2_qconnect_queue_snapshot, _renderer_snapshot, _session_snapshot
- v2_qconnect_report_playback_state, _report_volume
- v2_qconnect_get_device_name, _set_device_name
- v2_get_hostname

Renderer-report helpers moved alongside the report command:
- classify_qconnect_audio_quality,
  build_qconnect_file_audio_quality_snapshot,
  resolve_active_playback_audio_quality
- should_skip_renderer_report_due_to_stale_snapshot,
  determine_queue_lookup_report_strategy,
  should_report_queue_item_ids_for_renderer_state

mod.rs deps that commands.rs reaches via super:: are now pub(super):
- BUFFER_STATE_OK, DEFAULT_QCONNECT_CHANNEL_COUNT
- AUDIO_QUALITY_UNKNOWN/CD/HIRES_LEVEL1/LEVEL3 (the rest were
  already pub(super))
- QconnectServiceState.custom_device_name field

Test imports re-pathed to super::commands::* for the helpers that
moved with the report command.

Pure relocation. mod.rs: 3673 → 2865 lines. commands.rs: 847 lines.
All 37 qconnect tests pass.

cc
2026-04-27 17:24:22 -06:00
vicrodh b4a32aec5d refactor(qconnect): fix test module imports broken by submodule split
Steps 4-8 moved items into focused submodules but only verified `cargo
check` (lib only) — tests under #[cfg(test)] in mod.rs continued
referring to items via plain `super::FunctionName`, which only resolved
when the items were still in mod.rs.

This commit re-paths the test imports to point at their actual
submodules:

- decode_hex_channel, parse_subscribe_channels, default_qconnect_device_info
  → super::transport::*
- find_unique_renderer_id, refresh_local_renderer_id,
  QconnectFileAudioQualitySnapshot → super::session::*
- resolve_controller_queue_item_from_snapshots,
  resolve_queue_item_ids_from_queue_state, QconnectRemoteSkipDirection,
  resolve_core_shuffle_order, QconnectControllerQueueItemResolution
  → super::queue_resolution::*
- build_session_renderer_snapshot → super::session::*
- should_reload_remote_track → super::track_loading::*
- queue_state_needs_materialization → super::corebridge::*

Promoted decode_hex_channel and parse_subscribe_channels to pub(super)
in transport.rs, and should_reload_remote_track to pub(super) in
track_loading.rs so the tests can reach them across the submodule
boundary.

All 37 qconnect tests pass.

cc
2026-04-27 17:04:08 -06:00
vicrodh c9285a33e9 refactor(qconnect): extract device identity and transport config to transport.rs
Move 14 items that own QConnect transport setup, device identity, and
credential discovery into a dedicated transport submodule:

Device identity (pub(super)):
- default_qconnect_device_info / default_qconnect_device_info_with_name
- resolve_qconnect_device_uuid (with QCONNECT_DEVICE_UUID OnceLock)
- resolve_system_hostname, resolve_default_qconnect_device_name

Custom-device-name persistence (pub(super)):
- load_persisted_device_name, persist_device_name
  (qconnect_settings_db_path stays private to the submodule)

Transport config + credentials (pub(super) + private):
- resolve_transport_config (handles env-var fallbacks, qws/createToken
  auto-discovery, subscribe-channel parsing)
- fetch_qconnect_transport_credentials (private)
- normalize_opt_string, parse_subscribe_channels, decode_hex_channel
  (private)

Diagnostics (pub(super)):
- hex_preview (used by transport-event debug logs in mod.rs)

Constants moved to transport.rs (private to the submodule):
- DEFAULT_QCONNECT_DEVICE_BRAND/MODEL/TYPE/SOFTWARE_PREFIX
- VOLUME_REMOTE_CONTROL_ALLOWED
- QCONNECT_QWS_TOKEN_KIND, QCONNECT_QWS_CREATE_TOKEN_PATH

Constants kept in mod.rs but promoted to pub(super) so transport.rs
can borrow them: AUDIO_QUALITY_MP3, AUDIO_QUALITY_HIRES_LEVEL2.

mod.rs imports OnceLock and Uuid removed (now confined to transport).

Pure relocation. mod.rs: 4022 → 3673 lines. transport.rs: 386 lines.
Total qconnect/ submodules now: 8 (types, session, queue_resolution,
corebridge, track_loading, event_sink, transport, plus residual mod.rs).

cc
2026-04-27 15:48:30 -06:00
vicrodh 8391ef46df refactor(qconnect): extract QconnectEventSink impl to event_sink.rs
Move the Tauri-side event sink (struct TauriQconnectEventSink + the
QconnectEventSink trait impl + the apply_session_management_event
dispatcher) into a dedicated event_sink submodule.

This is the seam between the qconnect-app crate's event stream and
qbz's local state — every WS-driven event (renderer updated, queue
updated, command applied, session-management messages) flows through
on_event. Splitting it out makes the dispatch graph navigable
without scrolling through 4000+ unrelated lines.

apply_session_management_event handles 9 message types:
SESSION_STATE, ADD_RENDERER, UPDATE_RENDERER, REMOVE_RENDERER,
ACTIVE_RENDERER_CHANGED, RENDERER_STATE_UPDATED, VOLUME_CHANGED,
VOLUME_MUTED, MAX_AUDIO_QUALITY_CHANGED, LOOP_MODE_SET — plus three
post-dispatch hooks (apply_remote_loop_mode_to_corebridge,
sync_local_playback_for_renderer_ownership, sync_active_renderer_
projection).

mod.rs:
- TauriQconnectEventSink moved to event_sink module
- qconnect_now_ms marked pub(super) for cross-module access
- async_trait, qconnect_app::{QconnectAppEvent, QconnectEventSink}
  imports removed (now confined to event_sink.rs)
- The session/queue_resolution imports trimmed to only what mod.rs
  itself still references

Pure relocation. mod.rs: 4460 → 4022 lines. event_sink.rs: 425 lines.
Total qconnect/ submodules now: 7 (types, session, queue_resolution,
corebridge, track_loading, event_sink, plus the residual mod.rs).

cc
2026-04-27 15:42:05 -06:00
vicrodh 579aafbe2c refactor(qconnect): extract remote track streaming and load dedup to track_loading.rs
Move 7 items that own remote track loading (HTTP streaming, FLAC probe,
fallback download, and the load-attempt dedup window) into a dedicated
track_loading submodule:

- LOAD_ATTEMPT_DEDUP_WINDOW (5s constant)
- should_reload_remote_track (track-id-only reload predicate)
- load_remote_track_into_player (top-level load with streaming-then-
  download fallback)
- is_recent_load_attempt (dedup check against sync_state)
- ensure_remote_track_loaded (pub(super) — orchestrator called by
  corebridge::apply_renderer_command_to_corebridge and the V2
  play_track_handoff path)
- QconnectRemoteStreamInfo (FLAC probe result)
- stream_remote_track_into_player, probe_remote_stream_info,
  download_and_stream_remote_track, download_remote_audio

corebridge.rs now imports ensure_remote_track_loaded directly from
super::track_loading instead of going through mod.rs.

Pure relocation. mod.rs: 4777 → 4460 lines. track_loading.rs: 337 lines.
Total qconnect/ submodules now: 6 (types, session, queue_resolution,
corebridge, track_loading, plus the residual mod.rs).

cc
2026-04-27 15:05:19 -06:00
vicrodh 69ef5de5a0 refactor(qconnect): extract CoreBridge bridging logic to corebridge.rs
Move 6 functions that bridge cloud-emitted QConnect events to the
local CoreBridge audio engine into a dedicated corebridge submodule:

- queue_state_needs_materialization (dedup comparator)
- apply_remote_loop_mode_to_corebridge
- apply_renderer_command_to_corebridge (SetState/SetVolume/MuteVolume/
  SetLoopMode/SetActive handler — ~180 lines, the busiest function)
- sync_corebridge_remote_shuffle_projection (shuffle projection with
  identity-shuffle deferral; remains private to corebridge)
- materialize_remote_queue_to_corebridge (queue replication, ~210 lines)
- align_corebridge_queue_cursor

mod.rs deps that corebridge.rs reaches via super:: are now pub(super):
- PLAYING_STATE_PLAYING/PAUSED/STOPPED/UNKNOWN constants
- normalize_volume_to_fraction, qconnect_repeat_mode_from_loop_mode,
  model_track_to_core_queue_track
- ensure_remote_track_loaded (still in mod.rs, will move to
  track_loading.rs in a follow-up step)

corebridge.rs imports queue_resolution::* directly for the cursor and
order helpers, and session::is_peer_renderer_active for the echo-reset
gate.

Pure relocation. mod.rs: 5304 → 4777 lines. corebridge.rs: 565 lines.

cc
2026-04-27 15:02:14 -06:00
vicrodh 7ea9511c26 refactor(qconnect): extract queue cursor and order resolution to queue_resolution.rs
Move 18 items that own the protocol-side queue/cursor/order math from
mod.rs into a dedicated queue_resolution submodule:

Types (pub(super)):
- QconnectOrderedQueueCursor — Queue/Autoplay cursor abstraction
- QconnectRemoteSkipDirection — controller next/previous direction
- QconnectControllerQueueItemResolution — skip resolution outcome

Helpers (pub(super) when used by other submodules, fn otherwise):
- is_valid_ordered_queue_shuffle_order
- ordered_queue_cursors, queue_item_track_id_for_cursor,
  normalized_queue_item_id_for_cursor
- find_cursor_index_by_queue_item_id, find_cursor_index_by_track_id,
  find_cursor_index_by_track_id_before
- resolve_current_cursor_index_from_snapshots,
  resolve_controller_queue_item_from_snapshots,
  resolve_queue_item_ids_from_queue_state
- dedupe_track_ids
- resolve_remote_start_index, resolve_core_shuffle_order
- is_cloud_placeholder_current_queue_item,
  normalize_current_queue_item_id_from_queue_state

session.rs now imports cursor helpers directly from
super::queue_resolution::* instead of going through mod.rs.

Pure relocation. mod.rs: 5766 → 5297 lines. queue_resolution.rs: 460 lines.

cc
2026-04-27 14:57:54 -06:00
vicrodh a608ba2e06 refactor(qconnect): extract session topology and renderer snapshots to session.rs
Move 16 items from mod.rs to a dedicated session submodule:

Types:
- QconnectSessionState (pub) — session topology surfaced to frontend
- QconnectRendererInfo (pub) — renderer device descriptor
- QconnectSessionRendererState (pub(crate)) — per-renderer cached state
- QconnectFileAudioQualitySnapshot (pub(crate)) — audio quality dedup
- QconnectRendererReportDebugEvent (pub(crate)) — renderer report telemetry

Helpers (all pub(super)):
- refresh_local_renderer_id, find_unique_renderer_id
- normalize_active_renderer_id, is_peer_renderer_active, is_local_renderer_active
- sync_session_renderer_active_flags, ensure_session_renderer_state
- build_session_renderer_snapshot, build_effective_renderer_snapshot
- build_visible_queue_projection, queue_item_snapshot_for_cursor
- cache_renderer_snapshot

mod.rs deps that session.rs reaches via super:: are now pub(super):
- QconnectRemoteSyncState and all its fields
- QconnectOrderedQueueCursor
- ordered_queue_cursors, find_cursor_index_by_queue_item_id,
  find_cursor_index_by_track_id, normalize_current_queue_item_id_from_queue_state
- default_qconnect_device_info, resolve_qconnect_device_uuid

Pure relocation. mod.rs: 6109 → 5766 lines. session.rs: 372 lines.

cc
2026-04-27 14:51:33 -06:00
vicrodh e5c0261158 refactor(qconnect): extract wire types and request payloads to types.rs
Move 22 public types (QconnectConnectOptions, QconnectLifecycleState,
QconnectConnectionStatus, QconnectVisibleQueueProjection, the 20-variant
QconnectOutboundCommandType + 5 request payload structs, the 4 admission/
handoff types, the 8 device/session/queue payloads) into a dedicated
types.rs submodule. mod.rs re-exports via `pub use types::*` so external
callers (lib.rs invoke handlers, commands_v2/queue.rs) keep their import
paths.

Pure data and enum-mapping impls only. Two helper methods on
QconnectOutboundCommandType (to_queue_command_type,
requires_remote_queue_admission) become pub(super) since they are only
used from within the qconnect module tree.

mod.rs: 6371 → 6109 lines. types.rs: 273 lines.

cc
2026-04-27 14:17:19 -06:00