Commit Graph
2825 Commits
Author SHA1 Message Date
vicrodh 22687e7805 chore(player): lower sleep timer custom minimum to 1 min
15-minute floor was arbitrary; the user wants the lower bound at 1 minute
for short audio-segment workflows. Constant change propagates to both the
store's clamp and the input's min attribute.

cc
2026-05-09 01:11:53 -06:00
vicrodh 4d2b59eb18 fix(player): smart-positioned sleep timer popover with refined typography
Two issues from initial review:

1. The popover was anchored with position: absolute relative to the toolbar
   wrap, so when the queue panel sat near the viewport edge or the user had
   the window short, the popover overflowed and got clipped. Switched to
   position: fixed with a viewport-aware placement helper that prefers
   above the trigger, flips below when there isn't headroom, and shifts
   horizontally to stay inside the viewport. Recomputes on resize and on
   any ancestor scroll (capture-phase) so the popover stays anchored.

2. The Dropdown trigger was the visual peak of the stack — its default
   14px label dwarfed the surrounding eyebrows and made the popover read
   as inconsistent. Restructured the layout to a stacked eyebrow-above-
   control rhythm with one restrained type scale (11px eyebrow / 13px
   control / 12px action) and overrode the Dropdown trigger locally so
   it matches the input field. The countdown stays slightly larger (15px)
   as the deliberate focal point of the active state.

cc
2026-05-09 01:00:06 -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 f447385fa0 chore(local-library): silence svelte a11y warnings on tree sidebar resize handle
The tree-sidebar resize handle uses role="separator" with aria-orientation,
aria-valuenow/min/max and arrow-key handlers — the WAI-ARIA splitter
pattern. Svelte's a11y linter classifies role="separator" as
non-interactive and flags both tabindex="0" and the mousedown/keydown
listeners, even though both are correct for a resizer.

Suppress the two rules locally with svelte-ignore directives so the
console stops printing them on every dev reload. Behavior unchanged.

cc
2026-05-08 23:07:28 -06:00
vicrodh 2f2dbe626b perf(local-library): push sort into SQL and drop localeCompare from groupTracks
Two cooperating changes that flatten the long sort tail in the Tracks tab:

1. search_with_filter now ORDER BYs (album, COALESCE(album_artist, artist),
   disc_number, track_number, title) COLLATE NOCASE — matching the default
   album-grouped browsing. SQLite handles 100K rows in well under 100ms;
   the JS sort took several seconds for the same volume.

2. groupTracks replaces the chained localeCompare with decorate-sort-
   undecorate using precomputed composite keys (lowercase strings with a
   sentinel separator and zero-padded numeric segments). The Intl collator
   instantiation per comparison was the bulk of the per-pass cost.

Both changes preserve the existing ordering semantics for the album, artist
and name modes; locale-specific accent ordering becomes lowercase ASCII
order, which is acceptable for library browsing.

Tier 3 of three.

cc
2026-05-08 23:04:36 -06:00
vicrodh bc4cd85a08 perf(local-library): viewport-driven Plex hydration with quality override map
Replace the bulk Plex hydration that fired on every Tracks-tab open with a
viewport-driven path. VirtualizedTrackList now emits onVisibleTracksChange,
LocalLibraryView debounces those events (150ms) and hydrates only the rows
in the visible window — at most a few dozen calls instead of 16K+.

Hydration results land in a reactive plexQualityOverrides map (SvelteMap).
getQualityBadge consults the map for plex tracks, so badges update without
reassigning `tracks`. That removes the second full pass through groupTracks
that the old `tracks = [...localTracks, ...hydratedPlexTracks]` reassignment
used to trigger after every hydration completion.

loadTracks is now a single reactive write; the requestId still invalidates
in-flight viewport batches when the search query changes.

Tier 1b + Tier 2 of three.

cc
2026-05-08 22:59:33 -06:00
vicrodh 1fe4f759a0 perf(local-library): batch Plex track quality hydration with per-call timeout
The unbounded Promise.all over every Plex candidate flooded the server's
per-host connection limit and held the JS event loop until the whole list
settled. With 16K Plex tracks the freeze ran into minutes.

