Commit Graph
100 Commits
Author SHA1 Message Date
vicrodh 523cce46fd fix(library): apply per-track artwork fix to second scan loop, suppress legitimate users
A code review of the previous commit surfaced a second scan loop in
legacy_compat.rs that had the same destructive update_album_group_artwork
pattern. Apply the same fix: per-folder cache + drop the destructive
call. Same intent and shape as the main loop fix.

The deprecated marker also surfaced three callers that are NOT bugs
and stay as-is, with #[allow(deprecated)] + a comment to make the
intent explicit:
- CUE ingestion: a CUE sheet is one physical album, so a single cover
  for all CUE tracks is semantically correct.
- v2_library_set_album_artwork: user-initiated set-cover-for-album, by
  design propagates to all tracks.
- Discogs backfill: only fires for albums with no existing artwork, so
  there is nothing per-track to destroy.

cc
2026-05-06 16:38:14 -06:00
vicrodh 7eba2749f5 fix(library): preserve per-track artwork during scan, cache folder lookups
The scan loop called update_album_group_artwork after every successful
track insert, which propagated one track's artwork to every track in
the same group. Tracks with unique embedded covers ended up sharing
whichever cover was processed last. Drop the call; per-track artwork
is resolved correctly by extract_artwork (embedded) + find_folder_artwork
(cover.jpg fallback) before insert_track.

Replace the propagation's caching effect with an in-memory per-folder
HashMap that memoizes find_folder_artwork for the duration of the scan
run, avoiding repeated folder walks for tracks without embedded art.

Mark update_album_group_artwork #[deprecated] so no future caller
reintroduces the bug. Keep it compilable.

Existing libraries already have collapsed artwork paths in the DB and
need a re-scan to recover per-track covers; called out in release notes.

cc
2026-05-06 16:32:27 -06:00
vicrodh 74289fb682 Merge branch 'feature/internal/qconnect-honor-peer-seek-on-local-renderer' into pre-release
cc
2026-05-06 14:21:56 -06:00
vicrodh 6afbc94171 fix(qconnect): honor peer-controller seeks when qbz is the active renderer
Fixes #387: seeks issued from the official Qobuz mobile app while qbz
acts as the active renderer were silently dropped — audio kept playing
from the original position while the controller's progress bar locked.

Two independent gates were swallowing the command between the
WebSocket and the audio thread; either alone is enough to break the
flow, so both move together.

1. Upstream filter (crates/qconnect-app/src/app.rs)

   The is_echo guard treated every SET_STATE with playing_state=None
   AND current_track=None as a pure server echo and skipped emitting
   RendererCommandApplied. iOS sends a real seek as exactly that
   shape (only current_position_ms set, the rest unchanged), so the
   command never reached the bridge handler.

   Add current_position_ms.is_none() to the guard. Pure echoes still
   match (server echoes carry only next_track); position-only seeks
   pass through.

2. Downstream gate (src-tauri/src/qconnect/corebridge.rs)

   The seek branch was conditioned on peer_renderer_active — i.e. it
   only acted when qbz was the controller observing a peer renderer,
   never when qbz was the renderer itself. Removed; the existing
   is_echo_reset and abs_diff > 2 checks already filter the redundant-
   seek case the gate was originally added to defend against (147bcbd7).
   Drop the now-unused is_peer_renderer_active import.

Verified manually: seeks from iPhone (renderer_id=3) targeting qbz
(renderer_id=2) at ~1/4 and ~5/6 of a 150s track now translate to
local audio jumps to the correct positions.

cc
2026-05-06 14:21:27 -06:00
vicrodh 86e22bf886 Merge branch 'feature/internal/seekbar-uniform-poll-cadence' into pre-release
cc
2026-05-06 12:51:32 -06:00
vicrodh 1e16ec52f5 fix(playback): poll player state at uniform 250ms cadence
The previous adaptive scheme dropped to 5s when idle and 1s when
paused-with-track. PLAYBACK_STATE_WAKEUP catches synchronous V2
mutations (pause/resume/stop/seek) instantly, but play_data() is
asynchronous: it queues an AudioCommand::Play over a channel and
returns. The audio thread sets is_playing/start_playback_timer only
after decoder + device init (500-2000ms), so the wakeup fires too
early — the loop reads stale state, decides 'idle' or 'paused', and
sleeps for the full long branch before catching the actual transition.

current_position() is wall-clock based (now - playback_start_millis),
so by the time the next idle tick arrives, position is already past
second 1 and the seekbar appears to jump straight to 2+.

Polling uniformly at 250ms catches the async transition within one
cadence and keeps the seekbar showing 0:00 -> 0:01 -> 0:02 from the
start. Cost is negligible (~10 atomic reads x 4 Hz), and should_emit
filters every no-change iteration so the frontend wire is unaffected.

