Commit Graph
147 Commits
Author SHA1 Message Date
vicrodh a05e8abc25 feat(qconnect): add 12s renderer-liveness watchdog
A playing active-peer renderer that goes silent left the controller UI
stuck on stale 'playing' (no equivalent of the webplayer's 12s
rendererLost timer). Add a generation-guarded 12s watchdog keyed on the
active peer renderer in the RENDERER_STATE_UPDATED handler: reset on every
update, arm only while playing_state==PLAYING, disarm on
pause/stop/active-change/disconnect. On expiry freeze the projection to
UNKNOWN and emit qconnect:renderer_unreachable; Svelte shows an error
toast (rendererUnreachable, all 5 locales). Reuses the
freeze-projection-to-unknown helper from the ACTIVE_DISCONNECTED path.

cc
2026-06-01 03:03:16 -06:00
vicrodh d625e0afef feat(qconnect): handle ACTIVE_DISCONNECTED renderer status
The wire 'status' field on RENDERER_STATE_UPDATED was decoded into the
adapter payload (decoder.rs:801) then dropped — a graceful peer
disconnect was invisible and the controller kept showing stale 'playing'.
Add a RendererStatus enum (UNKNOWN/UNRECOGNIZED -> INACTIVE collapse) and,
on ACTIVE_DISCONNECTED for the active remote renderer (id != -1), freeze
the projection to UNKNOWN and emit qconnect:renderer_disconnected. Svelte
surfaces it as an error toast (rendererDisconnected, all 5 locales). Pure
consumer-side: no protocol change.

cc
2026-06-01 02:54:31 -06:00
vicrodh b5990a24e6 qconnect: on-connect queue push runs mixed-queue guard
The TransportConnected handler pushed the live queue via loadQconnectQueue
with no source check, shipping local/Plex row ids as Qobuz ids. The frontend
display queue (getQueueState) carries no `source`/`is_local`, so wire the push
to getBackendQueueState() whose tracks DO carry them (v2_get_queue_state maps
source per track in commands_v2/queue.rs). Route the ordered snapshot
([history, current, upcoming]) through assessQconnectQueueSync: refuse the
whole push on a mixed queue (no silent subset) and toast the user via the new
qconnect.mixedQueueNotCast key (all five locales). Lock the refuse-not-subset
contract with a unit test; page wiring covered by svelte-check + manual verify.

cc
2026-06-01 02:07:30 -06:00
vicrodh 47db92ff92 chore: remove qbzd headless daemon and its client integration
qbzd was a premature daemon experiment that never shipped (gated off
behind QBZD_ENABLED, no user access). Its ratatui 0.29 dependency pinned
unicode-width =0.2.0, which conflicts with slint's annotate-snippets in a
shared workspace. Remove it entirely; it will be reimplemented from
scratch later on the clean, UI-agnostic shell.

- Delete crates/qbzd daemon crate + workspace member (drops ratatui,
  crossterm and the other qbzd-only deps from the lock).
- Delete src-tauri qbzd_discovery (mDNS) + its commands/state wiring in
  lib.rs; drop the direct mdns-sd dep (Chromecast keeps its own via
  qbz-cast).
- Delete remoteApi/remoteEvents/playbackTargetStore; neutralize
  commandRouter to local-only dispatch (isRemote/skipIfRemote are now
  always-false no-ops so the ~100 guard call-sites keep compiling).
- Strip qbzd branches from playbackService and the remote banner from
  NowPlayingBar; excise qbzd from CastPicker (Chromecast/DLNA kept).
- Remove qbzd i18n keys (5 locales), README daemon node, gitignore note.

cargo check (crates + src-tauri) and svelte-check all green.

cc
2026-05-25 15:08:14 -06:00
vicrodh a0051ab513 fix(audio): default to shared System output and resolve Auto to a concrete backend (#470)
OOTB playback froze (seekbar advancing, no audio) on hosts where the
configured backend was not usable — e.g. PipeWire as the Linux default but
`pactl` absent (minimal Debian/Ubuntu). "Auto" (a null backend_type) fell
through to a legacy CPAL path that forced the track rate onto the shared
default device, producing a suspended/silent stream and leaving a
process-wide stuck audio handle that survived Reset.

- Add a cross-platform "System" backend (CpalDefaultBackend now on Linux
  too): opens the OS default output at its negotiated rate with rodio
  resampling and an explicit ~100ms CPAL buffer (MixerDeviceSink has zero
  internal buffering, so the bare default underran and stayed suspended).
  Shared, no exclusivity, no `pactl` — mixes with other apps like any
  normal player.
- Make System the OOTB default and what Reset lands on; seed it on first
  run. Drop the per-startup #375 NULL->PipeWire backfill migration.
- Resolve a null backend_type to System in the player so the legacy CPAL
  path is never reached (covers legacy installs and headless callers).
- "Auto" in Settings is now a resolve-and-set action: it picks the best
  available backend (PipeWire if available, else System), stores that
  concrete value and updates the dropdown + device picker, so backend_type
  is never null.
- Explicit PipeWire (DAC passthrough) and ALSA Direct (exclusive) configs
  are untouched and remain bit-perfect.

cc
2026-05-24 21:13:11 -06:00
vicrodh 3d29cf1384 fix(graphics): make recommendation advisory-only, GPU-aware DMA-BUF help
The graphics recommendation banner no longer writes settings: the Apply button and the v2_apply_graphics_recommendation command are removed, so the banner only detects and explains. The user applies the change with the real toggles, matching the HiFi Wizard's advisory model. write_recommendation stays for the qbz --autoconfig-graphics CLI.

The Force DMA-BUF help text is now GPU-aware: the instability warning only shows on NVIDIA-only systems; non-NVIDIA and hybrid hosts are told it is safe to enable. On hybrid hosts the rendering GPU dropdown marks the integrated GPU as recommended.

cc
2026-05-16 12:22:41 -06:00
vicrodh 1bb8d53dd0 feat(graphics): NVIDIA Wayland compat toggle + watchdog fallback to CPU
Two-pronged improvement to the graphics safety net for users hitting
the NVIDIA + Wayland + WebKit GBM issue:

1) New opt-in toggle 'NVIDIA Wayland compatibility (experimental)' in
   Settings > Graphics. When ON:
   - Sets __NV_DISABLE_EXPLICIT_SYNC=1 to bypass libnvidia-egl-gbm's
     buggy implicit-sync paths that produce "Failed to create GBM
     buffer of size NxN: Invalid argument" on driver 550+.
   - Lifts the defensive compositing-mode shutdown that QBZ applies
     to NVIDIA-only Wayland sessions — the user is signaling the real
     fix is now in place, so the safety net is no longer needed.
   - Persisted in graphics_settings.db column nvidia_compat_mode (new,
     default off). Migrated via ALTER TABLE so existing installs don't
     pick up a behaviour change.