Replace it with sequential batches of 5 (mirroring the existing pattern in
hydrateUnhydratedPlexTracks) and a 5s Promise.race timeout per call so a
slow/dead Plex host can't stall the whole pipeline.

Tier 1a of three; album-detail view uses the same helper so it benefits
even though its track scope is bounded.

cc
2026-05-08 22:58:12 -06:00
vicrodh c30f1408ca refactor(local-library): align album-art context menu with cover-context CSS
Replace the reinvented .artwork-context-menu-* classes with the
canonical .cover-context-* classes used by AlbumDetailView so the
menu visually matches the rest of the app. Menu still has only the
two simple options that apply to local library (View album art /
Edit metadata) - no custom-cover, browser, or save-as handlers, since
local-library artwork is a file path and cover changes belong to the
metadata editor. The ImageLightbox usage was already correct.

cc
2026-05-08 21:25:12 -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 bf747f836f feat(local-library): nav dropdowns for Local Library
Title bar and sidebar Local Library entries become dropdowns showing
Albums / Artists / Tracks / Folders in the user's configured order.
Clicking a tab navigates to the view and switches to that tab.

Two new shared stores back this:

  - libraryPreferencesStore: tab_order + hidden_tabs, hydrated by
    LocalLibraryView on mount and on save, plus a standalone fetch path
    used by the navigation components when the view hasn't mounted yet.
  - libraryTargetTabStore: a one-shot navigation target consumed by
    LocalLibraryView either at initial-mount time (inside the existing
    initialTabSet handshake in loadLibraryPreferences) or live via
    a dedicated $effect when the view is already mounted.

The title bar uses a hover dropdown that mirrors the existing My QBZ
pattern; the sidebar uses a collapsible parent row with NavigationItem
sub-items, matching the My QBZ section's visual register. Both filter
to the user's visible tabs only and respect the saved tab_order.

cc
2026-05-08 21:08:55 -06:00
vicrodh 02bd927254 style(local-library): drop the gradient HardDrive icon from the header
The page header now leads with the Local Library title alone. The
gradient drive square next to the title fought the rest of the chrome
without earning the visual weight, so it goes away. Markup and the
matching .header-icon CSS rule both removed; the .header flex container
is left intact for the title block.

cc
2026-05-08 20:58:30 -06:00
vicrodh 17686afe9d feat(local-library): right-click album art opens edit modal in tree mode
Right-clicking the artwork in the compact tree-mode album view now opens
the same album-edit modal as the pencil button in the page-level album
view. Plex source still requires the metadata-write opt-in, mirroring
the existing flow.

Implementation routes through a tree-specific helper that bridges the
modal's selectedAlbum / albumTracks dependencies to selectedAlbumForTree
/ treeAlbumTracks, gates the page-level album-detail takeover behind a
treeAlbumEditMode flag so the tree rail stays visible, and clears the
borrowed state through a cleanup effect once both album-edit and
tag-editor modals close.

cc
2026-05-08 20:57:36 -06:00
vicrodh de583baefa feat(local-library): selection + bulk actions in compact album view
Compact album view (tree-mode right pane) gains a selection toggle,
per-track checkboxes, and Play Next / Add to Queue / Add to Playlist
bulk actions alongside the existing album-level Add to Mixtape button.
When tracks are selected the mixtape button bulk-adds those tracks
instead of the whole album. Track search aligns to the right of the
action row (margin-left: auto). Selection resets when the user
navigates to a different album in the tree, so it doesn't bleed
across albums. Local + Plex split mirrors the tracks-tab handler so
each path stays in its own backend namespace.

cc
2026-05-08 20:38:39 -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 23865882b8 style(local-library): tighter tree toolbar (narrower search, 14px icons)
Tree search drops its flex-grow and pins to ~140px width. Check and
collapse-all icons shrink from 16px to 14px (2pt) for a tighter
register that matches the row icon size. Toolbar padding tightens
slightly to compensate.

cc
2026-05-08 20:32:33 -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 6934c699b9 feat(local-library): page-level layout polish for Folders tab
Flat/Tree switch moves inline with the tabs row (right-aligned, low
weight, only on Folders). Page-level search icon hides on the Folders
tab — flat mode has its own inline search; tree mode gains a dedicated
tree-search in a follow-up. Two-column divider extends up to almost
touch the jumpnav by relocating the toolbar inside the tree column.

