Commit Graph
4231 Commits
Author SHA1 Message Date
vicrodh a67e3c9217 Merge PR #391 (Vudgekek): macOS CoreAudio Exclusive Mode
Adds CoreAudio Hog Mode integration on macOS so Exclusive Mode actually
takes the device exclusively (previously the macOS path used the
Linux-shaped logic without any kernel-level lock). Pins the CoreAudio
device id through Hog Mode acquisition, sample-rate switching, and
stream creation so System Default doesn't drift mid-session. Linux
behaviour is unchanged.

Review asks (all four resolved by Vudgekek + afonsojramos):

- set_coreaudio_hardware_volume falls back to software volume on Err
  rather than reporting success, so knob-only DACs keep a working
  volume slider via engine.set_volume().
- CoreAudioExclusiveGuard snapshots the device's hardware volume on
  acquire and restores it before releasing Hog Mode, so leaving
  Exclusive Mode returns the DAC to the user's pre-Exclusive level.
  Guard is constructed before the FFI call so partial-acquire failures
  still trigger Drop and release.
- Linux unused-variable warnings on stream_opt and exclusive_guard
  cleaned up via cfg_attr(allow(unused_variables)) on the macOS-only
  guard plumbing. cargo check is clean.
- Silent shared-mode regression on macOS when backend init fails:
  init_device now sets_current_device(None) and returns None on
  exclusive_mode==true so the user sees an error instead of
  unknowingly playing in shared mode.

Bonus: 5f10b4ba adds real CoreAudio device-rate detection for macOS
Shared Mode, +90 lines in backend.rs.

Conflict resolution: single textual collision in
crates/qbz-audio/src/lib.rs (both branches added a pub use line).
Resolved by keeping both, alphabetical order — coreaudio_direct
re-export from pr-391, device_reservation re-export from pre-release.
All other auto-merges verified line-by-line: AlsaDirectStream's
DeviceReservation::acquire flow (alsa_direct.rs:60-97), the macOS
exclusive_mode guard (player/mod.rs:1077-1085 in qbz-player and the
mirror in src-tauri:1024), and CoreAudioExclusiveGuard's volume
snapshot/restore (coreaudio_direct.rs:351,369,373,388) all coexist
correctly because each branch touched different functions.

cargo check clean across all four crates. 9 device_reservation +
3 reserve_dac + 214 src-tauri unit tests pass.

cc
2026-05-07 19:09:37 -06:00
vicrodh d214125d1c feat(settings): expose reserve-DAC toggle in Audio settings
Mirrors the HiFi Wizard control (landing in Task 8) inline under
Exclusive Mode. Status row polls v2_get_dac_reservation_status every
5s while the row is rendered. Hidden when the selected device is not
a card-specific ALSA prefix (hw:, plughw:, front:, surround*, iec958:,
hdmi:). Six i18n keys consumed at settings.audio.reserveDac.*.

cc
2026-05-07 15:42:57 -06:00
vicrodh 4a8af7d316 i18n: add reserveDac keys for ALSA exclusive hardening UI
Five locales (en, es, de, fr, pt). English and Spanish are human-written
in the project's canonical Mexican neutral register; de/fr/pt are
machine-translated drafts. Pending-translations note recorded in
qbz-nix-docs/i18n/pending-translations.md (outside git scope).

Keys: settings.audio.reserveDac.{label, description, statusActive, statusContested, statusUnavailable, wirePlumberNote}.

UI surfaces (Settings mirror + HiFi Wizard) consume these in Tasks 7-8.

cc
2026-05-07 15:09:37 -06:00
vicrodh 11b46bbe9f chore(reservation): address Task 5 review feedback
- Document the Contested-vs-Unavailable classification limitation on
  the DacReservationStatus enum and its call site, deferring the
  stash-the-last-error refactor until Tasks 7-8 confirm the UI needs
  the distinction.
- Lift is_card_specific_device to pub(crate) in lib.rs and delete the
  duplicate in commands_v2/audio.rs to prevent the allow-list from
  diverging when new ALSA prefixes are added.