2) Reset-graphics CLI affordance, surfaced only when the toggle is on:
   inline copyable input + Copy button rendering `qbz --reset-graphics`
   (or `flatpak run com.blitzfc.qbz --reset-graphics` based on detected
   install method). Works around the global select-disable elsewhere
   in SettingsView via user-select: all + onclick={e.select()} + the
   explicit button. The boot watchdog still auto-reverts on crash, but
   this is the manual escape hatch when the user wants to recover
   without waiting for the watchdog to fire.

3) Watchdog recovery now lands in full CPU mode after ANY revert.
   The single-knob revert (hw_accel / dmabuf / preferred_gpu) still
   narrows the cause for the UI banner, but the safest post-crash
   state is full CPU mode (HW accel off + DMA-BUF off + preferred_gpu
   reset to auto). A partial revert can still hit the next crash if
   the failure was stack-wide (driver bug, missing extension); going
   all-the-way-CPU guarantees the user always reaches a paintable
   window after a recovery — they can selectively re-enable from the
   Settings banner.

4) preferred_gpu='software' now implies hardware_acceleration=off at
   env-var application time. Forcing Mesa llvmpipe while keeping
   WebKit GPU compositing on would just round-trip software pixels
   through a "GPU" path that has no GPU — nonsensical configuration
   that the watchdog had to clean up after. Now it's prevented at
   source.

i18n keys (en/es/de/fr/pt): settings.graphics.nvidiaCompatMode,
nvidiaCompatModeDesc, resetCmdTitle, resetCmdDesc;
actions.copiedToClipboard.

cc
2026-05-14 09:18:56 -06:00
vicrodh b2960e8029 feat(graphics): smart GPU preference with boot watchdog and crash recovery
Solves the long-running performance complaints (especially Optimus
laptops where the dGPU silently hijacks WebKit). The end-to-end shape:

- graphics_detection enumerates physical GPUs from /sys/class/drm and
  cross-references EGL vendor JSONs under /usr/share/glvnd/egl_vendor.d
  to expose only stacks the running process can actually use. Works
  inside Flatpak/Snap because /sys is still mounted read-only and the
  sandbox-visible EGL vendor JSONs are the right thing to read.
- preferred_gpu column in graphics_settings.db (string: auto, integrated,
  discrete, software, or a specific PCI slot id). Migrated with
  ALTER TABLE ... DEFAULT 'auto'.
- env_vars_for_preferred_gpu returns Set/Unset actions applied at startup
  before WebKit init. Integrated/AMD path explicitly unsets
  __NV_PRIME_RENDER_OFFLOAD so KDE/Optimus sessions that pre-set it to 1
  no longer override the EGL pin and yank rendering back onto NVIDIA.
- Auto resolver: on hybrid systems (iGPU + dGPU both usable), Auto pins
  to the iGPU. Single-GPU and no-GPU systems remain no-op. The dropdown
  surfaces what Auto resolved to ("Auto (Intel Graphics)") so the user
  sees the decision without picking explicitly.
- boot_watchdog writes a pending.json before WebKit init listing the
  risky knobs in effect. Frontend calls v2_mark_boot_succeeded after
  first paint to clear it and promote to last-good.json. If the next
  launch sees pending without a success ack, it blames matching fields,
  reverts them in-DB, and increments per-knob failure counters; two
  consecutive failures lock the knob until the user clears it.
- Settings > Graphics gets a Rendering GPU dropdown (between HW accel
  and DMA-BUF) and a crash-recovery banner that lets the user re-enable
  any auto-disabled knob. Restart-required affordance shared with the
  other risky toggles.
- i18n keys for the dropdown, banner copy, and autoWithTarget label
  across all five locales.

cc
2026-05-14 00:36:52 -06:00
vicrodh d0349119d0 feat(settings): add dismiss + manual re-detect to the graphics recommendation banner
Two interactions missing from the original banner landing:

  - Dismiss. The banner was always-on while the recommendation differed
    from persisted state — fine the first time the user sees it, but
    annoying if they read it, decided to keep their tuning, and didn't
    want the banner returning every time they opened the Graphics tab.
    Adds a Dismiss button that writes the recommendation's rationale
    string (joined) to a localStorage key as the dismissed signature.
    The banner stays hidden as long as the currently computed
    recommendation produces the same signature; if the environment
    changes enough to switch matrix branches (different GPU vendor,
    X11<->Wayland flip, VM detected), the signature mismatches and
    the banner re-surfaces so the user can re-evaluate.

  - Manual re-detect. Always-visible "Detect recommended settings"
    link above the banner area runs detection on demand and clears
    the dismiss flag. If the recommendation already matches the
    persisted state (no diff) the user gets a success toast confirming
    so — otherwise it would look like the button did nothing.

Apply now also clears the dismiss flag as a hygiene step — after Apply
the diff goes to false and the banner naturally hides, but the stale
signature would otherwise sit in localStorage waiting to suppress a
future legitimately-different recommendation.

i18n: 3 new keys per locale (dismiss / redetect / noChanges) across
en/es/de/fr/pt.

cc
2026-05-13 18:12:33 -06:00
vicrodh 700d5ef2c6 feat(settings): surface graphics recommendation banner in the Graphics tab
The detection + decision matrix in autoconfig_graphics.rs already knew
the right combination for every (GPU vendor x display server x desktop)
case, but the only way to consume it was the qbz --autoconfig-graphics
CLI tool which most users never run. The recommendation sat invisible
while users hunted toggles by hand.

Lifts the same logic into the Settings UI as a persistent banner at
the top of the new Graphics tab:

  - autoconfig_graphics: Environment and Recommendation structs become
    public with Serialize, the three pieces (detect_environment,
    compute_recommendation, write_recommendation) become pub so they
    can be re-used. The existing apply_recommendation CLI helper now
    delegates writes to the shared write_recommendation, keeping the
    behaviour identical (and inverting the DMA-BUF mapping so that
    rec.disable_dmabuf=false now writes force_dmabuf=true, matching
    the 1.2.13 opt-in semantics).

  - commands_v2/graphics_recommendation.rs (new): two thin Tauri
    commands — v2_get_graphics_recommendation returns the environment
    plus the computed recommendation so the frontend can compare it
    to persisted settings; v2_apply_graphics_recommendation writes
    the whole bundle in one call.

  - Settings UI: the Graphics tab loads the recommendation on mount.
    A $derived computes whether at least one recommended value
    diverges from the four persisted settings (HW accel / force_x11 /
    gsk_renderer / force_dmabuf with the inverted DMA-BUF axis).
    When it does, a violet-accent banner appears above the profile
    grid showing the detected environment, the rationale lines from
    the matrix, and an Apply button. Persistent (no dismiss) — vanishes
    the moment the persisted state matches the recommendation, no
    second copy of state to keep in sync.

  - i18n: 4 new keys per locale (title / detected / apply / applied)
    across en/es/de/fr/pt. The rationale strings stay in English
    because they're hardcoded in autoconfig_graphics.rs and reword
    them per scenario would mean a structured i18n key per case —
    not worth it for diagnostic copy that's read once and dismissed.