cc
2026-05-08 20:08:36 -06:00
vicrodh ed7246e82b fix(local-library): tree sidebar horizontal scroll + visible resize pill
The horizontal scrollbar never appeared on .folder-tree-scroll despite
rows declaring width: max-content. Root cause was contain: strict on
the scroll wrapper, which includes size containment; WebKit (Tauri)
under-reported scrollWidth so the bottom scrollbar was suppressed even
when long folder names extended past the rail. Drop size containment
(keep layout + paint for perf) and add flex-shrink: 0 to .row-name so
the flex row doesn't collapse the name and force the row's max-content
width to actually take effect.

The resize handle gains a centered vertical pill (4px x 36px, 40%
opacity by default, accent on hover/drag) so the divider has a visible
affordance independent of cursor rendering quirks under Wayland or
XWayland. Hit area widened to 8px for easier grabbing. Drag/keyboard
behavior and a11y attributes are unchanged.

cc
2026-05-08 19:52:37 -06:00
vicrodh 8d2cd41f88 feat(local-library): tighter tree sidebar default + horizontal scroll
Default width drops from 432px to 302px (~30% smaller). Long folder
names no longer ellipsize - the sidebar scrolls horizontally inside
itself when content exceeds the column width. Drag-resize and the
persisted width preference are unchanged: the user can still widen
to 40% of the content area, and that choice survives restart.

The resize handle stays anchored to the column's right edge because
it sits as a sibling of the scroll wrapper (.folder-tree-scroll),
not inside it - horizontal scroll inside the wrapper does not drag
the handle along.

cc
2026-05-08 19:37:58 -06:00
vicrodh 850cd41c13 style(local-library): minimalist tree rows
Tighter row padding (3px), smaller typography (~12.5px), 12px depth
indent, 14px icons. Removes the artwork thumbnail in favor of always
showing the folder icon. Track count moves inline as a muted suffix
(N) instead of a subtitle row, hidden when zero. Visual register
matches Plex/foobar2000 file-tree conventions.

cc
2026-05-08 19:31:17 -06:00
vicrodh 8ad30db8e3 feat(local-library): compact album view for tree right pane
New LocalLibraryFolderAlbumView renders a stripped album detail
(artwork + metadata + play + track list) sized for the tree-mode
right pane. The full-page album-detail view is unchanged; tree mode
no longer triggers the page-level takeover when an album folder is
selected, so the tree rail stays visible while browsing.

Tracks load into a separate `treeAlbumTracks` state via $effect on
`selectedAlbumForTree` (raceless: commits only when the selection
still matches at resolve-time). Play / shuffle / track-click set the
playback context against the tree-selected album so the now-playing
source label remains accurate.

cc
2026-05-08 17:59:51 -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 053f7377c3 fix(local-library): default to first tab in user's order on mount
The view used to hardcode `activeTab = 'tracks'` on mount, which on
large libraries (16K+ tracks) causes a startup freeze regardless of
the user's preferred ordering. Now `activeTab` initializes to the
first visible tab in the user's configured order. Existing reactivity
that follows tab visibility changes (e.g. when the user hides their
current tab) is preserved.

cc
2026-05-08 17:33:15 -06:00
vicrodh 3c5522ec60 fix(local-library): real track counts on tree scan roots; nav-only tree
Top-level scan-root rows now fetch their recursive descendant count
via v2_library_count_folder_tracks instead of showing a hardcoded 0
(closing the Task 7 trade-off). Cache invalidates when the exclude-
network filter flips. The tree itself loses its per-row play button:
navigation-only, with recursive playback available from the right-
pane detail view.

cc
2026-05-08 17:30:34 -06:00
vicrodh 69420dcb0d feat(local-library): pass excludeNetworkFolders to tree primitives
All tree-mode invokes route through shouldExcludeNetworkFolders() so
the tree rail mirrors the flat list and recursive-play boundary.
Closes the visible/play semantic mismatch flagged by Task 10.