- Drop the misleading "or non-card device" clause from the
  apply_dac_reservation degraded-path log; non-card devices are
  filtered earlier and cannot reach this log line.

No behaviour change.

cc
2026-05-07 14:50:58 -06:00
Vudgekek 5cd76f53d4 Merge branch 'pre-release' into feature/external/macos-exclusive-mode 2026-05-07 15:34:44 -05:00
Vudgekek 5f10b4baa4 fix(audio): check real CoreAudio device rate with macOS Shared Mode 2026-05-07 14:51:51 -05:00
vicrodh 4797fec939 feat(reservation): per-process DAC reservation + Tauri commands
Lifetime B from the design spec: a long-lived DeviceReservation guard
held in AppState, gated by the reserve_dac_while_running setting.
Acquired at startup if the persisted flag is true and the selected
device targets a single ALSA card; reapplied when the user picks a
different DAC; released on toggle-off, on a switch to a non-card
device, or AppState drop.

DB persistence: ALTER TABLE migration adds the column, SELECT and
reset_all are extended, set_reserve_dac_while_running setter parallels
the established pattern for boolean audio settings.

Tauri commands (V2 cleanroom):
- v2_set_reserve_dac_while_running(enabled)  - persist + apply
- v2_get_dac_reservation_status() -> DacReservationStatus

Status enum mirrors the four cases in the spec: Inactive (toggle off,
or non-card-specific device), Active (we hold the bus name), Contested
(another app holds the device at higher priority - guard is None but
the toggle is on), Unavailable (D-Bus unreachable, degraded guard).

v2_set_audio_output_device gains an AppState dependency so DAC changes
trigger a reservation handover when the toggle is on.

UI surfaces (Settings mirror + HiFi Wizard) land in Tasks 7-8.

cc
2026-05-07 13:48:51 -06:00
vicrodh a1a056c4b9 feat(audio-settings): add reserve_dac_while_running flag
Opt-in flag for the per-process DAC reservation lifetime (Lifetime B
in the spec). Defaults to false; serde default ensures legacy settings
JSON files round-trip cleanly without the field. Task 5 will read this
flag in AppState startup and the v2_set_reserve_dac_while_running
Tauri command.

Strictly additive: no existing field changed, no behaviour change for
users with the flag at its default (false).

cc
2026-05-07 12:39:59 -06:00
vicrodh d2185f7a6b fix(qbz-audio): resolve ALSA card by id, not by long name
The CARD= parameter in ALSA device strings refers to the card's short
id (e.g. "C20", "HDMI", "Generic"), not its long descriptive name
("Cambridge Audio USB Audio 2.0"). The previous resolver called
Card::get_name() and silently swallowed errors, so every CARD=<id>
lookup failed and acquire() degraded gracefully -- meaning the
reservation has been non-functional for users with named-card device
strings since Task 3 landed.

Reported by the maintainer on Cambridge DacMagic Plus exposed as
front:CARD=C20,DEV=0, 2026-05-07. Smoke-test scenario 2 had been a
false positive: audio worked because we degraded silently, not because
the reservation succeeded.

In alsa-rs 0.10, Card::get_name() wraps snd_card_get_name and despite
the name returns the LONG descriptive string. The short id (what
aplay -l shows in brackets and what CARD= references) is exposed via
ctl::CardInfo::get_id() (snd_ctl_card_info_get_id). Open a Ctl per
card, read the id from its CardInfo, and surface ALSA enumeration
errors as ReservationError::AlsaError instead of unwrap_or_default()
into an empty string.

cc
2026-05-07 12:07:03 -06:00
vicrodh a4eb441c4e fix(qbz-audio): accept all ALSA plugin prefixes; degrade on parse failure
The previous parse_card_index only accepted hw: and plughw: prefixes,
which broke playback for users whose ALSA device string uses front:,
plughw:, surround*:, iec958:, hdmi:, etc. The plugin prefix is
irrelevant for D-Bus reservation purposes - what matters is the card
identifier in the args. Generalise the parser to extract the card
from any plugin prefix.