cc
2026-05-06 12:34:01 -06:00
vicrodh b0716962e0 Merge branch 'feature/internal/seekbar-cold-start-latency' into pre-release
cc
2026-05-06 12:26:11 -06:00
vicrodh 8288350559 feat(playback): wake state polling loop on V2 playback mutations
The background task in lib.rs that emits playback:state to the frontend
sleeps adaptively: 250ms while playing, 1s when paused-with-track, and
5s when idle. The 5s idle branch leaves the seekbar blank for several
seconds on cold start because the loop only reads new state — and only
then emits — after the sleep ends, even though audio is already playing.

Add a tokio::sync::Notify (PLAYBACK_STATE_WAKEUP) shared via the
existing commands_v2::helpers module. Race the sleep against
PLAYBACK_STATE_WAKEUP.notified() with tokio::select. Each V2 command
that mutates playback state (play_track, pause, resume, stop, seek)
calls notify_one after the underlying bridge call succeeds, so the
loop wakes within milliseconds and emits the new state immediately.

notify_one stores up to one permit when no waiter is parked, so a
notification fired between iterations is delivered on the next select.

Make commands_v2::helpers a pub(crate) module so the static is
reachable from lib.rs and the V2 command modules.

cc
2026-05-06 11:42:21 -06:00
vicrodh 0aa923e32a Merge branch 'feature/internal/offline-cache-manager' into pre-release
cc
2026-05-06 10:49:52 -06:00
vicrodh 534c81016d feat(offline_cache_manager): cache size limit modal
Adds a pencil icon next to the usage bar in the manager's stats header.
Click opens a Modal (shared Modal.svelte) with a number input bound to
the current limit in GB plus Save / Cancel buttons. Validates >= 5 GB
client-side and surfaces a toast on success or error. Save calls
v2_set_offline_cache_limit which now persists across restarts. New
i18n keys land in all five locales (en/es/de/fr/pt).

cc
2026-05-06 10:24:55 -06:00
vicrodh 1b07036454 feat(offline_cache): enforce cache size limit on new track downloads
Pre-flight check_cache_limit reads the current total_size_bytes and
returns an informative error if the cache is already at or over the
configured limit. Wired into v2_cache_track_for_offline (single) and
v2_cache_tracks_batch_for_offline (batch). Re-download paths are not
gated since they target tracks already counted toward the total. The
check intentionally does not predict the new track's size; preventing
cache from growing further once at the limit is enough for v1.

cc
2026-05-06 10:23:27 -06:00
vicrodh 362f3154ac feat(offline_cache): persist cache size limit across restarts
Adds cache_limit_bytes column to offline_settings (idempotent ALTER
TABLE) plus get/set methods on the OfflineState store. v2_set_offline_cache_limit
now writes the value to disk in addition to the in-memory cache_state.
At session activation, OfflineCacheState::apply_persisted_limit reads
the stored value and seeds the in-memory limit so the user's chosen cap
survives app restarts. Legacy installs with NULL persisted value keep
the in-memory 5 GB default seeded by init_at.

cc
2026-05-06 10:23:18 -06:00
vicrodh 7ca515be7e feat(offline_cache): bump default cache size limit from 2 GB to 5 GB
Hi-res FLAC tracks at 24-bit/192 kHz routinely run 60-100 MB each, so a
2 GB cap held only ~25-30 album downloads before users had to clear
space. 5 GB is the new floor for the default; users can raise it from
the manager view and the value persists across restarts (next commits).

cc
2026-05-06 10:20:50 -06:00
vicrodh e71cd71f60 feat(i18n): rename Re-download album to Re-download all tracks
The old label implied the entire album would be downloaded, but the
action only re-fetches tracks that were previously cached. Updated in
all 5 locales (en/es/de/fr/pt). The translation key stays
offlineManager.row.actions.redownloadAlbum.

cc
2026-05-06 10:19:03 -06:00
vicrodh a1b29e4f72 feat(offline_cache_manager): live progress rings on album and track rows
Replaces the static '⟳' / '…' status glyphs with the same SVG progress
ring used by DownloadButton. Album-row ring shows the average progress
across currently-downloading tracks; track-row ring tracks per-track
progressPercent. Queued state renders an indeterminate spinner
(75%-fill rotating). Same pattern, no new dependencies.

cc
2026-05-06 10:13:30 -06:00
vicrodh f72f21838e feat(offline_cache_manager): show album cover artwork in rows
Backend cache rows do not store an artwork URL, so on each fresh load we
hydrate per album_id via v2_get_album in batches of 8 with parallel
Promise.all and an in-memory cache. Failures per album are tolerated and
recorded as null so the row falls back to the placeholder. Re-rollups
triggered by progress events and remove operations re-apply the cache so
covers do not flicker on state updates.

cc
2026-05-06 10:11:33 -06:00
vicrodh 695063916f feat(offline_cache_manager): expand albums by default
Auto-expand every album's track list on each rollup so the user sees
per-track status (size, badges, action menus) without needing to click
each row open. The set is sticky for the session — collapsing an album
manually persists until the next loadAll, and a fresh load re-expands.

