Mounts the existing `GenreFilterButton` (with its own popup and store
subscription) into the toolbar's right-side slot. On filter change,
re-runs the discover-index fetch with the new genre IDs — the V2
endpoint already accepts `genreIds` and the response containers come
back filtered.
Release-watch and home-resolved are not re-fetched on genre change:
the former is personalized (followed-artists feed, genre-agnostic) and
the latter draws from local reco-store state that the user's local
play history doesn't get retroactively filtered by browse-time genre.
Matches HomeView.svelte's existing behavior (only the discover-index
sections re-filter).
Extracted the on-mount load into `loadAll()` so the genre-change handler
can call it directly without duplicating the parallel-fetch logic.
cc
The toolbar exposes three tabs (Home / Editor's Picks / For You) but V1
of Discovery only populates the Home tab. Clicks on the other two were
toggling `homeTab` but the scroll-area kept rendering the Home sections
— misleading UI.
This commit gates the section block: when `homeTab !== 'home'`, render
only the "under construction" placeholder. Editor Picks and For You tab
content will land in follow-up commits with their own data fetches
(`v2_get_discover_index` already brings what Editor Picks needs; For You
uses the dailyq/weeklyq/topq/favq recommendation set).
cc
Adds three new card components and five more sections, completing the
Home tab's section coverage at V1 minimum:
New cards:
- TrackCardLite: identical layout to AlbumCardLite, separate file so
future track-only metadata lands here without bleeding into album
cards.
- PlaylistCardLite: same 180px square, ListMusic placeholder when no
cover, single title line (no description, owner, track count).
- ArtistTileLite: circular 140px avatar + name. No play count.
data.ts additions:
- `fetchHomeResolved` — wraps `v2_reco_get_home_resolved` with explicit
per-section limit args (Rust signature uses limitRecentAlbums,
limitContinueTracks, limitTopArtists, limitFavorites). Maps the
backend-resolved minimal shapes (AlbumCardMeta, TrackDisplayMeta,
ArtistCardMeta) into Discovery card types. Single round-trip yields
four lists.
- `DiscoverIndexSections.playlists` — the discover-index response
already carries a playlists container; we now extract it instead of
making a second call to `v2_get_discover_playlists`.
DiscoveryView fires three parallel fetches via Promise.all (release-watch
+ discover-index + home-resolved) on mount and renders eleven conditional
sections in display order: recently played, continue listening, release
watch, new releases, most streamed, editor picks, qobuzissimes, press
awards, Qobuz playlists, top artists, favorite albums.
cc
Renames `fetchNewReleases` to `fetchReleaseWatch` to match what the V2
command actually returns (followed-artists release radar, not editorial
new releases). Adds `fetchDiscoverIndex` which calls `v2_get_discover_index`
once and splits the response's five containers into per-section card
arrays. One round-trip yields:
- newReleases (editorial)
- pressAwards
- mostStreamed
- qobuzissimes
- editorPicks (album_of_the_week)
DiscoveryView mounts release-watch and discover-index in parallel via
Promise.all, then renders six sections in display order (release-watch,
new releases, most streamed, editor picks, qobuzissimes, press awards).
Each section is conditional on having items, so empty containers don't
render an empty title row. The `discoverAlbumToCard` mapper drops every
field except albumId/title/artist/artistId/artwork — V1 doesn't render
quality, awards, parental_warning, etc., so we don't carry them around.
An inline `albumGrid` snippet keeps the per-section markup DRY without
introducing another wrapper component.
cc
Adds `src/lib/discovery-v2/data.ts` with `fetchNewReleases(limit)`, a pure
wrapper around `v2_get_release_watch`. The V2 command already returns full
`SearchResultsPage<Album>` payloads in a single round-trip — the original
HomeView did N+1 calls (release-watch followed by `v2_get_album` per id),
which Discovery sidesteps. The mapping reduces each QobuzAlbum to the
minimal shape AlbumCardLite needs (id, title, artist name + id, artwork
URL via getQobuzImageForSize 'small'), dropping fields V1 doesn't render
(quality, bitDepth, samplingRate, awards, parental_warning, etc).
Wires the section into DiscoveryView: on mount, fetch 8 new releases and
render via DiscoverySection + AlbumCardLite. Click on a card calls
onAlbumClick, the permanent play button calls onAlbumPlay, and the artist
text link calls onArtistClick — all forwarded from the props +page.svelte
already passes through. "Ver todo" routes to the existing release-watch
full grid view via onNavigateNewReleases.
cc
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
The bar declared `background: var(--bg-secondary); backdrop-filter: blur(20px)`.
`--bg-secondary` is opaque across all themes, so the blur was computed for
nothing — WebKit applied the per-frame blur computation to pixels under the
bar that the opaque bg then completely overdrew. Pure waste under software
compositing.
cc
The action was applying `will-change: transform` + `transform: translateZ(0)`
to every cached <img> as a workaround for WebKitGTK 2.50+ texture eviction
flicker on hover/scroll.
Under hardware-accelerated compositing this is cheap — each image is a GPU
texture handle. Under software compositing (QBZ's default), each forced
layer materializes its own CPU framebuffer. With 100+ AlbumCards in the
home view, that's 100+ extra buffers the compositor must manage every
frame, dominating the structural paint cost of scroll.
User confirmed in the 2026-05-11 perf session that the flicker the hack
was meant to prevent does not reappear after removal on his setup. If it
shows up again on other reports, the re-introduction should be guarded by
the hardware-acceleration setting (apply only when GPU is on).
cc
The `.artwork-bg` layer rendered the playlist cover scaled-up + heavily
blurred behind the contain-fitted main image. Under software compositing,
filter:blur(30px) saturate(1.5) on N playlist cards × visible row = paint
cost proportional to card surface, paid every frame the row repaints
during vertical scroll.
For square playlist art (the common case) `.artwork-main object-fit: cover`
covers the whole container — the blurred bg was invisible anyway. For
non-square art the original cover shows through at the letterbox edges
without the blur. Acceptable trade.
cc
Removed under software compositing the changes that proved either wasteful
or hot during scroll:
- `:hover` marquee animations on title/artist (infinite keyframes that kept
WebKit's paint loop alive while hovered, especially across cards passed
during scroll)
- `.action-overlay` `backdrop-filter: blur(12px)` (computed every frame even
at opacity 0; the bumped opaque background covers visually)
- `.editorial-ribbon` `backdrop-filter: blur(4px)` (multiplied by N cards
with badges; minor individually, significant in aggregate)
Added `content-visibility: auto` + `contain-intrinsic-size: 230px 320px` on
`.album-card`. Confirmed in the 2026-05-11 perf session to substantially
help horizontal scroll under software compositing by skipping offscreen
card render entirely.
cc
Wire isTrackRemovedFromQobuz into TrackRow consumers (VirtualizedTrackList,
AlbumDetail, DynamicSuggest, TopQ, WeeklySuggest). Tracks marked unstreamable
by the API or hit by a TrackUnavailable error this session now render dimmed,
non-draggable, non-clickable, with the i18n tooltip player.trackUnavailable.
Reactivity for the {@const} reads uses a $state counter bumped from the
unavailable-tracks store subscription so the dim state updates live without
forcing the whole list to re-render.
cc
Reverts merge f7623154 which built macOS support on top of the variant
rendering infrastructure introduced by titlebar-mode-selector. With the
variant system gone (previous revert), PR #406's changes (class:macos,
stripped strip with traffic-light reservation, macOS-aware drag bands)
reference dead architecture.
Most of PR #406 was already removed by the previous revert via file
checkouts; this commit cleans up the residual macOS-specific cosmetics
in Sidebar.svelte and +page.svelte (padding-top 36→32, drag-region
height 32→28, macos main-content padding restored to generic rule).
PR #407 (sticky home header fix, ac627d43) is preserved — independent
from titlebar work, only touches HomeView.svelte.
cc
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
Drains the wiki "23 warnings" baseline back to zero so future regressions
stand out:
* Drop 16 unprefixed \`app-region\` CSS lines across TitleBar, TitleBarNav,
ImmersiveHeader, ImmersivePlayer, MiniPlayerFooter and MiniPlayerWindowControls.
Tauri's webview is WebKit / Chromium on every platform we ship to, so
\`-webkit-app-region\` already covers the chrome-drag region; the
unprefixed property only existed to keep stylelint quiet but the CSS
spec doesn't define it and svelte-check warns on every site.
* Remove 2 empty rulesets in AlbumCreditsModal and NowPlayingBar (leftover
comment-only blocks from earlier refactors).
* Pair \`-moz-appearance: textfield\` with the standard \`appearance: textfield\`
in LocalLibraryTagEditorModal so the rule no longer trips the
property-companion warning.
* Delete the unused \`.quality-text\` selector in NowPlayingBar.backup.svelte
(file is a frozen snapshot of an older NPB iteration; the live NPB
doesn't use that class).
* Annotate Tooltip.svelte and immersive/panels/VinylPanel.svelte with
\`svelte-ignore a11y_no_static_element_interactions\` plus a comment
explaining the context — both are decorative wrappers (a tooltip hit
area; a 3D tilt effect on artwork) where adding an ARIA role would
misrepresent the semantics.
* Add \`@types/node\` as a devDependency (^22) so the tsconfig resolution
for the SvelteKit-extended config stops complaining about the missing
type definition.
cc
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
Three coupled visual bugs that surfaced once the queue advance and
gapless wiring started landing virtual track changes correctly.
Seekbar (case A: manual click / next button)
- PlayingTrack gets cueStartSecs / cueEndSecs fields and a
patchCurrentTrackCueInfo() setter on playerStore. handlePlaybackEvent
subtracts cueStartSecs from event.position when computing the
displayed currentTime, so a virtual track that lives at e.g. offset
7:26 inside its FLAC reads 0:00 at frame zero instead of 7:26 / 0:00.
- LocalLibraryView keeps an $effect on activeTrackId that looks up the
matching ephemeral track and patches the cue info into playerStore
every time the active track flips. Non-CUE / non-ephemeral tracks
fall through with null/null which clears any prior boundaries.
Seekbar (case B: CUE boundary auto-advance)
- handlePlaybackEvent gains an "underlying CUE" branch: when event.track_id
is in the ephemeral range AND currentTrack is in the ephemeral range
AND currentTrack has cueStartSecs, the strict track_id match check is
bypassed and the same offset translation runs. This keeps the seekbar
ticking in virtual-track time after the boundary watcher swaps the
displayed metadata while the audio buffer is unchanged. EPHEMERAL_ID_FLOOR
is promoted to a module constant so both branches share it.
Album art (manual next button)
- queueArtwork.resolveQueueTrackArtwork was double-converting Tauri
asset:// URLs through convertFileSrc, producing a 403 path
(asset://localhost/asset%3A%2F%2F...). Add an asset: passthrough
alongside http(s) and file:// so URLs already resolved upstream
(LocalLibraryView's queue setter pre-converts via getArtworkUrl)
reach the NowPlayingBar intact.
Quality badge ("audio downgrade")
- Boundary advance's setCurrentTrack call was passing LocalTrack.sample_rate
(Hz) into PlayingTrack.samplingRate (kHz). The badge formatted as
"FLAC 16/44100" instead of "FLAC 16/44.1", reading as a quality drop
vs. the album list. Divide by 1000 to match the rest of the
PlayingTrack contract.
cc
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
Two cross-cutting plumbing changes the ephemeral folder pane needs to
behave correctly without touching the rest of the playback paths.
playerStore: gapless transition detection used to require event.is_playing
to be true at the moment the track_id changes. The ephemeral file-data
swap (one buffer of bytes replacing another) briefly reports is_playing
false during the engine restart, and that transient frame arrives BEFORE
the player can flip the flag back. Without a carve-out the event was
mis-classified as an external track change, the queue's current_index
hadn't advanced (it's a separate state machine), and the UI froze on the
previous track. Streaming playback paths don't see this race because
their buffer transition keeps is_playing pinned. Carve-out is scoped to
event.track_id >= EPHEMERAL_ID_FLOOR so Qobuz/Plex/library are untouched.
routes/+page.svelte: session save now strips ephemeral tracks from the
queue snapshot and clears current_index if it pointed at one. Ephemeral
ids aren't stable across processes (the cache rebuilds from scratch on
each launch), and even if they were, the playback path 404s if session
restore fires before the folder rehydrates. The folder-path persistence
already brings the pane back; the queue can come back via re-clicking.
Also: gapless transition handler logs the advance/expected ids and the
match flag, plus runs a defensive syncQueueState on success — that's
belt-and-suspenders for paths where the queue's view of "current" lags
the player's gapless transition by a tick (e.g. ephemeral, where the
queue advance and the player swap are independent state machines).
cc
The original design used negative i64 ids to distinguish ephemeral
tracks from DB rows, but the queue and playback-context commands
serialize ids as Vec<u64> end-to-end (V2QueueTrack, v2_set_playback_context).
serde rejects negative numbers at the boundary, so setQueueForLocalTracks
and setPlaybackContext failed the moment the user clicked an ephemeral
track:
invalid value: integer `-1`, expected u64
Switch synthetic ids to a high-positive range (EPHEMERAL_ID_FLOOR =
1<<48) instead. Pros over the negative scheme:
* Valid u64 — passes through the queue/context commands untouched
* Below 2^53 so JS Number round-trips without precision loss
* Far above any realistic DB autoincrement id, so the discrimination
test (`track_id >= EPHEMERAL_ID_FLOOR`) stays unambiguous
Also fixes the snake_case mismatch on EphemeralFolderState — the Rust
struct serializes with its native field names (folder_path,
skipped_files), but the TypeScript type was declared in camelCase, so
ephemeralFolder.folderPath was undefined at render time.
cc
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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