Defence in depth: when parse_card_index fails (unrecognised string,
no card identifier extractable, etc.), acquire() now logs a warning
and returns a degraded guard instead of propagating the error.
This preserves playback for non-card-targeting names like 'default'
and 'pulse' that are not meaningful for per-card reservation.

Reported on Cambridge DacMagic Plus exposed as front:CARD=C20,DEV=0,
2026-05-07.

cc
2026-05-07 11:10:12 -06:00
vicrodh 6b7ed75cc9 chore(qbz-audio): polish AlsaDirectStream reservation wiring
- Skip the 50 ms PipeWire-vacate margin on the degraded D-Bus path;
  no reservation transition happened, so no settle delay is needed.
- Add TODO(Task 5) breadcrumb for the app_device_name placeholder
  so Task 5's UI surfacing wires it to the user-facing DAC name.
- Soften the timing-claim comment on the sleep; the specific "~2-4 ms"
  was spec-cited, not measured in this commit.
- Lift the 50 ms literal into a named PIPEWIRE_VACATE_MARGIN const.

No behaviour change for the active-reservation path.

cc
2026-05-07 10:19:59 -06:00
vicrodh 92633dcf12 feat(qbz-audio): hold device reservation for AlsaDirectStream lifetime
Acquire org.freedesktop.ReserveDevice1.Audio<N> before opening the PCM,
hold for the entire stream lifetime, release on Drop after the PCM
closes. Closes the path where exclusive open would silently fight an
active PipeWire route.

Field order in AlsaDirectStream is significant: _reservation is the
last field so it drops AFTER the PCM closes, ensuring the kernel-level
exclusive lock is released before the D-Bus bus name is freed and
PipeWire reclaims the device.

This is the canonical Lifetime-A consumer the device_reservation
acquire doc-comment's tight-coupling rule allows: a DeviceReservation
created immediately before a real PCM::new() and held for as long as
the PCM is open. A 50 ms defensive sleep gives PipeWire time to vacate
the device after releasing the reservation.

cc
2026-05-07 10:09:07 -06:00
vicrodh 41b844cfa8 chore(qbz-audio): document tight-coupling rule and refactor contention proxy
- Document the load-bearing tight-coupling rule on acquire(): never
  construct a DeviceReservation without an immediate device consumer.
  Cambridge DacMagic Plus was stranded by a standalone reservation
  churn during initial Task 2 smoke testing on 2026-05-07.
- Document the Connection lifecycle: zbus does not pool session-bus
  connections, so acquire() pays a SASL handshake per call. Task 5
  will add acquire_with_connection() to reuse a long-lived bus.
- Refactor resolve_contention to open a single holder Proxy and read
  Priority + ApplicationName through it, instead of opening two.
- Specificity polish on the QBZ_APPLICATION_NAME dead-code comment;
  defensive-InQueue comment; release_name From-chain comment;
  clippy doc-list-overindentation fixes.

No behaviour change.

cc
2026-05-07 10:00:57 -06:00
vicrodh bc5d316e5a feat(qbz-audio): wire zbus client for ReserveDevice1 acquisition
Implements the full priority-resolution algorithm: request bus name
with DO_NOT_QUEUE; on EXISTS look up holder Priority via the
Properties interface; if our priority (5) is higher, call
RequestRelease and retry with REPLACE_EXISTING; if equal-or-lower,
return HigherPriorityHolder. AlreadyOwner is treated as success for
same-connection idempotency. Drop releases the bus name and logs
the ReleaseName reply variant.

D-Bus failure paths degrade to a no-op guard so playback continues
exactly as before in environments without a session bus.

Smoke-tested against PipeWire/WirePlumber via dbus-monitor.

cc
2026-05-07 09:43:37 -06:00
vicrodh f0ea0c88ec chore(qbz-audio): address Task 1 review feedback
- Add ReservationError::AlsaError variant; stop conflating ALSA
  enumeration errors with DbusError.