cc
2026-05-06 10:08:43 -06:00
vicrodh 23995067a9 fix(offline_cache_manager): correct fully-cached batch response shape
v2_check_albums_fully_cached_batch returns a HashMap<String, bool> which
serializes as a JSON object, not an array of tuples. The previous typing
made new Map(result) silently throw with object-is-not-iterable, which
aborted loadAll before buildRollup ran and forced the empty state on
every load. Use Record<string, boolean> + Object.entries instead, and
log loadAll errors so future shape mismatches do not fail silently.

cc
2026-05-06 10:06:25 -06:00
vicrodh 17452f5691 feat(settings): replace Open folder button with Manage offline cache navigation
Repoints the handler from v2_open_offline_cache_folder to navigateTo('offline-manager').
The encrypted CMAF bundle layout makes the folder content meaningless to
users; the new in-app manager is the correct affordance for free-up-space
and per-album/per-track operations.

cc
2026-05-06 08:19:53 -06:00
vicrodh ad0ae8cb0e feat(routing): add offline-manager view branch and exclude from last_view persistence
Wires the new OfflineCacheManagerView into +page.svelte's view switch with
onBack returning to settings, onGoToAlbum delegating to handleAlbumClick,
and onGoToFavorites returning to the user's last favorites tab. Adds
'offline-manager' to the ViewType union so the new branch type-checks.

Excludes 'offline-manager' from last_view persistence by routing it through
getSessionFallbackView('offline-manager') -> 'settings' (its entry point),
guarding against Phantom CSS Error Cause 3 on rehydration when the cache DB
is not yet ready at first paint.

cc
2026-05-06 08:17:13 -06:00
vicrodh 41ca8ea65b feat(views): polish empty state CTA in offline cache manager
Adds Go to Favorites button wired to the onGoToFavorites prop. Removes
the eslint suppression now that all three navigation props are used.

cc
2026-05-06 08:05:43 -06:00
vicrodh c4fe11aa71 feat(views): add album rows with expand and per-track actions to manager
Album rows show cover placeholder, title/artist, cached ratio, dominant
quality, size, and status badge. Menu actions: re-download album, retry
failed (visible only when applicable), remove from offline (with ask()
confirm), go to album. Expanded rows show per-track actions
(re-download, remove). Singles pseudo-album omits redownload-album and
go-to-album; remove iterates per-track.

cc
2026-05-06 08:03:38 -06:00
vicrodh 026e654295 feat(views): add alpha-index artist rail to offline cache manager
Rail groups artists by leading letter with sticky headers. Horizontal
alphabet bar above scrolls the rail to the chosen letter. Inactive
letters dimmed.

cc
2026-05-06 07:59:42 -06:00
vicrodh b4b4b050d8 feat(views): add offline cache manager shell with header and stats
Header + stats section with usage bar, sort dropdown, and Show only failed
toggle. Loading and empty states are placeholders for the next tasks
(left rail in P6.2, right pane in P6.3, empty CTA in P6.4).

cc
2026-05-06 07:57:11 -06:00
vicrodh 6807259813 feat(stores): wire reactive state and V2 invokes for offline cache manager
Adds the runtime API: loadAll, subscribeToProgress, removeAlbum,
redownloadAlbum, redownloadTrack, removeTrack, selectArtist,
toggleExpand, setSort, setShowOnlyFailed. Reuses buildRollup on every
mutation so derived ArtistGroup state stays consistent.

Subscribes to four downloader events: caching_started, caching_progress,
caching_processed (the canonical Ready signal), and caching_failed. The
intermediate caching_completed event is intentionally ignored since
caching_processed already covers post-processing completion.

Renamed the file to offlineCacheManagerStore.svelte.ts because Svelte 5
runes ($state, etc.) are only legal inside .svelte / .svelte.ts /
.svelte.js modules. Updated the vitest import in the sibling .test.ts
to match the new path.

cc
2026-05-06 07:53:10 -06:00
vicrodh 4f624e038d feat(stores): add offlineCacheManagerStore types and buildRollup pure function
Pure function maps flat CachedTrackInfo[] into ArtistGroup[] with album
sub-groups, status rollup, quality dominance, and Singles bucket for
albumId-less tracks. Covered by vitest with 7 cases.

cc
2026-05-06 07:48:33 -06:00
vicrodh 50e8d65ce6 feat(i18n): add offlineManager namespace and retitle Manage offline cache button
Five-locale update for the new offline cache manager view (en/es/de/fr/pt).
Repurposes the settings.offlineLibrary.openFolder button label to point at
the manager view; the underlying handler is changed in a later task.

