Commit Graph
2810 Commits
Author SHA1 Message Date
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
vicrodh 8977e721c6 refactor(album-detail): align tracklist columns and smooth select-mode toggle
- Reduce table-header left padding to 4px and switch the # column to
  text-align: left so track numbers line up with the bare quality badge
  in the toolbar above. Track rows in the album-detail context get the
  same 4px left padding via a :global override on .track-list and the
  track-number column switches to flex-start so the row stays aligned.
- Match the select-all checkbox width to the track-row checkbox (24px)
  and shrink the checkbox itself to 15x15 so the header checkbox lands
  on the same x as the per-row ones.
- Always render the select-all and track-row checkbox containers and
  animate width + opacity in/out (180ms ease) so toggling select mode
  no longer pops the column into existence.
- Move the AlbumMenu (...) trigger to the end of the action row so the
  bulk-action triggers (CassetteTape, multi-select) sit before the
  overflow menu — same convention as other detail views.

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

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

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

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

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

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

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

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

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

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

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

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

cc
2026-05-06 18:56:09 -06:00
vicrodh ea98a34f8e feat(library): give Albums tab the full Folders action bar (group/filter/sort/view/select)
Albums was a stripped-down VirtualizedAlbumList over metadataAlbums with
no controls. Folders had the full toolbar (group dropdown, quality/format
filter, sort, view toggle, multi-select with bulk actions, alpha index).
The user wants Albums to be 1:1 with Folders, no reinvention.

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

cc
2026-05-06 18:10:04 -06:00
vicrodh 9130bdc913 refactor(library): relocate tab order editor into folder management toolbar
The gear icon mounted next to the tab buttons inside .jump-links was
visually noisy and overloaded a UX area meant for navigation. Moved it
into the folder-actions toolbar of the Library Settings panel, alongside
the existing folder management icon-btns (FolderPlus, PenLine, Trash2),
where ordering controls sit naturally. Swapped the icon from Settings
(generic) to ListOrdered (semantically a list-ordering action).

Removed the orphaned .jump-link.tab-edit-btn CSS rule.

cc
2026-05-06 18:01:06 -06:00
vicrodh fa38c8a552 fix(library): trigger active tab loader after preferences settle on mount
The visible-tabs rework moved tab order behind LibraryPreferences, so the
initial activeTab can be any of folders/albums/artists/tracks. Mount only
called loadLibraryData (folders + stats), leaving Tracks/Artists/Albums
empty until the user manually clicked a different tab and back.

Extracted the lazy-load switch from handleTabChange into
ensureActiveTabDataLoaded() and call it from both handleTabChange and
onMount once preferences (and therefore activeTab) have settled. Folders
needs no branch because loadLibraryData populates it.

cc
2026-05-06 17:59:20 -06:00
vicrodh 863e5dc86f feat(local-library): add metadata-grouped Albums tab
Wires the new Albums tab in LocalLibraryView via the metadata-grouped
V2 commands. Default tab order in LibraryPreferences now includes
'albums' between 'folders' and 'artists'; existing users get the new
slot via sanitizeLibraryPreferences's backfill of missing default tabs.

LocalAlbum interface gains the optional source_folders field exposed by
v2_library_get_albums_metadata. fetchAlbumTracks branches on the album
record's source_folders presence: metadata rows fetch via
v2_library_get_album_tracks_metadata, folder rows keep using
v2_library_get_album_tracks. No separate viewMode plumbing is needed
because the discriminator travels on the row itself.

The Albums tab renders through the same VirtualizedAlbumList component
as Folders (single ungrouped group, no controls in this initial pass),
loaded lazily on first visit via loadMetadataAlbums. Cache invalidates
on every loadLibraryData, post-scan, and library-clear so a re-scan
surfaces fresh metadata grouping.

cc
2026-05-06 17:27:40 -06:00