- Mirror ReservationError variant set on the non-Linux stub so cross-
  platform pattern matching compiles on every target.
- Add empty Drop to stub for RAII symmetry with the Linux variant.
- Collapse ReservationState to its real Task 1 shape (Degraded only);
  Task 2 reintroduces Active when it has real fields.
- Rewrite acquire() doc-comment to honestly describe Task 1's behaviour
  and hint at Task 2's full failure-mode set.

No behaviour change. Foundation commit for Task 2.

cc
2026-05-07 09:18:19 -06:00
vicrodh 4bc4213b97 feat(qbz-audio): add device-reservation module skeleton with parsers
Adds the new module that will host the org.freedesktop.ReserveDevice1
D-Bus client. This commit only ships the public surface, the parser
helpers (hw:N -> Audio<N> bus name, hw:CARD=name resolution) and unit
tests for them. acquire() currently returns a degraded guard; D-Bus
implementation lands in the next commit.

cc
2026-05-07 08:57:31 -06:00
Afonso Jorge Ramos 88e072a4ac fix(macos): harden CoreAudio exclusive stream handling
Address review feedback from the macOS Exclusive Mode PR.

Pin the resolved CoreAudio output device before acquiring Hog Mode so
System Default cannot drift between Hog Mode acquisition, sample-rate
switching, and stream creation.

Fall back to software volume when CoreAudio hardware volume is unavailable,
which keeps the volume slider usable on knob-only DACs.

Snapshot and restore CoreAudio hardware volume around Exclusive Mode so
leaving Exclusive Mode does not leave the output device at QBZ's last
hardware volume.

Refuse shared-mode fallback when macOS Exclusive Mode stream creation
fails, so the app does not silently play in non-exclusive mode while the UI
still reports Exclusive Mode.