cc
2026-05-06 07:44:50 -06:00
vicrodh 49e1e5951e feat(tauri): register offline cache manager commands
cc
2026-05-06 07:39:37 -06:00
vicrodh 57dc9f0a73 feat(commands_v2): add offline cache manager commands
v2_remove_cached_album, v2_redownload_cached_album (with failed_only flag),
and v2_redownload_cached_track. Thin dispatchers over offline_cache::*
following V2 cleanroom rules.

cc
2026-05-06 07:35:49 -06:00
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 4fe2390129 chore(scripts): update v2 backend purity check for commands_v2 directory layout
The script was created when commands_v2 was a single file. After the
refactor to a directory, the path was stale and the script errored on
missing file. Also switch from rg to grep so the check works in
environments without ripgrep installed.

cc
2026-05-05 22:29:01 -06:00
Victor RH e2ea853c27 Merge PR #384: skip Sidebar isOffline $effect on initial subscription
Skip 's first fire so it does not race with onMount → loadSidebarData(). Eliminates a double v2_get_user_playlists call on cold start.

cc
2026-05-05 18:29:37 -06:00
vicrodh 3e26cdfe11 Merge branch 'bugfix/internal/btn-primary-contrast' into pre-release
WCAG-aware btn-primary-text token across all themes (62 components),
contrast-driven auto-theme generator, plus two svelte-warning fixes.

cc
2026-05-05 17:59:27 -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 cdf99f9709 Merge branch 'feature/internal/node24-bump' into pre-release
Bumps Node 20 to Node 24 in CI workflows and Flatpak manifest.
Cherry-picked from PR #377 (Mar0xy), Discord IPC parts intentionally
deferred until that feature actually exists.

cc
2026-05-05 12:14:31 -06:00
vicrodh a912ced3d0 build: bump Node 20 to Node 24 across CI workflows and Flatpak manifest
Cherry-picked from PR #377 (thanks @Mar0xy) — Node-only portion, no
Discord IPC additions:

- 4 workflows (release-{linux,linux-aarch64,macos,flatpak}.yml) now
  install Node 24
- packaging/flatpak/com.blitzfc.qbz.yml: SDK extension node20 → node24,
  build append-path and npm_config_nodedir aligned

Forcing function: @icons-pack/svelte-simple-icons@7.x already in use
declares engines.node >=24, so Node 20 produces an EBADENGINE warning
in CI. Node 20 also reached EOL on 2026-04-30. Node 24 has been LTS
since October 2025 (~7 months of maturity).

Discord IPC permissions and qbz-wrapper script from #377 were
intentionally NOT cherry-picked — there's no Discord RPC feature in
QBZ today (#336 is still planned but not implemented), so adding the
sandbox permission and the launch wrapper now would expand the
attack surface for nothing. They'll land together with the actual
feature when it ships.

cc
2026-05-05 12:10:07 -06:00
vicrodh 0b181b0530 Merge branch 'feature/internal/multiselect-album-detail-381' into pre-release
Adds full multi-select coverage to the local library album detail view
per #381 — select-all checkbox, shift-click range, and Ctrl/Cmd+A
scoped to the open album with cross-view selection preserved.

cc
2026-05-05 12:01:21 -06:00
vicrodh 19a426f8ad feat(local-library): full multi-select coverage in album detail view (#381)
The album detail view (after drilling into a local album) had the
select-mode toggle button but was missing the rest of the multi-select
UX:

- Select-all checkbox in the track-list-header (only renders when
  select mode is active). State is derived from albumTracks vs
  selectedTrackIds so it accurately reflects partial / all / none
  for the currently-open album.
- Shift-click range support: TrackRow's onToggleSelect now passes
  the MouseEvent up. A new toggleAlbumDetailTrackSelect computes
  the absolute index in albumTracks (so ranges work across discs)
  and reuses applyShiftRange.
- Ctrl/Cmd+A while in album detail now scopes to the open album
  and unions with any selection already built up in other views,
  rather than replacing with library-wide tracks.

The toggle button itself (SquareCheckBig) was already wired — only
the missing pieces are added here. Cross-view selection composition
(tracks tab + album detail share selectedTrackIds) is preserved.

cc
2026-05-05 11:51:39 -06:00
vicrodh 05d7d8645d Merge branch 'bugfix/internal/playlist-duplicate-track-selection' into pre-release
Fixes #386 (duplicate-track selection collapse in playlist editing) plus
two related multi-select issues found while testing:
- Left-click on TrackRow checkbox now reliably toggles selection
  (input made pointer-transparent, parent div owns the click).
- Selected row background bumped from 12% to 22% accent so the
  visual cue is readable next to the saturated checkbox.

cc
2026-05-05 11:39:51 -06:00
vicrodh afb058119f fix(track-row): make checkbox input pointer-transparent + bump selected highlight
Two related issues uncovered while testing the playlist duplicate-track
fix in multi-select mode:

1) Left-click on the checkbox did nothing while right-click marked it.
   The input had onclick={(e) => e.preventDefault()} to suppress the
   browser's native toggle, relying on the parent div's onclick to
   dispatch onToggleSelect. Under webkit2gtk this combination
   intermittently absorbed the left-click without bubbling cleanly.
   Replace with style="pointer-events: none" on the input — clicks pass
   straight through to the parent div, which owns the toggle. The input
   is now purely visual, the div is the only event target. No more
   bubble-vs-default-action conflict.

2) Selected row background was too subtle to read against the heavy
   accent-coloured checkbox. Bump TrackRow's .track-row.selected from
   12% to 22% (hovered variant 20% to 30%) and align
   PlaylistDetailView's .track-row-wrapper.multi-selected wrapper
   rule to 22% so the two sources of truth stay in sync.