cc
2026-05-13 16:46:29 -06:00
vicrodh 11721594f4 refactor(settings): split Graphics out of Appearance into its own section
Appearance was getting kilometric — themes, layout, immersive panels,
immersive FPS, and an embedded collapsible "Composition" subsection
(GDK / GSK / X11 / Wayland / DMA-BUF / env-var reference) all under one
tab. The Composition piece is its own business unit (GPU infrastructure
that affects whether the app runs, not how it looks), and burying it
inside a collapsible inside an unrelated tab made the recommendation
work in the next commit awkward to surface.

Moves the entire Linux-only Composition block to a new top-level
Graphics section. Flattens the collapsible wrapper since the whole page
is the section now — the collapse-to-summary affordance only made
sense when it was a sub-block. The Graphics nav entry is injected
dynamically after Appearance and only on Linux (matches the existing
gating; macOS/Windows have no GDK/GSK/DMA-BUF surface to expose).

What stays in Appearance: themes, fonts, layout density, immersive
background mode, immersive panels per-panel toggles, immersive FPS
preset. Those control what the user sees, not the GPU infrastructure.

Locale subtrees move with the section — settings.appearance.composition
becomes settings.graphics across en/es/de/fr/pt. Title and summary
strings rewritten for the new "Graphics" framing in each locale; the
~30 leaf keys (hardwareAcceleration, forceDmabuf, profiles, env-vars
reference, etc.) keep their existing translations and just change path.

The unused compositionCollapsed state var drops out as a side effect.

cc
2026-05-13 16:27:25 -06:00
Victor RH 0daca399cd Merge pull request #420 from afonsojramos/refactor/internal/macos-shared-mode-followup
refactor(audio): macOS shared-mode polish + review follow-ups from #410
2026-05-13 10:56:38 -06:00
Afonso Jorge Ramos a251849094 i18n(toast): add audioInitFailed translation key
Add localized strings for the new backend audio init failure toast
(en, de, es, fr, pt). Reason placeholder receives the backend's
already user-friendly message (e.g. "macOS audio device changed its
sample rate during stream open...").
2026-05-13 13:09:31 +01:00
vicrodh 4b29030820 feat(immersive): CPU-mode lite path + per-panel toggles + warning modal + badge unification
Comprehensive set of changes that make immersive mode usable on
machines without hardware-accelerated GPU compositing (Wayland NVIDIA
with the broken driver path, software-rendered llvmpipe, VMs, etc.).

Detection lives in `graphicsState.ts` — when `bootstrapGraphicsState()`
resolves and `hardware_accel_enabled` is false, we add an
`html.no-hwaccel` root class. Every CPU-mode rule below gates itself
on that class so GPU users see zero change.

Chrome-level work (ImmersivePlayer wraps every panel):

  - Blanket CSS rule on `.immersive-player` and descendants kills
    every `transition`, `animation`, `box-shadow`, and `text-shadow`.
    Box-shadows with 32-60px blur radii were the dominant per-frame
    cost — confirmed game-changer when stripped (user-tested on
    Visualizer with `0 8px 32px + 0 20px 60px` halo around the
    artwork).
  - `.player-bar`, `.window-menu`, `.tabs`, `.viz-dropdown`,
    `.viz-submenu`, and the quality-badge backdrop become opaque
    `#000` and drop `backdrop-filter` (40px / 16px / 12px blurs).
  - Whitelisted animation exceptions: the 4-bar equalizer in
    Coverflow's now-playing indicator (4 x 3px scaleY, basically
    free) and `fadeInUpLite` for LyricsFocus line changes
    (opacity + transform only, no `filter: blur`).

Per-panel LOW_PROFILE flag (captured from `isHardwareAccelEnabled()`):

  - Oscilloscope: skip `shadowBlur: 8` glow, fake with fat-underlay
    + thin-opaque stroke pair; force dpr=1.
  - Lissajous: skip shadowBlur: 6 with same trick, trail 8 -> 3
    frames, drop the 85-fillRect-per-frame dot cloud, hide the
    blurred watermark, dpr=1.
  - EnergyBands: drop per-band energy-driven shadowBlur (up to
    ~20px x 5 rings); halo-arc + ring trick; dpr=1.
  - Visualizer: taller cubes (14px vs 6px, half the count per bar);
    drop the per-cubelet gradient entirely so the whole frame uses
    a single `fillStyle`; dpr=1.
  - Coverflow: snap side-cover transitions (only `.center` keeps
    its 500ms transform); neutralize hover transforms on side
    covers (rotateY + scale 1.05 forces re-rasterization under
    software perspective); side covers go to opacity:1 so they
    don't pay alpha compositing.

LyricsLines split mode in immersive: restore line transitions for
opacity, color, and transform (all compositor-only). Skip the
font-size transition because that one forces re-layout per frame.

New per-panel enable/disable store (`immersivePanelsStore.ts`):

  - localStorage-backed `Record<ImmersivePanelId, boolean>`.
  - In CPU mode the five inherently-heavy panels default to
    disabled: AlbumReactive, NeonFlow (Laser), TunnelFlow,
    CometFlow, TransientPulse. These rely on shaders, particle
    systems, or dense per-pixel filter passes that simply cannot
    work in software compositing.
  - `applyCpuModeDefaultsIfNeeded()` re-applies defaults after
    `bootstrapGraphicsState()` resolves, so first-launch CPU users
    don't have to relaunch to see the heavy panels hidden. Wiring
    lives in `bootstrap.ts` to avoid a circular import between
    the store and graphicsState.
  - ImmersiveHeader filters the visualizer dropdown and Neon
    submenu by enabled state; if all three Neon variants are off,
    the submenu item disappears entirely. The `Visualizer` main
    tab falls back to the first enabled viz option instead of
    hardcoded spectral-ribbon.

First-time CPU-mode warning modal (`CpuModeWarningModal.svelte`):

  - Built on ModalLite. Triggered from `+page.svelte` via $effect
    when immersive opens, HW accel is off, and the user has not
    dismissed the warning before. Disarmed for the rest of the
    session after fire.
  - "Don't show again" checkbox writes
    `qbz-cpu-mode-warning-shown-v1` to localStorage when checked.
  - Explains which panels are hidden and points to Settings ->
    Appearance -> Composition and -> Immersive Panels.