Clean up Linux unused-variable warnings from the macOS-only guard plumbing.
2026-05-07 09:36:20 -05:00
vicrodh a705e4f297 Merge feature/internal/discord-rpc-v2 into pre-release
Discord Rich Presence (#336): when the user opts in, QBZ pushes the current
track as Listening status in Discord with cover, progress bar, and Get QBZ
button. Off by default, silent degrade when Discord is unavailable, sandbox
override block in Settings -> Integrations.

cc
2026-05-07 02:01:22 -06:00
vicrodh 3a0c2ab6ce build(flatpak): expose discord-ipc-0 socket for RPC
Adds --filesystem=xdg-run/discord-ipc-0 to the self-hosted Flatpak manifest
so Discord Rich Presence can reach the IPC socket from inside the sandbox.
The Flathub manifest is updated separately at release time.

Refs #336.

cc
2026-05-07 01:47:54 -06:00
vicrodh f2b0ce7071 feat(discord-rpc): settings UI with toggle and sandbox override block
Adds a Discord Rich Presence section to Settings -> Integrations:
- Toggle wired to discordRpcService (off by default, persisted in localStorage)
- Collapsible override block under the toggle with the flatpak override
  command and a Snap fallback note. Always shown - runtime detection isn't
  worth the complexity for a help block.

Refs #336.

cc
2026-05-07 01:42:42 -06:00
vicrodh 6b08cf6848 i18n(discord-rpc): add translation keys for 5 locales
Adds settings.integrations.discordRpc keys (label, description, overrideHeader,
flatpakNote, snapNote) to en/es/de/fr/pt for the Discord Rich Presence settings
toggle and sandbox override block.

Refs #336.

cc
2026-05-07 01:38:04 -06:00
vicrodh 232276f4fa feat(discord-rpc): wire initDiscordRpc into app boot
Calls discordRpcService.initDiscordRpc() once from +page.svelte onMount so
the backend state matches the persisted toggle and the playerStore subscriber
is registered when the user has opted in.

Refs #336.

cc
2026-05-07 01:35:19 -06:00
vicrodh 7531c2f092 feat(discord-rpc): frontend service with localStorage toggle and dedupe
Creates discordRpcService that:
- persists the user toggle in localStorage (qbz-discord-rpc-enabled, off by default)
- on enable, subscribes to playerStore and forwards (track_id, is_playing) transitions
- dedupes by (track_id, is_playing) so currentTime ticks don't spam the backend
- silently ignores backend errors (headless, sandbox-blocked, etc.)

Refs #336.

cc
2026-05-07 01:32:07 -06:00
vicrodh 82453b8fee feat(discord-rpc): backend module and Tauri commands
Adds src-tauri/src/discord_rpc.rs with DiscordRpcState (Mutex<Option<client>>
+ AtomicBool enabled) and three V2 commands:
- v2_discord_rpc_set_enabled
- v2_discord_rpc_update
- v2_discord_rpc_clear

Lazy connect on first update; silent degrade when Discord is unavailable.
Hardcoded Application ID 1501835855587708988 (public identifier).

Refs #336.

cc
2026-05-07 01:29:03 -06:00
vicrodh 2b5366eb06 chore(deps): add discord-rich-presence 0.2.4
For Discord Rich Presence integration (#336).

cc
2026-05-07 01:20:27 -06:00
vicrodh 9bab69310b chore(deps): batch bump minor/patch versions
Manual batch instead of merging the 10 dependabot PRs individually so
Cargo.lock and package-lock.json each see a single coherent update.

Rust (src-tauri/Cargo.lock):
- tokio 1.52.1 -> 1.52.2 (#401)
- rustls 0.23.37 -> 0.23.40 (#400)
- tauri-plugin-opener 2.5.3 -> 2.5.4 (#399)
- tauri-plugin-deep-link 2.4.8 -> 2.4.9 (#397)
- notify-rust 4.16.1 -> 4.17.0 (#394)

npm (package.json + package-lock.json):
- @sveltejs/kit 2.58.0 -> 2.59.1 (#398)
- @tauri-apps/plugin-dialog 2.7.0 -> 2.7.1 (#396)
- jsdom 29.1.0 -> 29.1.1 (#395)
- @tauri-apps/api 2.10.1 -> 2.11.0 (#393)
- svelte-check 4.4.6 -> 4.4.8 (#392)

cargo check + npm run check both clean.

cc
2026-05-07 00:19:45 -06:00
vicrodh a9c17365e6 Merge feature/internal/artist-header-gradient into pre-release
Apply the album/artist detail backdrop overhaul:
- Blurred artwork background instead of a flat palette gradient
- Gradient extended to artist detail, sharing the existing toggle
- Image cache plumbed through extractPalette so artist images bypass
  Qobuz CDN's missing CORS headers and Vibrant can read pixels
- Per-view height tuning so each fade lands above its own toolbar
- Artist jump-nav transparent at rest, gains its background only when
  pinned (IntersectionObserver sentinel)
- Reactive text + action button overrides extended to artist view

cc
2026-05-07 00:13:55 -06:00
vicrodh 55a200a8da feat(detail-views): blurred artwork backdrop and artist-page gradient
Promote the album/artist detail header gradient from a flat
palette-color fade to a Qobuz/Feishin-style blurred backdrop, and
extend it to the artist page.

- artworkPalette: route the URL through getCachedImageUrl before
  handing it to Vibrant. Qobuz CDN sends Access-Control-Allow-Origin
  for album covers but not artist images, which tainted the canvas
  Vibrant uses and made pixel reads fail. The cache resolves to a
  local asset:// path and primes the disk store for next time.
- AlbumDetailView and ArtistDetailView: ::before now renders the
  artwork itself with filter:blur + saturate, masked to fade into the
  theme background; ::after carries the contrast scrim so the blur
  layer doesn't muddle it. The CSS variable --art-image-url carries
  the locally-resolved URL so the backdrop image is served from disk
  instead of being re-downloaded.
- Heights tuned per view: artist's header is taller (image + bio +
  actions) than album's, so artist gets 340 × scale(1.1) and album
  gets 280 × scale(1.15) so each fade lands above its respective
  toolbar / JUMP TO bar.
- ArtistDetailView jump-nav: now transparent at rest and gains its
  background only when stuck (IntersectionObserver against a 1px
  sentinel placed right above it). Stops the bar from cutting a
  flat-color stripe across the gradient when it's not pinned.
- Reactive text + action button overrides on .has-art-bg apply to
  the artist page too: back button, bio text, bio toggle, and the
  action-btn-circle family read in white-tinted variants over the
  colored backdrop.

cc
2026-05-07 00:12:58 -06:00
vicrodh eed2f4f636 refactor(playlist-detail): unify selection on multiSelectMode and remove visual flash
Custom sort had two parallel selection systems: an inherent per-row
checkbox column (selectedTrackKeys) used solely to enable Move Up/Down
in a separate batch toolbar, and the toolbar's multiSelectMode for
bulk actions. They had different stores, different widths, and one
disabled the other. Consolidate onto multiSelectMode; drag and the
existing per-row reorder controls cover ordering, and the wrapper
propagates dragTrackIds for group moves.

- Drop selectedTrackKeys, isSelectionMode, toggleTrackSelection,
  clearSelection, selectAllTracks, moveSelectedUp, moveSelectedDown
  and the associated batch-controls toolbar / col-checkbox header
  branch / inherent per-row checkbox.
- Move the multi-select checkbox out of TrackRow into the wrapper so
  the column order in custom sort reads [checkbox][reorder][TrackRow];
  TrackRow's internal track-checkbox is hidden via :global so its
  click + keyboard handlers stay wired through selectable=true.
- Reserve 24px for the row-checkbox and the header col-select-all at
  all times and animate opacity (180ms) instead of width. Toggling
  select no longer reflows the columns to its right; the checkbox
  fades inside a slot that was always there.
- Stable each-key for virtualized rows: use track identity
  (`<namespace>-<id>-<number>`) instead of `${visibleTrackRange.start
  + loopIdx}-${track.id}`. The old key mixed scroll state into the
  identity, so a re-render that bumped trackListScrollTop reused row
  instances against new data — rows showed mismatched track / # /
  album combinations until the next paint.

cc
2026-05-06 23:26:01 -06:00
Vudgekek 96fd15cb33 fix(settings): enable macOS exclusive mode 2026-05-06 23:26:16 -05:00
Vudgekek 59b1ceec2e fix(macos): align exclusive device and hardware volume 2026-05-06 23:26:15 -05:00
Vudgekek d4a51a5c63 fix(player): recreate CoreAudio exclusive streams 2026-05-06 23:26:15 -05:00
Vudgekek e0151af6d9 feat(audio): add CoreAudio exclusive mode 2026-05-06 23:26:15 -05:00
vicrodh 8977e721c6 refactor(album-detail): align tracklist columns and smooth select-mode toggle
- Reduce table-header left padding to 4px and switch the # column to
  text-align: left so track numbers line up with the bare quality badge
  in the toolbar above. Track rows in the album-detail context get the
  same 4px left padding via a :global override on .track-list and the
  track-number column switches to flex-start so the row stays aligned.
- Match the select-all checkbox width to the track-row checkbox (24px)
  and shrink the checkbox itself to 15x15 so the header checkbox lands
  on the same x as the per-row ones.
- Always render the select-all and track-row checkbox containers and
  animate width + opacity in/out (180ms ease) so toggling select mode
  no longer pops the column into existence.
- Move the AlbumMenu (...) trigger to the end of the action row so the
  bulk-action triggers (CassetteTape, multi-select) sit before the
  overflow menu — same convention as other detail views.

cc
2026-05-06 22:21:51 -06:00
Victor RH ecb8f96fa1 Merge pull request #390 from afonsojramos/bugfix/internal/track-change-click
fix(audio): eliminate macOS track-change crackle
2026-05-06 22:09:41 -06:00
Victor RH 1fdf0bfcb2 Merge pull request #389 from vicrodh/dependabot/cargo/src-tauri/openssl-0.10.79
chore(deps): bump openssl from 0.10.78 to 0.10.79 in /src-tauri
2026-05-06 22:09:31 -06:00
vicrodh f4c9b71366 Merge feature/internal/album-header-gradient into pre-release
Album detail UX overhaul:
- Artwork-derived gradient backdrop on the album header (opt-out toggle in
  Settings > Appearance > Album header gradient).
- Bare mode for QualityBadgeStatic so quality reads inline without a
  pill container.
- Shrink label/awards sidebar by ~20%.
- Inline tracks count and duration with the metadata row, format duration
  as Xh Ym Zs.
- Plumb album description with sanitized HTML and click-to-expand.
- Tracklist toolbar with track search and inline quality badge.
- Featured artists and explicit (parental warning) badge in the artist
  line.
- Reactive divider between header and tracklist (visible only when the
  gradient is off), reactive action button colors, metadata centering
  when there is no description.

cc
2026-05-06 22:08:33 -06:00
vicrodh 5b05b6fae3 feat(album-detail): tracklist toolbar, featured artist line, explicit badge, header polish
Inline UX iteration for the album detail header and tracklist:

- Tracklist toolbar above the table: bare quality badge on the left,
  borderless search input on the right (filters tracks by title or
  artist), with min-height alignment so badge and input share a baseline.
- Artist line renders the explicit badge (when parental warning) before
  the main artist and appends featured artists separated by a dot, each
  clickable to its discography. Featured names use a quieter weight and
  size to keep the main artist primary.
- Reactive divider between header and tracklist: visible only when the
  artwork gradient is off; when on, the gradient itself provides the
  demarcation.
- Symmetric vertical breathing room around the toolbar (20px top to
  optically compensate for the 40px-tall table-header below) and the
  same offset on the album sidebar so the LABEL section aligns with the
  toolbar row.
- Action buttons in the header (play/shuffle/heart/etc.) react to the
  gradient backdrop with white-tinted strokes and a brighter hover.
- Metadata column vertically centers against the artwork when the
  album has no description.

i18n keys added in all five locales: tracklist.searchPlaceholder,
tracklist.noMatches.

cc
2026-05-06 22:08:06 -06:00
vicrodh e8d58196c5 feat(album): plumb featured artists and parental warning
Add Album.parental_warning and Album.artists (with the new AlbumArtist
struct) to qbz-models so the /album/get response surfaces both fields.
Mirror them in the QobuzAlbum / AlbumDetail TypeScript types and
populate featuredArtists (filtered to non-main contributors) and
parentalWarning in convertQobuzAlbum. Wire onFeaturedArtistClick from
the route so featured artist names route to their discography.

cc
2026-05-06 22:06:54 -06:00
vicrodh e1447159bb feat(album-detail): plumb album description with click-to-expand toggle
Pass Qobuz album.description through adapter and AlbumDetail prop, then
render below the metadata row with a 2-line clamp and a Read more / Show
less toggle. Description HTML (Qobuz CMS markup with paragraph and
emphasis tags) is sanitized through the existing allowlist before being
inserted as HTML so the formatting renders cleanly without exposing
data-* attributes.

cc
2026-05-06 21:01:59 -06:00
vicrodh 7e87de4352 refactor(album-detail): inline tracks/duration in metadata row
Folds the standalone .album-quality and .album-stats lines into the
existing date/label/genre row, since the quality string moves to the
tracklist toolbar in a follow-up. Plumbs durationSeconds through
AlbumDetail and the Qobuz adapter, plus bitDepth/samplingRate so the
inline QualityBadgeStatic can render the same tier label as Mixtape and
Collection rows. Total duration renders as Xh Ym Zs (no leading zeros).

cc
2026-05-06 20:52:15 -06:00
vicrodh 16e134fba1 refactor(album-detail): shrink label/awards sidebar by ~20%
The right-side sidebar was carrying too much weight versus the tracklist.
Drops the column from 280px to 224px and proportionally reduces avatar,
icon, and font sizes so the label/awards stack reads as supporting
metadata rather than competing with the track table.

cc
2026-05-06 20:49:00 -06:00
vicrodh a2866f8d96 feat(quality-badge): add bare prop for inline (no-container) usage
When bare={true}, the badge drops its background, border, padding,
min-width, and fixed height so it can render inline above a tracklist
toolbar without visually competing with the table header.

cc
2026-05-06 20:46:20 -06:00
vicrodh 0d784545eb feat(album-detail): album header gradient backdrop from artwork palette
Extracts the dominant color from album artwork (via node-vibrant) and
paints it as a soft top-down gradient behind the album header, with an
appearance toggle to opt out. Adds the .has-art-bg state class plus
contrast-lifted overrides for back button, info text, and artist link.

cc
2026-05-06 20:40:18 -06:00
vicrodh 19f33cb157 Merge feature/internal/local-library-followups into pre-release
Local Library follow-ups:
- fix tracks tab empty on initial load (active tab loader trigger)
- relocate edit-tabs button into folder management toolbar with sort icon
- give Albums tab the full Folders action bar (group/filter/sort/view/select)
- include Plex albums in metadata-grouped Albums view
- show Albums in Edit Library Tabs modal
- declare standard line-clamp alongside -webkit-line-clamp

cc
2026-05-06 18:56:36 -06:00
vicrodh 4bbff02c4b style: declare standard line-clamp alongside -webkit-line-clamp
svelte-check warned about missing standard property next to the
-webkit-line-clamp prefix in 9 spots across 8 components. Add the
standard line-clamp declaration with the same value to clear the
warnings without changing rendering.

cc
2026-05-06 18:56:18 -06:00
vicrodh ebe98ab4b8 fix(library): show Albums in Edit Library Tabs modal
The modal sanitizer filtered tab_order against availableTabs, which
omitted 'albums', so the metadata-grouped Albums tab never appeared
in the order/visibility editor. Add it to both availableTabs and
DEFAULT_TAB_ORDER to match the backend default and LocalLibraryView.

cc
2026-05-06 18:56:15 -06:00
vicrodh a1197983cf fix(library): include Plex albums in metadata-grouped Albums view
loadMetadataAlbums only queried the local SQLite library, so the new
Albums tab was missing all Plex-cached content while the Folders tab
showed it. Mirror the merge pattern from loadLibraryData: fetch Plex
cached albums in parallel and concatenate after mapping.

cc
2026-05-06 18:56:09 -06:00
Afonso Jorge Ramos e93fcaec99 fix(audio): defer host-stream drop on Stop to reuse device across track changes 2026-05-07 02:32:39 +02:00
vicrodh ea98a34f8e feat(library): give Albums tab the full Folders action bar (group/filter/sort/view/select)
Albums was a stripped-down VirtualizedAlbumList over metadataAlbums with
no controls. Folders had the full toolbar (group dropdown, quality/format
filter, sort, view toggle, multi-select with bulk actions, alpha index).
The user wants Albums to be 1:1 with Folders, no reinvention.

Implementation:
- Extracted the album-controls block into a Svelte 5 snippet
  `albumControls(alphaGroups)` declared at the top of the library-view
  template. The snippet body references the existing shared state
  (group/filter/sort/view/select); only `alphaGroups` differs per source.
- Extracted the body of `filteredAndGroupedAlbums` into a helper
  `buildFilteredAndGroupedAlbums(source)` and added a parallel
  `filteredAndGroupedMetadataAlbums` derived for the Albums tab.
- Generalized album-selection state: `currentAlbumsSource` switches
  between `albums` and `metadataAlbums` based on activeTab; Select All
  and the keyboard shortcut now operate on the active list. Bulk action
  handlers look up album IDs across both lists (the two ID spaces don't
  overlap) so playlists/queues/mixtapes work from either tab.
- Moved the album BulkActionBar out of the Folders branch so it renders
  once for either tab.
- handleTabChange clears in-flight album selection when crossing tabs.
- metadataAlbums state declarations moved up to live next to `albums`
  because the new selection derivations reference them earlier in scope.

cc
2026-05-06 18:10:04 -06:00