cc
2026-05-08 17:05:06 -06:00
vicrodh 632accbe40 fix(local-library): clear tree-mode state in bulk-action handlers
handleBulkPlayNext, handleBulkPlayLater, handleBulkAddToPlaylist now
call resetMultiSelect() so treeSelectMode and treeSelectedTracksById
are cleared alongside the existing flat-mode reset. Without this, the
BulkActionBar lingered with count 0 after firing from tree mode. The
tree-mode BulkActionBar's onClearSelection closure now delegates to
the same helper for consistency. resetMultiSelect was already
correct (it leaves trackPathById intact, which is what we want).

Also: warn when toggleTreeTrackSelection's cache-miss fallback hits a
track path with no parent folder, instead of silently returning.

cc
2026-05-08 16:37:15 -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 8420a5ea0b feat(local-library): tree-mode search with path-parents preserved
When the user types in the search input while in tree mode, matching
folders/tracks stay visible and every ancestor path expands to keep
context. Empty branches collapse out. Clearing the query restores the
user's pre-search expand state. Implementation: in-memory contains over
the loaded `albums` array, debounced 200ms. The match expands the union
of matched paths and their ancestors in `searchExpandedPaths`, which
replaces `treeExpandedPaths` while search is active. Pre-search snapshot
is restored on clear. Tracks remain rendered under any visible folder.

cc
2026-05-08 16:00:27 -06:00
vicrodh a7b40e6e8a chore(local-library): handlePlayRecursive error toast + network-filter parity
- Surface a toast on playback failure so the user gets UI feedback
  rather than only a console error.
- Honor shouldExcludeNetworkFolders() for consistency with loadTracks
  and the rest of the view's search invocations.

cc
2026-05-08 15:55:33 -06:00
vicrodh 03d817f32a feat(local-library): two-column tree mode in Folders tab
Wires LocalLibraryFolderTree (left rail, 240px fixed) and the
LocalLibraryFolderDetail / album-detail flow (right pane) into a
two-column shell that mirrors the Artists tab CSS byte-for-byte.
View-mode segmented control on the Folders toolbar toggles between
Flat (the existing folder-grouped list) and Tree (the new hierarchical
view). The toggle is hidden when browsing Hidden Albums so that
sub-view keeps its existing flat rendering.

Selection routes the right pane: folder paths matching a known
album_group_key drive the existing handleAlbumClick flow (which sets
selectedAlbum and renders the top-level album-detail view); non-album
folders open LocalLibraryFolderDetail with subfolder cards plus
direct-children tracks. Empty-state when nothing is selected uses the
library.foldersTree.selectAFolder string.

Recursive play: handlePlayRecursive fetches all tracks via
v2_library_search (limit:0), client-side prefix-filters on file_path,
sorts by path and queues the result. Backend does not yet expose a
recursive-tracks-under-path command — flagged as a Task 7 trade-off
to revisit if telemetry shows tree-mode play stalling on large
libraries.

Mode persists in library_preferences.folders_view_mode via the
existing v2_set_library_folders_view_mode command, hydrated on mount
through v2_get_library_preferences. Flat mode rendering is
byte-for-byte unchanged: zero deletions in the existing markup, the
new tree shell wraps it via an outer if/else.

LibraryPreferences type extended with the optional folders_view_mode
field so TS strict-mode passes on the new prefs.folders_view_mode
read.

cc
2026-05-08 15:44:00 -06:00
vicrodh b4a7015ae7 chore(local-library): guard FolderDetail $effect against stale fetches
The folderPath snapshot existed but wasn't used to guard the async
writes. Rapid folder switches could land an older fetch's results on
top of the newer selection. Add three short stale-guards on .then,
.catch, and .finally so only the latest in-flight result wins.

cc
2026-05-08 15:33:16 -06:00
vicrodh 6742597cdf feat(local-library): LocalLibraryFolderDetail right-pane component
Renders the contents of a folder that is NOT a known album_group_key —
shows immediate subfolders + direct-children tracks side by side, with
a "Play all recursively" button for the whole folder. Used in the right
column of the upcoming tree-mode two-column layout (Task 7). Extracts
FolderTreeEntry to a shared $lib/types/folderTree.ts module so both
LocalLibraryFolderTree and FolderDetail import the same type.