Settings UI: new "Immersive Panels" collapsible section above the
existing "Immersive FPS" section in Settings -> Appearance. Per-panel
toggle, with a GPU badge + warning paragraph on the five heavy panels
when running in CPU mode.

QualityBadge unification (out of CPU-saver scope, addressed in same
branch by user request): every immersive callsite that previously
used the reactive `QualityBadge` (compact or full) now renders
`QualityBadgeStatic` — the same component used in Collection /
Mixtape detail lists. Sixteen callsites total: nine canvas panels
(compact mode), five center-content panels (full mode), plus
ImmersivePlayer split mode and ImmersiveControls. `QualityBadgeStatic`
gains `text-align: start` on its `.badge-text` block so it renders
correctly inside parents that center their content (VisualizerPanel,
StaticPanel, etc.).

i18n: new keys for `settings.appearance.immersivePanels.*` (title,
summary, desc, cpuWarning, heavyTooltip, warningModal.{title,body,
hint,dontShowAgain,acknowledge}) across en/es/de/fr/pt.

cc
2026-05-12 21:21:59 -06:00
vicrodh 145831fec6 feat(local-library): opt-in album artwork in tracks/folders list (#412)
User request: show the album cover thumbnail next to each track in
the Local Library Tracks and Folders list views (same UX the Qobuz
tracks and playlist views already offer).

Default OFF per the 2026-05-12 user call: a library with 16k+ local
tracks would render that many decoded images per scroll session,
and the user shouldn't opt into that without knowing. The setting
lives in `libraryPerformanceStore` (`showTrackArtwork: false` by
default) alongside the other library-perf knobs.

Frontend wiring:

  - `libraryPerformanceStore.ts` — new `showTrackArtwork` field,
    `isTrackArtworkEnabled()` getter, plumbed through `updateSetting`.
  - `LocalLibraryView.svelte` — subscribes to the store, passes
    `showArtwork={showTrackArtwork}` and a
    `getArtworkUrl={(track) => getArtworkUrl(track.artwork_path)}`
    bridge to `VirtualizedTrackList`. `VirtualizedTrackList` and
    `TrackRow` already supported these props (used by Playlist
    detail view).
  - `SettingsView.svelte` — new toggle in Appearance, next to the
    "Show playlist cover collages in sidebar" toggle. Uses the same
    Toggle component and same persistence pattern.

i18n: new `settings.appearance.localLibraryTrackArtwork` +
`settings.appearance.localLibraryTrackArtworkDesc` keys across all
five locales.

cc
2026-05-12 18:20:56 -06:00
vicrodh b2e88c47e9 refactor(settings): hard-disable chrome toggle without GPU, terser note
User feedback: a long explanatory note plus an interactive toggle
that the runtime silently overrides is confusing. The right shape is
"you literally cannot turn this on without a GPU" — hard-disable the
toggle when HW accel is off, and let the note be a one-liner.

Changes:

  - Toggle's `disabled` prop now also depends on `!isHardwareAccelEnabled()`.
    Combined with the existing `hideTitleBar || useSystemTitleBar`
    guards, the user simply can't flip the toggle in CPU mode. The
    visual reads as "available only when conditions are met."

  - Toggle's `enabled` prop is also gated so the visual state matches
    the effective state, not the stored preference, when HW accel is
    off. The stored preference is still preserved on disk and
    reactivates the next time HW accel is on.

  - i18n note shortened to a single line across all five locales:
    "Available only with GPU acceleration." (and equivalents).

cc
2026-05-12 11:34:50 -06:00
vicrodh d1ad0fc888 feat(window): disable transparent chrome when hardware acceleration is off
User finding (2026-05-12): under SW compositing, toggling the
"Match system window chrome" setting OFF turned an unusable modal
experience into instant popups. The transparent main window forces
GTK to repaint the client-side decoration chrome (rounded corners +
subtle edge + window shadow) on every frame. Under GPU compositing
the cost is imperceptible; under SW compositing it becomes the
dominant frame-budget cost — modals, the splash spinner, and any
other animation visibly drop frames because each tick the WebKit
surface has to be recomposited against the CSD chrome.

Auto-gate the transparent window on hardware acceleration so users
in CPU rendering mode (whether by setting or by env var) don't have
to discover this themselves. While we work toward Slint, this is the
single biggest win available for the SW path.

Implementation:

  - `should_use_main_window_transparency()` in src-tauri/src/lib.rs
    now reads QBZ_HARDWARE_ACCEL env var (mirroring main.rs) and
    falls back to graphics_settings.db.hardware_acceleration. When
    HW accel is off, returns false regardless of the user's
    match_system_window_chrome preference. The window is built
    opaque; the frontend's `windowTransparent` reads false from the
    Rust side; the `.match-chrome-transparent` CSS class never
    activates. Round corners disappear, opaque rectangle remains.

  - The user's chrome preference is preserved in the DB. When HW
    accel is re-enabled (next restart with QBZ_HARDWARE_ACCEL unset
    or set to 1), the preference reactivates automatically.

  - SettingsView shows a `<small class="setting-note">` under the
    chrome toggle when `isHardwareAccelEnabled()` is false,
    explaining the override. Toggle itself stays interactive so the
    user can still set their preference for the next HW-accel-on
    session.

  - New i18n key `settings.appearance.matchSystemChromeCpuOverride`
    in all 5 locales.

cc
2026-05-12 11:32:55 -06:00
vicrodh d729c68dfd feat(discovery-v2): spotlight rewrite + artist follow + loading skeleton
Three visual fixes from the latest review:

1. SpotlightLite rewritten faithfully to the legacy ForYouTab structure:
   a hero (portrait + ARTIST label + name + play-top-tracks + go-to-artist
   circle buttons) plus a single paginated row mixing a TOP TRACKS radio
   card, a RADIO card, the artist's Qobuz playlists (as PLAYLIST-labelled
   radio cards) and the artist's albums (AlbumCardLite). Dropped the
   earlier invented sub-blocks (separate Top Tracks list, separate
   Albums grid, separate Playlists row). Internal `handleSpotlightTopTracks`
   and `handleSpotlightRadio` ported from the legacy ForYouTab so the
   play + radio buttons work out of the box from DiscoveryView.

   RadioCardLite gains an optional `label` prop (default "RADIO") so the
   same stereo-component visual can render the TOP TRACKS and PLAYLIST
   variants the spotlight needs.