Affects every view that uses TrackRow with selectable=true — Album,
Artist, FavQ, Playlist, etc — all benefit from the consistent
pointer-events pattern and more visible selected state.

cc
2026-05-05 11:38:47 -06:00
vicrodh 6c8bfa4473 fix(playlist): align multi-select with TrackRow pattern (checkbox sync)
The explicit checkbox+label markup with onclick+preventDefault on the
input desynced the input's internal checked state from the rendered
checked attribute on click. Result: row background highlighted but
checkbox stayed visually unchecked.

Replace with the standard TrackRow.selectable/selected/onToggleSelect
pattern used by AlbumDetailView, FavQView, ArtistDetailView. Side
benefits:
- Shift/ctrl+click on row body now works for range/toggle multi-select,
  matching UX in those other views
- Single source of truth for the checkbox visual (TrackRow internal),
  no two-checkboxes risk
- Custom-order-mode checkbox preserved as-is - it uses onchange not
  onclick and was never affected

cc
2026-05-05 11:13:01 -06:00
vicrodh f5705cc8c3 fix(playlist): include index in track-selection key for duplicates (#386)
Bulk-edit selection used `${trackId}:${isLocal}` as the Set key. Two
duplicate tracks (same trackId+isLocal appearing twice in a playlist)
collapsed into a single entry, causing:

- clicking duplicate A selected duplicate B and vice versa
- background highlight and checkbox indicator desynced
- bulk delete removed both duplicates instead of the selected one

Fix: include the row index in getTrackKey so duplicates get distinct
selection keys. All 18 call sites updated to pass the index from the
local iteration context. The {#each} key was already index-aware
(visibleTrackRange.start + loopIdx + track.id) so Svelte's element
tracking is unaffected.

For Qobuz tracks the existing remove path uses playlistTrackId
(per-entry unique) so the correct entry is removed. Local-track
duplicates depend on backend semantics of v2_playlist_remove_local_track
keyed by localTrackId — that's a separate consideration outside this
UI fix.

Closes #386.

cc
2026-05-05 10:25:34 -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 a370bfb34d chore(deps-dev): bump @sveltejs/kit from 2.57.1 to 2.58.0
cc
2026-05-05 08:07:28 -06:00
vicrodh 3329d56c58 chore(deps-dev): bump jsdom from 29.0.2 to 29.1.0
cc
2026-05-05 08:06:40 -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
Victor RH 2bc14e7ab8 Merge pull request #385 from afonsojramos/bugfix/internal/macos-output-device-picker
fix(audio): populate macOS output device picker
2026-05-05 07:12:25 -06:00
vicrodh a5a6f0dcac feat(titlebar): system mode now mounts stripped strip when content is enabled
Plasma and system mode become structurally symmetric — both render OS
chrome on top (KWin SSD via Xwayland for plasma, GTK CSD / WM SSD for
system) plus an optional stripped QBZ strip below for search and nav.

Same content-driven rule applies to both: the strip mounts only if the
user has search-in-titlebar enabled OR any titlebar-nav item enabled.
With both off, the user sees just the bare OS chrome, matching the
previous system-mode behavior.

Effective rules table:
- showWindowControls / matchSystemChrome: user pref in qbz only
- searchBarLocation / titlebarNav: user pref in qbz/plasma/system,
  forced/n-a only in hidden

i18n disabledHint strings updated to reflect the new rules across all
five locales.

cc
2026-05-05 01:50:15 -06:00
vicrodh dab72cd588 fix(titlebar): hide stripped strip in plasma when empty
If the user has search-in-titlebar off AND no titlebar-nav items
enabled, plasma mode would render an empty 42px strip below the KWin
SSD. Gate shouldShowTitleBar() in plasma on whether either is enabled.
+page.svelte's search-location and titlebar-nav subscriptions now
also recompute showTitleBar so the layout reflows immediately when
the user toggles those settings.

cc
2026-05-04 16:25:26 -06:00
vicrodh f23cee9ecf fix(titlebar): bump plasma stripped variant to 42px
The 32px guess from the spec left the search input touching the bottom
edge — no breathing room. 42px gives proper vertical centering matching
the qbz variant's visual rhythm. Tested visually on KDE+Wayland.

cc
2026-05-04 15:13:51 -06:00
vicrodh e4835b6c55 fix(i18n): move titlebar block from settings.appearance to settings root
The Task 9 SettingsView consumer reads settings.titlebar.* directly
(see lines 1030, 1037, 4666, 4678 of SettingsView.svelte). The original
Task 11 commit placed the block under settings.appearance.titlebar
which would not resolve. Moving the block one level up.

cc
2026-05-03 20:30:25 -06:00
vicrodh 4e854072fa i18n(titlebar): add mode labels, disabled hints, sandbox caveat
Five locale files updated in lockstep: en, es, de, fr, pt. Mexican
Spanish dialect for es.json per project convention.

cc
2026-05-03 20:26:02 -06:00
vicrodh 0546dbf5cb feat(layout): mount TitleBar by mode + Sidebar consumes effective getters
The +page.svelte mount gate now reads shouldShowTitleBar() so layout
reacts to mode changes. Sidebar reads the effective search-location
and titlebar-nav getters so search and nav fall back to the sidebar
in system/hidden modes regardless of user pref. miniplayerService
reads getMode() instead of the removed legacy boolean.

cc
2026-05-03 20:14:23 -06:00
vicrodh d69c67ad34 fix(settings): include current mode in fallback availableModes
When v2_available_titlebar_modes fails, the dropdown's selected value
may not appear in its options. Include the user's current titlebarMode
in the fallback list to ensure the selected value is always present.

cc
2026-05-03 20:09:49 -06:00
vicrodh b7c686e99c feat(settings): mode dropdown replaces hide/system toggles
Single Dropdown drives the titlebar mode. Orthogonal toggles
(window controls, match-chrome, search location, titlebar nav) are
shown disabled with explanatory tooltips when the active mode does
not apply them. Sandbox caveat is shown inline when Plasma mode is
selected inside Flatpak/Snap.

cc
2026-05-03 20:01:36 -06:00
vicrodh b8ef96d600 fix(titlebar-store): align getTitleBarHeight with rendered CSS
The CSS at .titlebar / .titlebar.variant-full was already 44px before
this branch, and +page.svelte / Sidebar.svelte hardcode 44px in their
calc() reservations. The store was returning 40 — a latent lie waiting
for Task 10's layout consumers. Change qbz mode to return 44 so all
sources agree before they get wired together.

cc
2026-05-03 15:42:17 -06:00
vicrodh a811e2b032 feat(titlebar): variant-aware rendering (full vs stripped)
The full variant matches current behavior. The stripped variant (used
in plasma mode) drops drag region, dblclick maximize, window controls,
and rounded chrome — KWin SSD owns those. Search and per-item nav
remain in the strip below the SSD.

cc
2026-05-03 15:35:31 -06:00
vicrodh b47222d8cb feat(stores): effective getters that read titlebar mode
Search bar falls back to sidebar in system/hidden modes regardless of
user pref. Titlebar nav is N/A outside qbz/plasma. Both getters are
non-destructive: the user's persisted preference is preserved and
resumes when the mode allows it again.

cc
2026-05-03 15:30:30 -06:00
vicrodh 8d9e38d3ab fix(titlebar-store): pessimistic setMode + drop unconditional backend sync
Two issues raised in code review on Task 6:
1. setMode now invokes the backend before mutating local state, so a
   backend rejection leaves frontend state untouched and consistent.
2. Drop the unconditional v2_set_titlebar_mode invoke in initTitleBarStore.
   The migration path already covers the legitimate divergence case;
   firing it on every launch was overhead with no functional benefit.

cc
2026-05-03 15:27:46 -06:00
vicrodh 0dd509305b feat(titlebar-store): mode-based store with legacy migration
Replaces hideTitleBar/useSystemTitleBar booleans with a single mode
enum. First-run migration reads localStorage legacy keys, derives the
new mode deterministically, and pushes it to the backend. setMode
restarts the app only when the change crosses the decoration boundary.

cc
2026-05-03 15:21:31 -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 773fa390eb i18n(settings): make qconnect startup-mode title explicit
"Auto-connect on startup" → "Auto-connect Qobuz Connect on startup"
(and equivalents in es/de/fr/pt). The earlier wording was ambiguous —
it could read as auto-connecting QBZ itself, not the QConnect feature.

cc
2026-05-01 16:01:51 -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 74775aaebf Merge branch 'feature/internal/qconnect-startup-persistence' into pre-release 2026-05-01 14:25:59 -06:00
vicrodh aa0f3c97eb i18n(settings): add qconnect startup mode strings (en/es/de/fr/pt)
Six new keys under settings.integrations.qconnect.startupMode covering
the dropdown labels, description, and local-library compatibility note.

cc
2026-05-01 13:34:11 -06:00
vicrodh 56951c6510 feat(settings-ui): qconnect startup mode dropdown + local-library hint
Adds the three-state dropdown to Settings > Integrations next to the
device-name field, plus a permanent compatibility note about local
library being incompatible with QConnect.

cc
2026-05-01 13:30:23 -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 1b01c95898 feat(qconnect-app): add QconnectStartupMode + compute_effective_startup
Pure enum + decision function for QConnect auto-connect-on-startup.
Persistence and CLI parsing stay in the Tauri adapter; this crate
remains UI-agnostic.

cc
2026-05-01 12:12:09 -06:00
vicrodh c6fb25275e poc(about): add Kawarp attribution
Adds @kawarp/core (MIT, by better-lyrics) to the About modal's
"Built with" section, in the same Lucide-anchored slot. New i18n
key about.attributions.kawarpDesc landed in all five locales.

cc
2026-04-29 09:03:36 -06:00
vicrodh 3ab83e6224 poc(immersive): retire standalone KawarpPanel — Static now uses kawarp via ImmersiveBackground
After the central refactor in c106928a, ImmersiveBackground in 'full'
mode renders kawarp directly. Static (and every other panel that
consumes ImmersiveBackground — Coverflow, Lyrics, Queue,
AlbumReactive, TrackInfo, Suggestions, the split modes) now inherits
the kawarp look automatically.

The standalone KawarpPanel was a temporary A/B vehicle for the POC
benchmarks; with the new architecture it's redundant and would only
confuse the panel selector.

Removed:
  - src/lib/components/immersive/panels/KawarpPanel.svelte
  - 'kawarp' from FocusTab and ImmersiveViewKey type unions
  - Menu entry in ImmersiveHeader
  - Routing branch in ImmersivePlayer
  - i18n key in en/es/de/fr/pt
  - 'kawarp' from the bg-skip exclusion list (no longer needed —
    Static no longer has its own canvas, it uses ImmersiveBackground)

cc
2026-04-29 02:58:47 -06:00
vicrodh c106928a53 poc(immersive-bg): replace WebGL2 path with kawarp in 'full' mode
ImmersiveBackground feeds every panel that doesn't render its own
canvas (Static, Coverflow, Lyrics-focus, Queue-focus, AlbumReactive,
TrackInfo split, Suggestions split, the legacy split lyrics/queue),
so refactoring it once swaps the bg engine across all of them in a
single change.

What changed:

  - 'full' mode used to dynamic-import ImmersiveAmbientCanvas (a
    bespoke WebGL2 renderer maintained in-house) and a CSS-blur
    canvas as fallback. Replaced with a kawarp WebGL canvas and
    'lite' mode as the WebGL-failure fallback (was already the
    watchdog's degradation target). Drops ~400 lines of legacy
    canvas blur code from the bundle indirectly — the dynamic
    import path is gone.
  - Kawarp is mounted directly inside the component on a bound
    canvas via $effect once the canvas is in the DOM.
    transitionDuration: 1000 ms means kawarp handles cross-fades
    natively, so when in full mode we skip the manual CSS opacity
    dance the lite/off modes still need.
  - Tuning matches the values landed on KawarpPanel after the A/B
    rounds (warp 1.0, blur 7, speed 1.15, scale 1.0, saturation 1.5,
    tint 0.06). Comments cite the rationale.
  - switchToMode disposes the kawarp instance when degrading away
    from full, so no leaked GL context after a watchdog event.
  - lite + off modes are unchanged, including their CSS-fade logic.

cc
2026-04-29 02:57:11 -06:00
vicrodh a48d1b369d poc(kawarp): saturation 1.7→1.5, tint 0.03→0.06
cc
2026-04-29 02:49:57 -06:00
vicrodh ded6537ee0 poc(kawarp): saturation 1.9→1.7
cc
2026-04-29 02:46:56 -06:00
vicrodh 0b844e53a0 poc(kawarp): bump animationSpeed 1.0→1.15
cc
2026-04-29 02:46:05 -06:00
vicrodh 30aec49b72 poc(kawarp): nudge saturation 2.0→1.9 and blurPasses 8→7
Tiny dial-back per visual feedback — saturation was clipping faintly
on bright covers, and one fewer blur pass leaves a hair more cover
texture in the warp without re-introducing blob artefacts.

cc
2026-04-29 02:45:44 -06:00
vicrodh f92ef50832 poc(kawarp): push saturation/tint further in the same direction per feedback
User feedback after the previous tweak (saturation 1.8, tint 0.08):
the result felt visually close to the demo defaults — i.e. not enough
chromatic punch from the cover yet. Push further in the same direction:

  - saturation 1.8 → 2.0 so muted palette colours register at full
    strength on low-contrast covers without amping detail extraction.
  - tintIntensity 0.08 → 0.03 effectively removes the global dark-blue
    tint pull. The trace amount keeps a faint anchor for pure-black
    regions so they don't float as flat colour, but the cover's own
    palette dominates the result.

cc
2026-04-29 02:44:32 -06:00
vicrodh 7d54bf840b poc(kawarp): boost saturation, drop tint instead of widening scale
Last tweak (scale 1.5 + speed 1.2) made kawarp surface non-dominant
colours by exposing more cover detail per frame, but the side-effect
was that kawarp started looking closer to Static — same "translate of
regions" feel — defeating the differentiation that's supposed to be
kawarp's selling point.

Different lever: keep the abstract domain-warp look (scale 1.0,
speed 1.0 — exactly the demo) but push the colour pipeline:

  - saturation 1.5 → 1.8 amplifies muted palette so secondary colours
    of low-contrast covers register without forcing detail through.
  - tintIntensity 0.15 → 0.08 halves the global dark-blue tint pull
    so the cover's own palette dominates the result rather than the
    tint dragging everything toward navy.

Result is more chromatic variance with the same fluid abstraction —
keeps kawarp clearly distinct from Static while addressing the
"non-dominant colours don't show" feedback.

cc
2026-04-29 02:42:28 -06:00
vicrodh 69023c199c poc(kawarp): wider sample area + slightly faster drift to surface non-dominant colours
User feedback after round 2 of the A/B benchmark in the immersive view:
metrics-side kawarp is fair (CPU/GPU equal to Static within noise,
+20 MB RSS for the WebGL textures), but the existing Static panel
exposed non-dominant colours from the cover better — its
ImmersiveBackground translates the blurred image so different
regions dominate the bg over time.

Two parameter bumps to bring kawarp closer to that behaviour without
abandoning its fluid-warp look:

  - scale 1.0 → 1.5: widens the area the warp samples per frame so
    the secondary colours of the cover (peach band, yellow accents,
    etc.) come through instead of being averaged into the dominant
    tone.
  - animationSpeed 1.0 → 1.2: slightly faster drift so the colour
    movement reads as movement, not slow ambience.

cc
2026-04-29 02:39:10 -06:00
vicrodh ec1f54a6a9 poc(kawarp): match upstream demo defaults + lift content over the canvas
Two fixes after first round of A/B in the immersive view:

  - The kawarp params shipped were a tamer profile (warp 0.65, slower
    speed, deeper blur, more dither) chosen defensively. On low-contrast
    monochromatic covers (e.g. Billie Eilish "HIT ME HARD AND SOFT"
    underwater shot) the result was visibly grainy and almost static
    versus the lively domain warp on kawarp.boidu.dev. Restore the
    upstream demo defaults exactly so the look in QBZ matches what
    users see on the homepage before evaluating performance fairly.

  - The canvas at z-index 0 was painted ABOVE .track-info, hiding the
    NOW PLAYING / title / artist / album lines entirely. .artwork-wrapper
    and .artwork-container have their own position:relative so the
    cover survived, but the metadata wrapper didn't. Anchor both content
    wrappers at position:relative; z-index:1 so every panel surface
    sits above the canvas regardless of its inner positioning.

cc
2026-04-29 02:27:06 -06:00
vicrodh 3ad68a175a poc(immersive): wire kawarp WebGL background into KawarpPanel
Adds @kawarp/core (vanilla TS, zero deps, MIT) as a dev dep and mounts
it on a full-bleed canvas inside KawarpPanel, behind the existing
artwork/title content. The panel signals ImmersivePlayer to skip the
global ImmersiveBackground when active so the comparison vs Static
isn't a double-blur.

Settings tuned for album art ambience:
  - warpIntensity 0.65 (subtle organic motion, not hallucinatory)
  - blurPasses 12 (avoids visible bands on high-contrast covers)
  - animationSpeed 0.6 (slow drift, doesn't fight the music)
  - transitionDuration 1000 ms (matches the gapless lead-time so the
    background is already on the new artwork before the audio swap)
  - tintIntensity 0.12 (gentle dark anchor for low-key covers)

Lifecycle:
  - onMount creates the renderer, loads the current artwork, starts
  - $effect reloads on artwork change (kawarp does its own crossfade)
  - ResizeObserver keeps the WebGL backing store in sync with layout
  - onDestroy disposes the GL context

Compositing layer hint (will-change/translateZ) mirrors the fix used
on the 1.2.10 sticky headers — keeps WebKitGTK from repainting
surrounding chrome on every animation frame.

cc
2026-04-29 02:03:35 -06:00
vicrodh 0373b9c1fc poc(immersive): add KawarpPanel as exact duplicate of StaticPanel
Baseline checkpoint for the kawarp benchmark. KawarpPanel is currently
identical to StaticPanel (same template, same CSS, same animation set
— just an equalizer on the title row when playing). Wired into the
immersive panel selector so it can be picked side-by-side with Static
for A/B measurement.

Subsequent commits will introduce the kawarp WebGL background only
inside this panel; if the GPU/CPU/RAM delta is unjustified the panel
gets removed without touching StaticPanel.

cc
2026-04-29 01:59:29 -06:00