cc
2026-05-08 15:28:17 -06:00
vicrodh 0e503b75e7 chore(local-library): address Task 5 review nits
- Keep self-import for recursion and replace the misleading "deprecated
  in Svelte 5" comment with a corrected rationale. svelte-check confirms
  `<svelte:self>` is in fact deprecated in Svelte 5 (svelte_self_deprecated)
  and self-import is the recommended replacement; the original code was
  right, only the comment was wrong.
- Switch the row role from `button` to `treeitem` so that `aria-selected`
  is valid (svelte-check rejects aria-selected on role=button) and add
  `aria-expanded` for folders. treeitem is also the semantically correct
  role for a tree row, so screen readers now announce both selection and
  expansion state.
- Add :focus-within parity for the play button so keyboard users see it
  when the row receives focus.

cc
2026-05-08 15:24:11 -06:00
vicrodh ad5d13a0c3 feat(local-library): LocalLibraryFolderTree recursive component
Lazy-loaded recursive tree-row component for the upcoming tree mode in
the Folders tab. Renders one node per call; children loaded on first
expand via v2_library_list_folder_children. ADR-001 compliant (no $t
inside $derived). Indentation by depth * 16px matches Poweramp-style
hierarchy. Recursion uses self-import pattern (svelte:self deprecated
in Svelte 5). Not yet wired into LocalLibraryView - that lands with
the two-column layout integration (Task 7).

cc
2026-05-08 15:13:54 -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 72a81490f4 fix(mixtape-detail): stabilize virtualized list scroll-up behavior
Three issues observed when fast-scrolling collection detail views
(reported on the Macross collection, ~67 items, list mode):

1. Horizontal scroll artifact at the bottom-right corner — same root
   cause as AlbumDetailView's: an absolutely-positioned artwork backdrop
   widens past 100% of the .detail-view. Add overflow-x: hidden alongside
   the existing overflow-y: auto.

2. Empty container when scroll position briefly exceeds the content
   tail. The virtualWindow math could compute firstRow * cols >= total
   during overscroll moments, slicing windowedItems to an empty array.
   Clamp firstRow / firstIdx to total - 1 so at least one row renders
   even when the scroll position has overshot.

3. The user-visible regression that started this thread: scrolling
   UP from deep in the list could not reach scrollTop = 0 even though
   the "Top" button worked. Two fixes layered together:

   a. overflow-anchor: none on .detail-view. Browser scroll-anchoring
      tries to preserve visual stability when content above the viewport
      changes height — but the virtualizer manages its own spacer
      heights, so the auto-anchoring nudges scrollTop in the opposite
      direction of user input. Disabling it lets the wheel input win.

   b. content-visibility: auto gated to expanded mode. Until now
      .item-block applied it unconditionally. The grid view's .grid-card
      already had this exact issue diagnosed and fixed (see the inline
      comment "doubling up with content-visibility: auto caused cards to
      re-eject and re-paint on scroll-back"); .item-block had been left
      behind. Layering native content-visibility on top of manual
      virtualization caused the browser to intermittently treat rendered
      rows as off-screen during fast scroll, snapping them to the
      contain-intrinsic-size hint for a frame, which the spacer math
      doesn't predict — producing the empty-viewport-with-stuck-scroll
      symptom. The .lazy class now scopes content-visibility to expanded
      mode, where virtualization is OFF and the optimization is needed.

Smoothness during very fast wheel scroll is still imperfect but the
viewport no longer goes blank and scroll-up reaches the top reliably.

cc
2026-05-08 00:01:40 -06:00
vicrodh d452838cd1 fix(album-detail): clip horizontal overflow from artwork backdrop
The .has-art-bg ::before pseudo-element renders the album artwork
heavily blurred at transform: scale(1.15) for the Qobuz/Feishin-style
header. Without overflow-x: hidden on the .album-detail container, the
15% horizontal upscale of the absolutely-positioned backdrop bleeds
past the right edge and surfaces a horizontal scrollbar on the page.
Add overflow-x: hidden to the existing overflow-y: auto so the blur
stays clipped without affecting vertical scroll.

cc
2026-05-08 00:01:16 -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 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
Vudgekek 5cd76f53d4 Merge branch 'pre-release' into feature/external/macos-exclusive-mode 2026-05-07 15:34:44 -05: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 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