2. ArtistTileLite gains a subtle hover (background tint, 120ms ease) so
   the cursor reads as interactive on Your Top Artists. With an `onFollow`
   callback it also renders an "Artist" sublabel and a Follow chip with a
   UserPlus icon (or LoaderCircle while pending). DiscoveryView passes
   `onFollow` only for the Artists-to-Follow section via a new
   `artistTileFollowable` snippet; Top Artists keep the bare tile. Follow
   uses `toggleArtistFavorite` and strips the artist from the suggestion
   list on success, matching legacy behavior.

3. Replaced the "Discovery V2 — under construction" placeholder with a
   loading skeleton: three placeholder sections (title bar + a row of
   220x220 card silhouettes) shown until the three main fetches
   (release-watch, discover-index, home-resolved) resolve. Cero efectos
   (no shimmer, no animation) — the shape alone signals "loading".
   Removed `discovery.comingSoon` from all 5 locale files; the truly
   empty case now falls through to `empty.noContent`.

cc
2026-05-12 10:56:09 -06:00
vicrodh 556a345cbd feat(discovery-v2): RadioCardLite + Spotlight radio + Mixes resize + i18n fixes
Three issues from the latest visual review:

1. Spotlight was missing the RADIO card that anchors the Qobuz layout.
   Added `RadioCardLite.svelte` — a 220×220 stereo-component-styled
   card with vertical speaker-bar pseudo-elements, the seed's artwork
   centered as the "display", a RADIO wordmark, and a background color
   pulled from the artwork's dominant palette (artworkPalette). Used
   in SpotlightLite as the first item of the albums row alongside the
   six release tiles, and as the renderItem for the new "Radio
   Stations" section in DiscoveryView (seeded from the union of
   recentlyPlayedAlbums + favoriteAlbums, deduped, sliced to 12).

2. Three untranslated keys in SpotlightLite. The keys lived in
   different namespaces than I'd guessed:
     - `home.popularTracks`        → `artist.popularTracks`
     - `home.albums`               → `artist.albums`
     - `home.radio.startArtistRadio` → `actions.radio.startArtistRadio`
   Added one new key `discovery.qobuzRadioStation` for the radio card
   subtitle, across all 5 locales.

3. QobuzMixesRow tiles were stretched by `grid auto-fit minmax(200px,
   1fr)` so on wide windows they ran 280-320px each — visually outsizing
   every other card and making the section feel cartoonish. Switched
   to `display: flex; flex-wrap: wrap; gap: 32px` with fixed 220px
   tiles. Same scale as AlbumCardLite now.

cc
2026-05-12 10:21:17 -06:00
vicrodh 5a8d3a2023 feat(discovery-v2): For-You-exclusive sections (Spotlight, Rediscover, etc.)
Closes parity with the legacy ForYouTab by adding the 7 sections it
had that aren't shared with Home / Editor's Picks:

  - qobuzMixes (Home + For You) — 4-tile entry point to DailyQ /
    WeeklyQ / FavQ / TopQ via existing onNavigate* callbacks.
    Implemented as `QobuzMixesRow.svelte` (CSS grid + gradient
    artwork tiles, no fetch).
  - rediscoverLibrary (For You only) — albums favorited but unplayed
    recently. Wraps `v2_reco_get_forgotten_favorites`.
  - artistsToFollow (For You only) — similar artists derived from
    user's top-3 seeds, deduped against already-followed. Wraps
    `v2_get_cached_favorite_artists` + `v2_get_similar_artists` × 3.
  - similarAlbums (For You only) — Qobuz suggestions seeded by a
    random recent album. Dynamic title "Similar to {seed}".
  - essentialsByGenre (For You only) — `ideal-discography` featured
    albums for the user's top genre. Dynamic title "Essentials in
    {genre}". Wraps `v2_reco_get_top_genres` + `v2_get_featured_albums`.
  - artistSpotlight (For You only) — featured-artist hero with
    portrait + top 5 tracks + 6 albums + playlists. Wraps
    `v2_get_artist_page`. Implemented as `SpotlightLite.svelte`,
    minimal but representative: no radio-card extraction, no
    spotlight color theming (deferred — both were cosmetic in legacy).

Type system: 7 new IDs added to `DiscoverySectionId`. `DEFAULT_PREFS`
now reflects per-tab availability (the customize modal naturally hides
sections not present in the active tab's prefs list):

  - home: 12 universal + qobuzMixes
  - editorPicks: 7 editorial-only (no personalized ever)
  - forYou: 12 universal + qobuzMixes + 6 exclusives + spotlight

Data layer: 5 new fetch functions in `data.ts` (`fetchRediscoverLibrary`,
`fetchArtistsToFollow`, `fetchSimilarAlbums`, `fetchEssentialsByGenre`,
`fetchArtistSpotlight`) and 3 new typed shapes (`SimilarAlbumsSection`,
`EssentialsSection`, `SpotlightSection`). All fired in parallel after
home-resolved completes (the seeds — top artists, recent albums — are
needed first). All errors caught and degraded to empty results so a
broken backend response doesn't break the whole view.

i18n: adds `discovery.{similarAlbums, similarTo, rediscoverLibrary,
essentialsByGenre, essentialsIn, artistsToFollow, artistSpotlight}`
across the five locales.

radioStations is registered as an ID but not yet rendered — the legacy
implementation derived radio cards from recent + favorite albums, will
land in a follow-up commit.

cc
2026-05-12 10:09:06 -06:00
vicrodh b4e08ac5e3 feat(discovery-v2): wire add-to-playlist + songlink share to album quick menu
Completes the AlbumQuickMenu surface with the two remaining actions the
parent already passes through Props but Discovery hadn't been forwarding:

  - Add to Playlist (onAddAlbumToPlaylist → onAddToPlaylist on the
    menu): opens the parent's add-to-playlist modal.
  - Share Songlink (onAlbumShareSonglink → onShareSonglink): copies the
    Songlink URL to the clipboard via the parent's existing flow.

i18n: adds `actions.shareSonglink` ("Share (Songlink)") across the five
locales. `actions.addToPlaylist` already existed and is reused. The two
actions render only when the corresponding callback is passed, so cards
on sections that don't wire them (no current case) silently omit the
items.

Reuses the same AlbumQuickMenu instance — no new component, just two
extra optional props forwarded through AlbumCardLite. This sets up the
shared menu surface that the upcoming AlbumRowLite kebab will reuse for
the Top Albums section, avoiding duplicate menu logic across cards.

cc
2026-05-12 07:49:14 -06:00
vicrodh 5d33db2d14 feat(discovery-v2): wire kebab to minimal action quick-menu
Replaces the stub `onMenu` callback with a real inline popover.

`AlbumQuickMenu.svelte`: portaled action list, six entries:
  - Play Next
  - Play Later
  - Go to Album (uses the card's own onClick)
  - Go to Artist (uses onArtistClick)
  - Share on Qobuz
  - Download

Each entry only renders if its callback is provided, so cards without a
particular handler (e.g. tracks without an artist link) get a shorter
menu. Positions itself near the kebab via `getBoundingClientRect` and
clamps to the viewport. Integrates with the shared `floatingMenuStore`:
opening this menu auto-closes any other floating menu in the app, and
vice versa. Outside-click closes. No backdrop-filter, no animation
beyond the implicit mount — single popover at a time, minimal cost.

AlbumCardLite now hosts the menu inline. DiscoveryView forwards the
remaining four parent callbacks (onAlbumPlayNext, onAlbumPlayLater,
onAlbumShareQobuz, onAlbumDownload) to the card. Songlink share,
add-to-playlist, and per-album re-download are intentionally omitted
from V1 — they require more parent state plumbing and can land in a
follow-up.

i18n: adds `actions.playLater` and `actions.download` across the five
locales. `actions.playNext`, `goToAlbum`, `goToArtist`, and `share`
already existed and are reused.

cc
2026-05-11 23:14:16 -06:00
vicrodh 871e550da3 feat(discovery-v2): Qobuz-style hover overlay with 3 action buttons
Replaces the bottom-gradient + corner play button with the full-cover
hover overlay pattern from the Qobuz web player:

  - Dark scrim covers the whole artwork on hover (rgba(0,0,0,0.6)
    instead of bottom-fade gradient). Single `::after` element, pure
    opacity transition.
  - Three outline-style action buttons centered on the cover:
       * Primary play (filled white circle, 44px)
       * Favorite/heart (outlined circle, 36px; fills + accent color
         when isFavorite=true)
       * Menu/kebab (outlined circle, 36px; emits onMenu — parent will
         wire to context menu in a follow-up)
  - All three only visible on hover via opacity + pointer-events. No
    slide-in keyframes, no scale-up entrance, no blur. The buttons sit
    on a separate `.actions` container above the scrim — both fade in
    together via the same 150ms transition.

DiscoveryView subscribes to `albumFavoritesStore` and bumps a `$state`
version counter on each notification so per-card `isFavorite` reads in
the snippet re-evaluate when the favorites set changes. `onFavorite`
toggles via `toggleAlbumFavorite(albumId)` which already handles the
optimistic V2 round-trip + cache sync.

i18n: adds `actions.toggleFavorite` and `actions.moreActions` across
the five locales (en/es/de/fr/pt) for aria-label.

The `onMenu` callback is stubbed at the DiscoveryView level — clicking
the kebab does nothing yet. Wiring it to a context menu with the queue
/ share / download actions the parent already exposes via Props is the
next iteration.

cc
2026-05-11 23:03:13 -06:00
vicrodh 3fb9e5540e feat(discovery-v2): customize modal + gear icon + bigger gaps
Three small UX additions:

  1. Discovery settings modal (`DiscoverySettingsModal.svelte`)
     - Wraps shared `Modal.svelte` (z-index, ESC, portal, backdrop).
     - Lists all 12 sections with checkbox toggle + up/down reorder
       arrows. No drag-to-reorder for V1 — chevrons are simpler and
       work with keyboard.
     - Performance warning at the top: "Each enabled section adds to
       the work done on every scroll" with TriangleAlert icon.
     - Live "N of M sections enabled" counter.
     - Reset-to-defaults button in footer.

  2. Gear icon in the toolbar to the right of the genre filter. Opens
     the settings modal. Uses lucide Settings glyph.

  3. Spacing tuning:
     - Inter-card gap: 16px → 32px (doubled). Matches the `gap` JS
       constant used by itemsPerPage math.
     - Inter-section bottom margin: 32px → 48px (~50% more breathing
       room between rows).

i18n: adds `discovery.{customize,perfWarning,enabledCount,moveUp,
moveDown,resetDefaults}` across all five locales (en/es/de/fr/pt).

cc
2026-05-11 21:56:33 -06:00
vicrodh 373018a1ea feat(discovery-v2): scaffold clean-room rebuild of home view
Establishes the skeleton for a Discovery rebuild under "cero efectos"
constraint after a perf debug session confirmed the original home view's
vertical scroll cost was structural and accumulative, not a single
offender. Spec: qbz-nix-docs/specs/2026-05-11-discovery-v2-clean-room.md.

Adds:
  - src/lib/discovery-v2/AlbumCardLite.svelte: 5-element card (cover,
    title, artist, permanent play button). Cero `transition`, cero
    `:hover` paint, cero `will-change`, cero `backdrop-filter`.
  - src/lib/discovery-v2/DiscoverySection.svelte: titled inline CSS grid
    of cards + "See all" link. No `overflow-x: auto` carousel — sections
    wrap onto rows naturally.
  - src/lib/discovery-v2/DiscoveryView.svelte: top-level. Same Props as
    `HomeView.svelte` for drop-in mount swap. Toolbar with three tab
    buttons (Home / Editor's Picks / For You) on the left and a slot
    for the genre selector on the right — fixed 56px height, static
    (no `position: sticky`), part of the main paint pass.
  - i18n keys `discovery.{comingSoon,seeAll}` across en/es/de/fr/pt.

Swaps the mount in `src/routes/+page.svelte:5826` from `<HomeView>` to
`<DiscoveryView>`. `HomeView.svelte` stays in the tree as rollback. Real
sections will be wired incrementally in follow-up commits, each measured
against the perf baseline established by the scripts/measure-cpu-window.sh
tooling.

cc
2026-05-11 20:44:02 -06:00
vicrodh e3bdbc9ab4 revert: roll back titlebar-mode-selector merge
Reverts merge 1be46f1a which introduced the 4-mode titlebar enum
(qbz/system/plasma/hidden), variant-aware rendering (full/stripped),
GDK_BACKEND=x11 coercion for Plasma, and per-mode mounting in +page.

The "stripped strip mounted alongside Plasma SSD" architecture caused
all titlebar modes to render incorrectly in the production binary —
empty space at top, separation gap above the now-playing bar, controls
visible without a backing strip. Reverting restores v1.2.10 behavior:
two booleans (use_system_titlebar + hide_title_bar), no variants, no
GDK coercion. Plasma support is dropped pending a less brittle redesign.

Conflicts in SettingsView.svelte resolved by taking the pre-merge side
for the four titlebar UI blocks; non-titlebar additions in this cycle
(Discord RPC settings, reserve-DAC, offline cache nav) preserved.

cc
2026-05-09 17:48:37 -06:00
vicrodh 2d33d7cb42 feat(player): disable metadata actions for ephemeral tracks
Library-bound actions (favorite, add-to-playlist, track info) require a
DB row to operate; ephemeral tracks live only in memory and would 404
on use. Surface them disabled rather than removed so the affordance
stays discoverable in the chrome — users still see the icon, the
tooltip explains why it's inert.

Surfaces wired
- NowPlayingBar: + and ♥ disabled when active track id >= 2^48.
- QueuePanel: now-playing ♥ + per-track Add to Playlist + Track Info
  in both queue and history menus disabled per-row by id range.
- ImmersiveControls + PlayerControlsCompact: ♥ disabled.
- ImmersivePlayer: passthrough prop to PlayerControlsCompact.
- MiniPlayerFooter / MiniPlayerWindowControls: no metadata actions in
  these surfaces, nothing to gate.

Detection is by id range (>= EPHEMERAL_ID_FLOOR), not by source string.
The setQueueForLocalTracks mapping rewrites `source: 'ephemeral'` to
`'local'` before the track reaches the queue, so a string-equality
check would never fire — id range matches the rest of the ephemeral
codebase (gapless carve-out, CUE boundary, seek offset, queue/now-
playing wipe).

Tooltip on the disabled state uses the new
`actions.unavailableForEphemeral` key, translated for en, es, de, fr, pt.

cc
2026-05-09 09:24:37 -06:00
vicrodh 6d958fc659 feat(local-library): ephemeral pane iteration + folder-tree sidebar polish
A consolidated UI commit covering the LocalLibrary pane work from this
session. Backend additions land in earlier commits; this one is the
frontend surface plus the i18n strings.

Folder-tree sidebar polish (Folders > Tree)
- Search input pushed to the right edge via margin-left:auto so the
  action icons stay anchored on the left.
- FOLDER_TREE_SIDEBAR_DEFAULT_WIDTH and *_MIN both bumped to 290px (was
  302/200) — matches the user-tuned width and sets a coherent floor for
  the resize handle.

Ephemeral pane render
- Group tracks by album_group_key and render an album-style block per
  group: 96px cover, title/artist/year/quality/track-count, per-album
  Play button. Discs render as section headers when an album has more
  than one. CUE-derived virtual tracks slot into the same structure
  (they share the underlying FLAC's group key).
- Single-album sessions hide the per-album play button — it's redundant
  with the folder-level "Play all" header. Multi-album sessions surface
  it so each album has its own affordance.
- Loading spinner replaces the empty-pane state during scan; on slow
  NAS folders the wait used to be silent.
- Close button changed to the BrushCleaning icon ("Clear session" /
  "Limpiar sesion") — the X read as "close window" rather than "wipe
  this transient state". Strings updated in en, es, de, fr, pt.

Ephemeral session lifecycle
- Folder path persists to user storage; rehydrateEphemeralFolder reopens
  it on mount as long as the directory still exists. A `rehydrating`
  flag suppresses the auto-close $effect during the rehydration window
  so a delayed result doesn't get clobbered by a concurrent navigation.
- wipeEphemeralPlaybackArtifacts clears the queue + now-playing slot
  when the active or pinned track is ephemeral. Detection consults both
  activeTrackId AND playerStore.currentTrack — sometimes the track is
  visually pinned without playback being active (e.g. after session
  restore). Backend stop+clear doesn't reset current_track_id at the
  player level, so the frontend explicitly nullifies currentTrack to
  flush the NowPlayingBar chrome.
- Closing/replacing an ephemeral session triggers the wipe; navigation
  to a tree folder or album also triggers it via the watch-effect.

Ephemeral playback flow
- Queue scope is now the album the played track belongs to, not the
  whole folder. Folder-level "Play all" still queues everything in
  album-then-disc-then-track order; track-row clicks queue just the
  album. Per-album play button matches.
- Loading toast (buffering type) shows immediately on play to cover the
  multi-second file read off NAS. Dismissed when v2_library_play_track
  resolves.
- CUE virtual-track boundary advance: a raw playback:state listener
  watches absolute FLAC position and, when the displayed CUE track's
  cue_end_secs is crossed, calls v2_next_track + setCurrentTrack(next)
  + syncQueueState. Audio is unchanged (continuous decode of the same
  FLAC); only the chrome flips. Seekbar resets to 0:00 because the
  player's track_id stays pinned to the first virtual track — known
  limitation queued for a follow-up that virtualises position display
  via cue_start_secs offsets.

cc
2026-05-09 05:16:01 -06:00
vicrodh aa015829aa feat(local-library): ephemeral folder playback (no DB persistence)
Adds an "Open Folder" button in the LocalLibrary > Folders tree toolbar
that lets the user point QBZ at any folder on disk and play it without
adding anything to local_tracks. Lets the app act as an ad-hoc media
player for downloaded albums, external drives, etc.

Backend:
- New ephemeral_library module: in-memory HashMap<i64, LocalTrack> keyed
  by synthetic negative ids. Reuses LibraryScanner and MetadataExtractor
  to walk the folder and build LocalTrack records.
- Three V2 commands: v2_ephemeral_open_folder (scan + populate, replaces
  any prior session), v2_ephemeral_clear, v2_ephemeral_get_track.
- v2_library_play_track gets a small branch at the top: track_id < 0
  resolves through the ephemeral state and goes straight to the regular
  local-file playback path (read file, hand to player). DB-backed paths
  (Qobuz cache, CUE start positions) are skipped — ephemeral tracks
  can't be any of those.

Frontend:
- "Open Folder" button (FolderOpen icon) next to the collapse-all button
  in the tree toolbar. Click opens the OS file picker, the result lands
  in a new ephemeralFolder $state.
- Right pane gets a new branch above the existing album/folder views:
  when ephemeralFolder is set, render a dedicated pane with header
  (folder name, path, track count, Play All, Close) and a flat track
  list with play-on-click rows.
- Lifecycle per spec: a session survives until (1) a different album or
  tree folder is selected — handled via $effect that watches
  selectedAlbum and selectedFolderPath, (2) the user clicks the Close
  button, or (3) the backend reports the file/directory is gone (caught
  in the play handler and folded into the same close path).
- Translations for en, es, de, fr, pt.

Notes:
- The audio_cache key is `track_id as u64`, which for negative ids wraps
  to a large u64 value — unique and safe (DB ids are positive).
- Edit-metadata gating for ephemeral tracks is deferred: there is no
  edit trigger from the new pane yet, so the sidecar/direct toggle
  behaviour is unchanged. Will gate when an edit path lands.

cc
2026-05-09 02:13:44 -06:00
vicrodh 8cde516887 feat(player): sleep timer with hard-stop pause (issue #402)
Adds a frontend-only sleep timer button to the queue panel toolbar. When
the deadline expires the timer invokes cmdPause() — no fade, no DSP, just
a hard stop consistent with QBZ's bit-perfect posture.

UX:
- Clock icon button next to the infinite-play toggle in QueuePanel.
- Click opens a popover with presets (30 min, 1 hr, 2 hr, 3 hr, 5 hr) plus
  a Custom option that reveals a 15-1440 minute number field.
- Once active, the button turns green and a small green countdown label
  appears beside it (no background, no border) showing the remaining time.
- Clicking the button again opens an Edit/Cancel view; Edit pre-fills the
  form with the current duration so the user can re-pick.

Behavior per spec:
- Frontend-only (single setTimeout fires the pause), no backend command.
- Timer does not persist across app restarts.
- Hard pause only; the app stays open so the user can resume where it
  stopped.

Translations added for en, es, de, fr, pt.

cc
2026-05-08 23:21:46 -06:00
vicrodh a942551e45 feat(local-library): album-art context menu with view + edit options
Right-clicking the artwork in the compact album view now opens a
small context menu with two options: View album art (opens the
existing ImageLightbox component) and Edit metadata (the existing
edit-modal flow). Click outside or Escape closes the menu.

cc
2026-05-08 21:15:13 -06:00
vicrodh b5f0acfac4 feat(local-library): folder-detail polish — circular play, search, list view
The big pill "Play all" goes away. Header gains a circular primary
Play button (action-btn-circle.primary) that replaces the folder
glyph and triggers recursive playback. A new right-aligned search
filters subfolder cards by name. View-mode toggle switches between
the existing grid and a new compact list view of subfolders.

cc
2026-05-08 20:37:17 -06:00
vicrodh fe7320b968 feat(local-library): tree toolbar revamp - flat check, collapse-all, dedicated search
The select-mode toggle drops its backgrounded style for a flat icon
that just changes color when active. New collapse-all button next to
it clears every expanded folder. Tree mode gains its own dedicated
search input alongside the toolbar buttons, decoupled from the
global album search. Folder rows lose the inline (N) track count for
a cleaner Plex/foobar register; the underlying count fetch stays in
place.

cc
2026-05-08 20:22:20 -06:00
vicrodh 9f1c3b37be feat(local-library): standard buttons + per-view track search in compact album
The compact album view's Play All / Shuffle buttons drop the pill
treatment and adopt the standard app button style. A new track search
input filters the displayed list by title or artist; empty results
show a short message. New i18n keys added to all five locales.

cc
2026-05-08 20:16:53 -06:00
vicrodh fc9b59c1dd feat(local-library): drag-resizable tree sidebar (default x 1.8)
Sidebar default widens from 240px to 432px. A 6px right-edge handle
resizes between 200px and 40% of the content area. Width persists
across restarts via v2_set_library_folders_tree_sidebar_width on
drag-end. ResizeObserver tracks content-area changes (window resize +
app sidebar toggle) and re-clamps the user width when the cap shrinks.
Keyboard parity: arrow keys nudge in 16px steps (64px with Shift),
Home/End jump to bounds. Aria semantics use role="separator" with
valuenow/min/max. Five-locale i18n for the handle label.

cc
2026-05-08 17:49:37 -06:00
vicrodh 3a09d8deb8 feat(local-library): recursive multi-select in tree mode
Folder-row checkbox in tree mode selects every descendant track via
v2_library_list_folder_tracks_recursive, deselects them on a second
click, and shows the standard indeterminate state when only some
descendants are selected. Track-kind rows in the tree honour the
same flow so users can deselect individual tracks from a partially-
selected folder. Both checkboxes share the existing selectedTrackIds
set, so the bulk-action bar (play next, play later, add to playlist)
operates on the union without any special-case handling.

Indeterminate state is computed cheaply: a SvelteMap<number, string>
caches track-id -> file_path (populated from the in-memory tracks
list and from each recursive fetch on toggle), and `countSelectedUnder`
walks the small selectedTrackIds set checking the prefix. Folder-row
state derives via a parent-supplied callback; the child reads the
prop reference of selectedTrackIds so reactivity propagates when the
parent reassigns the set.

Adds a dedicated treeSelectMode flag so the flat-mode tracks-tab
selection toggle stays independent. Renders the BulkActionBar at the
end of the tree-mode container when treeSelectMode is on. Adds a new
toast.failedSelectFolderTracks key across all five locales.

cc
2026-05-08 16:27:34 -06:00
vicrodh 5c5fce2423 fix(i18n): relocate folder-tree keys to library.foldersTree namespace
Task 4's structural change of `library.folders` from string to object
broke LocalLibraryView.svelte:4337's dynamic tab label
($t(`library.${tab}`)) which expects a string. Move the new tree keys
to a sibling namespace `library.foldersTree.*` instead, and restore
`library.folders` to its original flat string value.

cc
2026-05-08 14:56:30 -06:00
vicrodh 107efc0f87 i18n: add library.folders.tree keys for tree-mode UI
Five locales (en, es, de, fr, pt). en and es are human-written in the
project's canonical Mexican neutral register; de/fr/pt are machine-
translated drafts flagged in qbz-nix-docs/i18n/pending-translations.md
for community review. Seven keys: modeLabel, flatLabel, selectAFolder,
treeFolderTracks (ICU plural), treeLoading, treeEmpty, playAllRecursive.

The flat library.folders string ("Folders" / "Carpetas" / etc.) was
migrated to an object; the original label is preserved at
library.folders.label. No source code currently references the key,
so the restructuring is non-breaking. "Track" is translated to match
each locale's existing convention (pista, Titel, titre, faixa).

cc
2026-05-08 14:51:57 -06:00
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 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
Vudgekek 5cd76f53d4 Merge branch 'pre-release' into feature/external/macos-exclusive-mode 2026-05-07 15:34:44 -05: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
Vudgekek 96fd15cb33 fix(settings): enable macOS exclusive mode 2026-05-06 23:26:16 -05: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 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 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 1a5c9b81ce feat(local-library): rename Albums tab to Folders, add tab order modal
Frontend half of Phase 2: rename the existing 'Albums' tab (which has
always shown folder-grouped data) to 'Folders' so the label matches
the behavior. NorinB filed issue #380 because of this discrepancy.

Wires LocalLibraryView to the new per-user library_preferences backend
(landed in the previous commit). LibraryEditModal is a clone of
FavoritesEditModal trimmed to the Library prefs scope (tab_order +
hidden_tabs only).

Tabs render in the user-defined order, hidden tabs are skipped. Active
tab on session start is the first visible tab. Default order matches
the current tab list with 'folders' in place of the old 'albums';
Phase 3 will add the new metadata-grouped 'albums' to the default.

i18n updated in 5 locales: new library.folders key + library.editTabs
block. Existing 'Library Folders' settings header moved to the new
library.folderListTitle key to free up library.folders for the tab
label.

cc
2026-05-06 16:57:38 -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 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 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