Commit Graph

457 Commits

Author SHA1 Message Date
Frank Stellmacher 874b0c67ae fix(context-menu): drop inline z-index that hid menus under floating player (#522)
* fix(context-menu): drop inline z-index that hid menus under floating player

The main context menu wrapper carried an inline `zIndex: 999` that
overrode the `.context-menu { z-index: 10000 }` stylesheet rule. The
floating player bar sits at z-index 1000, so when a menu opened near
the bottom of the screen — long enough or anchored low — the player
bar covered it.

Removing the inline override lets the stylesheet rule (10000) win, so
the menu always paints on top of the floating bar. Submenus inherit
the same stacking context so they follow the parent menu.

Closes #521.

* docs(changelog): add context-menu floating-player z-index fix entry
2026-05-09 18:52:37 +02:00
Frank Stellmacher fec513b629 fix(home): swap Because-you-listened rail to AlbumRow under 696 px (#520)
* fix(home): swap Because-you-listened rail to AlbumRow under 696 px

The hero-style BecauseCards are tuned for full-rail widths (3 cards at
1052 px+, 2 cards at 696-1051 px). Below that the cards stretched
full-width with a fixed 160 px cover stuck on the left and centred text
floating in a wide empty area — looked like three over-sized banners
stacked vertically instead of a compact recommendation rail.

A `ResizeObserver` on the rail wrapper now watches the container width
and below 696 px renders a standard `AlbumRow` (which is already
perf-tuned for narrow rails: artwork budget, viewport windowing, scroll
paging). Wide layouts keep the unchanged hero card layout, so the
mainstage view at full width is identical to before.

* docs(changelog): add Because-you-listened narrow-layout fix entry
2026-05-09 18:11:22 +02:00
Frank Stellmacher 6f50fb6a19 feat(player-bar): album context menu on song title right-click (#512)
* feat(player-bar): album context menu on song title right-click

Right-clicking the track title in the player bar now opens the same
album context menu that album cards use (open, play next, enqueue,
go to artist, favorite, rate, share, download, add to playlist).

Mirrors the existing left-click behavior on the title, which already
navigates to the album. Suppressed for radio and preview, matching
the click handler.

MarqueeText gains an optional onContextMenu prop; PlayerBar builds a
SubsonicAlbum shape from currentTrack on demand.

* docs(changelog): add entry for PR #512 (player-bar title context menu)
2026-05-07 23:33:25 +02:00
Frank Stellmacher 57fe847d71 refactor(settings): collapse all sections, drop font dropdown, surface OpenDyslexic (#508)
* refactor(settings): collapse all sections, drop font dropdown, surface OpenDyslexic

Settings opened on a tab where four or five sub-sections were expanded
on first render — audio device, theme list, lyrics sources, sidebar
customizer, random-mix copy, offline dir, language picker, keybindings
table. The page felt like a wall of controls before the user had even
looked for something specific. Removed every `defaultOpen` flag from
the SettingsSubSection call sites so each tab now boots with only the
section headers visible. Component default was already `false`.

ThemePicker auto-expanded the group containing the active theme on
mount. Same noise on a screen that already has the longest accordion
list in the app, and the blue dot in the group header already tells
the user which group holds the active theme. Initial open-group is
now `null` — all groups collapsed until the user clicks one.

Font picker had a dropdown-style button that toggled a list inside
the sub-section, which meant two clicks (open the section, then open
the dropdown) for what should be a one-click choice. Removed the
button + the `fontPickerOpen` state — opening the Font sub-section
now reveals the full list directly and a click sets the font without
collapsing anything. OpenDyslexic moved to the top of the list so
users with dyslexia don't scroll past 14 sans-serifs to find their
option; the rest stays in the original order.

* docs: changelog entry for PR #508

Logs the Settings collapse-by-default + font picker cleanup +
OpenDyslexic ordering in v1.46.0 "## Changed".
2026-05-07 22:31:50 +02:00
Frank Stellmacher bd56177e2c feat(home): Lossless Albums rail + dedicated page + sidebar nav (#506)
* wip(home): Lossless rail + dedicated /lossless-albums page

Rail under Home > mostPlayed and a dedicated infinite-scroll page that
list albums whose tracks are tagged in lossless containers. Walks
Navidrome's native /api/song?_sort=bit_depth&_order=DESC, dedupes by
albumId on the way down, stops when the song stream crosses into lossy
(bitDepth==0) or the server runs out of rows. _filters has no operators
on quality columns, so a sort + walk is the only path; equality on
sample_rate / bit_depth probes returned empty (verified).

The page paginates through the song-cursor with an in-flight ref so
overlapping IntersectionObserver fires don't double-add albums, plus a
cancelled flag so React StrictMode's double-mount doesn't apply two
parallel result sets in dev.

Suffix allowlist excludes ambiguous wrappers — m4a/m4b can be ALAC
*or* AAC and Navidrome's response carries an empty codec field, so we
can't tell them apart; same story for wma. Allowlist is flac, wav,
aiff/aif, dsf/dff, ape, wv, shn, tta — containers that are only
lossless. ALAC-in-m4a setups will miss out, acceptable trade-off
without a reliable codec field.

Status: WIP. Settings home-customizer label and en.ts strings landed,
no other locales yet, no quality badge on AlbumCards across the rest of
the app, no CHANGELOG. Branch was 'feat/home-hires-rail' during the
earlier hi-res-only iteration before the lossless broadening.

* wip(home): Lossless page header parity + streaming + sidebar nav

Page header now mirrors All Albums: selection mode with three action
buttons (Enqueue Selected, Add Offline, Download ZIPs) wired to the
same handlers Albums.tsx uses, selection-counter title swap, and the
perfFlags.disableMainstageStickyHeader respect path. Filters were
intentionally skipped — the rail is sorted by bit_depth, mixing client-
side filters with server-driven pagination would produce gaps.

Loading feels noticeably faster: ndListLosslessAlbumsPage takes an
optional onProgress callback that fires once per internal fetch with
the entries discovered in that fetch, so the page can stream new
albums into state instead of waiting on the whole loadMore. Page-side
budget dropped from 5×200 to 2×100 songs per loadMore (~1 MB worst
case vs 5 MB before), since the rail's catch-em-all pass is wrong for
infinite-scroll UX. Subtitle under the page title primes the user that
this is slower than other album pages because Psysonic walks the song
catalog by quality (Navidrome ignores _fields, so per-song responses
ship with lyrics + tags + participants whether we want them or not).

Sidebar nav entry registered under 'losslessAlbums' with a Gem icon,
defaults to visible:false (matches composers / folderBrowser /
deviceSync — niche browsing modes). Existing users get the entry
appended at the end of their persisted sidebar list automatically via
the onRehydrateStorage merge that sidebarStore already runs for new
DEFAULT_SIDEBAR_ITEMS.

i18n: full coverage across all 8 locales for sidebar.losslessAlbums,
home.losslessAlbums, losslessAlbums.empty, losslessAlbums.unsupported
and the new losslessAlbums.slowFetchHint subtitle. ru/zh are
machine-translation quality, flagged for a polish pass.

* feat(home): default Lossless sidebar entry to visible

Flips the DEFAULT_SIDEBAR_ITEMS entry for `losslessAlbums` from false
to true. Existing installs keep whatever the user has in persisted
storage; fresh installs see the entry in the sidebar from the start.

Earlier wip commit defaulted it off (matched composers / folderBrowser
/ deviceSync as a niche browse mode), but the rail + page do show
something useful for any library with at least one FLAC/WAV/etc. album,
so off-by-default just hid the feature.

* docs: changelog entry for PR #506

Logs the Lossless Albums rail + page + sidebar entry in v1.46.0
"## Added".

* docs(settings): contributor entry for PR #506

Adds the Lossless Albums rail/page bullet to Psychotoxical's
contributions list.
2026-05-07 20:44:27 +02:00
Frank Stellmacher a41e3a624a feat(song-info): show absolute file path on Navidrome via native API (#504)
* feat(song-info): show absolute file path on Navidrome via native API

Subsonic's `getSong.view` returns at most a relative path (or none on
Navidrome), so the Path row in the Song Info modal stayed empty for
most users. Feishin and the Navidrome web client surface the full
server-side path by hitting Navidrome's native `/api/song/{id}` instead.

Added an `nd_get_song_path` Tauri command that logs in to the native
API with the active server's credentials, fetches the song, and returns
the `path` string. Wired it into `SongInfoModal`: the Subsonic
`getSong` call still drives the rest of the dialog, and the native call
runs in parallel only when the active server's identity is
"navidrome". When it returns a path, that absolute path replaces the
relative Subsonic value; native-API failures are silent and the modal
falls back to whatever Subsonic provided.

No token cache yet — the modal is opened occasionally enough that one
fresh login per call is fine.

Closes discussion #479.

* docs: changelog entry for PR #504

Logs the Navidrome native-API absolute file path support for the
Song Info dialog in v1.46.0 "## Added".

* docs: settings contributor entry for PR #504, drop competitor mention

Adds the song-info absolute path bullet to Psychotoxical's contributors
list in Settings, and rewords the existing CHANGELOG entry so neither
text references competing clients.
2026-05-07 20:22:55 +02:00
Frank Stellmacher 726f3f0ff2 fix(radio): queue navigation, dedup, and similar-first variety (#500) (#503)
* fix(radio): queue navigation, dedup, and similar-first variety (#500)

After a Radio session ran a while, three things broke:

Queue navigation through duplicates. playTrack re-resolved the active
queue index by `findIndex(t.id === track.id)`, returning the *first*
matching id, so reaching the second occurrence of a track snapped
queueIndex back to the earlier slot — highlight jumped and the next
advance played the wrong follow-up. Added an optional
`targetQueueIndex` to playTrack, threaded through next(), previous(),
the audio:ended repeat-one path, queue-row click, and the queue-item
context menu. findIndex stays as the fallback for callers that just
have a track and a fresh queue.

Queue accumulation. enqueueRadio didn't dedupe incoming tracks; the
next() top-up deduped against the live queue but trimmed the played
tail down to HISTORY_KEEP=5, so a song heard 8 ago was gone from
`existingIds` and a later Last.fm/topSongs response could re-add it;
and the `.filter(...)` pass admitted intra-batch repeats (top +
similar overlap is common) because it read the dedup set before
mutating it. A module-level radioSessionSeenIds set, fed by
enqueueRadio and both top-up paths and reset on artist change and
clearQueue, closes all three: trimmed ids stay in the set, ids about
to be replaced (fresh enqueueRadio wiping the pending radio block)
are removed first so callers can re-introduce them, and the dedup
pass mutates the set inline.

Variety. Starting Radio on a track stacked five top tracks of the
seed artist before any similar-artist material played. Switched the
seed path and both top-up paths to lead with similar songs (other
artists) and only fall back to top tracks when similar comes back
empty — preserves the "no Last.fm" graceful degradation but stops
the seed artist from monopolising the front of the queue.

Not affected: gapless audio:track_switched (already index-based, no
findIndex), AudioMuse Instant Mix / Lucky Mix (single-element queues
or enqueue-only paths), the artist-radio path (no seedTrack — already
picks just one top track and fills the rest from similar).

Reported by netherguy4.

* docs: changelog entry for PR #503

Logs the radio queue navigation/dedup/similar-first fix in v1.46.0
"## Fixed".
2026-05-07 19:56:57 +02:00
Frank Stellmacher ddb1f29af9 refactor(settings): remove redundant Animations 3-state setting (#495)
* refactor(settings): remove redundant Animations 3-state setting under Seekbar Style

The `animationMode` setting (Full / Reduced / Static) duplicated work
the perf-flag system and OS-level reduced-motion preference already
covered:

- `perfFlags.disableMarqueeScroll` already kills marquee scrolling on
  demand, replacing what `static` mode used to gate.
- The `data-perf-disable-animations` html-level switch already strips
  every `*` animation, replacing what `static` mode used to do globally.
- `@media (prefers-reduced-motion: reduce)` honours the OS setting for
  every user that asked for it via system preferences.
- The 30 fps cap that `reduced` mode applied to the seekbar wave was
  better served by per-feature perf toggles cucadmuh added later.

Removed:
- `AnimationMode` type, `animationMode` field + setter from auth store.
- Settings UI block (3 buttons + hint text) under Appearance > Seekbar
  Style.
- `animationMode === 'static'` short-circuit in WaveformSeek's rAF
  effect; `isReduced` skip-every-other-frame logic; `static`-checks in
  `drawNow` / `needsDirectDraw`.
- `animationMode !== 'static'` guard and `data-anim-mode` attribute in
  MarqueeText.
- `[data-anim-mode="static"]` and `[data-anim-mode="reduced"]` rules in
  layout.css.
- Seven i18n keys (animationMode + 6 variants) across all eight
  locales.

Migration: the persist layer strips `animationMode` (and the legacy
`reducedAnimations` boolean predecessor) so anyone who had `'reduced'`
or `'static'` selected silently lands on the former `'full'` path on
first launch after upgrade. No user-facing prompt — the missing setting
just stops existing.

cucadmuh's PR #472 (FPS overlay), #476 (preview-freeze main seekbar,
sleep-recovery hooks, card-hover removal) and #486 (interpolation
anchor reset on resume) are all preserved untouched — they live in
separate effects / files and were not driven by `animationMode`.

* docs(changelog): add Removed section for animationMode setting (PR #495)

* docs(changelog): refine animationMode removal rationale (drop prefers-reduced-motion overstatement)
2026-05-07 12:53:19 +02:00
Frank Stellmacher ba73649360 fix(home): more variety + reliable render in Because-you-listened rail (#494)
Two reports rolled into one pass:

1. Rail occasionally rendered nothing on Home open. Cause: the rotated
   anchor sometimes had no Last.fm similar-artists or no library matches
   among the sampled set, and the old code gave up after one try and
   stored that dud anchor as the rotation cursor — so the next mount
   started from the same dud's neighbour and could fail again.

2. Recommendations felt repetitive. Cause: pool of 12 anchors walked
   round-robin made each anchor recur every 12 mounts, similar-artist
   sample of 6 from 12 had heavy overlap visit-to-visit, and per-artist
   single-album random pick meant artists with one library album always
   surfaced the same record.

Anchor selection: random pick from pool with a per-server cooldown
buffer (last 5 anchors excluded, capped at floor(pool/2) so small
libraries don't soft-lock). Up to 4 anchors are tried in a shuffled
candidates list before giving up; the localStorage cursor only advances
on a successful anchor so duds don't poison future mounts.

Picks variety: similar-artist fetch raised from 12 to 25 (same
getArtistInfo call, larger response — Last.fm typically returns up to
~50). Per-server ring buffer of the last 30 shown album ids; per-similar
-artist album choice prefers an album not in that buffer, falling back
to any album when the artist's whole catalogue is stale so the slot is
never lost.

Pool cap raised 12 -> 20 to give the cooldown buffer room to breathe in
libraries with varied listening history.

Storage: legacy `psysonic_because_anchor:` single-id keys from the
round-robin era are stripped on module load (one-shot localStorage
sweep, the new `..._anchor_history:` prefix has a different colon
position so no false matches).

API budget unchanged in the hot path: 1 getArtistInfo + 6 getArtist
per Home mount. Worst case (3 dud anchors, 4th succeeds) is 4
getArtistInfo + 6 getArtist.
2026-05-07 10:40:12 +02:00
Frank Stellmacher d75670ec4b feat(home): broaden Because-you-like seed pool + tidy orphan card at 1080p (#493)
* feat(home): mix recently-played + starred into Because-you-like anchor pool

Anchor pool was sourced only from getAlbumList(frequent), so the rotation
cursor walked the same eight top-played artists no matter how varied the
rest of the listening history was. Round-robin merge of mostPlayed,
recentlyPlayed and starred (dedup by artistId) means each mount can land
on a different listening *mode* — heavy rotation, current focus, or
explicit favorites — instead of stepping through the same top-played
sequence.

Pool size 8 -> 12 to let the cursor visit all three modes before
wrapping. Visibility guard widened so the rail still renders when the
server has no frequent-play data yet but starred or recent items exist.
Zero new API calls — all three lists are already in Home's initial
fetch.

* fix(home): drop orphan 3rd Because-card in 2-col range, keep all 3 stacked on mobile

auto-fit grid wraps to 2 cols between 696-1051px container width, which
left the third card alone on a second row at 1080p. Container query
hides the 3rd card only inside that 2-col band; on wider screens the
full 3-up row stays, on narrow viewports (single column) all three
cards stack vertically as expected.

* docs(changelog): Because-you-listened seed pool + 1080p layout polish (PR #493)

* docs(changelog): fold PR #493 refinements into the existing Because-you-listened entry

Drop the separate Changed section entry — the feature is in the same
1.46.0 release window as PR #489, so readers want a single description
of the final behaviour, not "added X, then changed X" for the same
release. PR reference becomes "PRs #489, #493".
2026-05-07 09:12:47 +02:00
Frank Stellmacher b01e76df9c fix(home): Because-cards — readable layout at 1080p / 3-up (#492)
At 1080p with three cards per row (~370 px wide) the previous layout
broke down:

- 200×200 cover left only ~150 px of text width after gap + padding,
  so titles truncated mid-word and the meta-pill wrapped vertically
  into a stack instead of staying on one line.
- The "·" separators in the meta vanished as soon as the pill wrapped,
  because the ::after lives inside the wrapping flex line.
- Albums without cover-art rendered the placeholder as an empty grey
  rect — visually broken next to neighbours that did have art.

Adjustments:

- Cover wrap 200 → 160 px. Buys 40 px of text width per card and brings
  cover/text proportions into balance.
- Meta-pill: inline-flex with width: max-content + max-width: 100% so
  the pill is content-fit when the meta line fits, capped at the
  available text width otherwise. Font 12 → 10 px, column-gap and
  padding tightened, flex-wrap kept (wraps to a second row if a server
  ever returns a really long meta string instead of clipping).
- Cover-art placeholder shows a centred Lucide Music icon at 30 %
  --text-primary alpha — same visual weight as a faded thumbnail
  instead of an empty rect.
2026-05-07 02:58:01 +02:00
Kveld. f82f1be63a feat: redesigned community themes (#490)
* redesigned community themes

* fixed select arrow obsidian-black & violet-haze

* docs: CHANGELOG + Contributors entry for community themes redesign (PR #490)

---------

Co-authored-by: Psychotoxical <171614930+Psychotoxical@users.noreply.github.com>
2026-05-07 02:43:14 +02:00
Frank Stellmacher d1ff2fab51 feat(home): Because you listened recommendation rail (#489)
* feat(home): "Because you listened" recommendation rail

New Home rail (under Recently Added, default on, toggleable in Settings →
Personalisation → Home Page) that surfaces 3 albums from artists similar
to one of your top-played artists. Anchor rotates per Home mount so a
different top-artist seeds the recommendations each visit; within each
anchor, both the similar-artist subset and the chosen album per artist
are randomised, so the same anchor returns different picks on subsequent
visits.

Card layout matches the regular Album cards' surface (--bg-card with
accent-tinted border + 1px inset top highlight) and gets the same
Play / Enqueue hover overlay buttons. Cover and meta scale via CSS only
— no infinite animations, no filter/blur/transform, no compositing
layers. Grid wraps below 3-up at <400px card width instead of shrinking.

API budget: one getArtistInfo2 + 6 parallel getArtist calls per Home
mount, both reusing the existing mostPlayed payload to derive the anchor
pool (no extra API call to find top artists). All 8 locales seeded.

* fix(home): ru plurals + per-server anchor + narrower card grid

- ru: add _few / _many for becauseYouLikeTracks (CLDR Russian needs
  4 forms — 3 треков was wrong, now 3 трека).
- Anchor rotation memory is now per-server. The localStorage key
  becomes psysonic_because_anchor:<serverId>; switching servers no
  longer aliases server A's rotation onto server B's pool.
- because-card grid minmax(400px, 1fr) -> minmax(340px, 1fr) so two
  cards fit side by side at typical sidebar-expanded widths instead
  of collapsing to a single card per row.

* docs: CHANGELOG + Contributors entry for Because-you-listened rail (PR #489)

* style(home): blurred cover backdrop + centred layout for Because-cards

- Each Because-card renders the album cover as a blurred, low-opacity
  full-bleed background layer behind the existing cover thumb and text.
  Resolved through useCachedUrl so the cache layer feeds it (same key
  as the thumbnail) instead of a fresh salted URL on every render.
- Card content (cover thumb + text block) now centred horizontally and
  vertically within the card; the meta line lives in a small pill that
  sits centred under the artist row.
- Text contrast halo and meta-pill background are theme-aware via
  color-mix on var(--bg-card) / var(--text-primary), so the same rules
  read on dark and light themes (was hard-coded rgba black before and
  smudged the type on Latte / Nord Snowstorm).
2026-05-07 02:28:58 +02:00
Frank Stellmacher 5b37ab70f1 perf(tracklist): drop now-playing pulse + EQ-bar animations (#488)
* perf(tracklist): replace animated EQ-bar + active-row pulse with static icon

The currently-playing track in any tracklist (AlbumDetail, ArtistDetail,
PlaylistDetail, Favorites, RandomMix) was rendered with two animations
on top of an already-busy DOM:

- `.track-row.active` ran `track-pulse` 3s opacity 1 → 0.6 → 1 infinite
  on the entire row subtree (title button, several Lucide icons, star
  rating SVGs, hover affordances). Opacity is a compositor property, but
  on WebKitGTK without compositing — Linux + NVIDIA proprietary +
  WEBKIT_DISABLE_COMPOSITING_MODE=1 — every animated row falls back to a
  full software repaint of the subtree per frame.
- The "now playing" indicator in the track-number cell was three
  `<span class="eq-bar">` siblings with `transform: scaleY()` keyframes
  (`eq-bounce`), each on its own delay/duration. Same composite story:
  three software-repainted layers per frame, every frame.

On AlbumDetail (long tracklist + cover-art header background + the
already-running WaveformSeek progress rAF in the player bar) the
combined cost held the WebProcess at ~80 % CPU with 1.2 GB RSS for the
entire duration of playback. CPU dropped immediately on pause/stop;
on Composers / Settings (no track rows) the symptom never appeared.
Profiler confirmed continuous Layout & Rendering work synchronised with
isPlaying, not the canvas itself.

Replace both animations with static visuals:

- `.track-row.active` keeps the `--accent-dim` background, drops the
  pulse animation entirely.
- The "now playing" indicator becomes a single Lucide `AudioLines` icon
  (four vertical bars of different heights — reads as an EQ icon, no
  animation, one SVG per active row instead of three animated spans).
  `.eq-bars` className now only sets the accent colour.

Cleanup: dead `@keyframes track-pulse`, `@keyframes eq-bounce`,
`.eq-bars.paused` rule, plus a duplicate `.eq-bar` block in theme.css
(with a `wave` keyframe that was being shadowed by components.css and
had no other consumers).

No behaviour change beyond removing the animation; the row is still
visibly the active one and the icon still marks the playing track.

* docs: CHANGELOG entry for tracklist animation perf fix (PR #488)
2026-05-07 01:57:14 +02:00
Frank Stellmacher 59744601d4 feat(composer): Browse by Composer page (issue #465) (#487)
* feat(composer): Browse by Composer page (issue #465)

New library section listing every artist credited as composer on at
least one track, with a detail page showing all works they're credited
on in that role. Targeted at classical-music libraries where the
"recording artist" tag carries the orchestra and the "composer" tag
carries Bach / Mozart / Chopin.

Hits Navidrome's native /api/artist?_filters={"role":"composer"} for
the listing and /api/album?_filters={"role_composer_id":"…"} for the
works grid — Subsonic getArtist only follows AlbumArtist relations and
returns 0 albums for composer-only credits, so the native API is the
only path that works. Requires Navidrome 0.55+ (uses
library_artist.stats role aggregation); on older / pure-Subsonic
servers the page shows a one-line capability banner.

- Two new Tauri commands: nd_list_artists_by_role +
  nd_list_albums_by_artist_role, generic over participant role so
  conductor / lyricist / arranger pages are trivial to add later.
- Composers grid: text-only compact tiles (name + participation count
  pulled from stats[role].albumCount). No avatars — composer libraries
  carry no useful imagery and the listing endpoint exposes no image
  URLs anyway.
- ComposerDetail: hero with Last.fm bio (via getArtistInfo2) plus the
  full work grid, with a graceful fallback when the artist has no
  external info synced.
- Sidebar entry default off (Feather icon) — opt-in for the niche
  classical use case.
- nd_retry backoffs widened from [500] to [300, 800, 1800] — helps
  every nd_* call survive intermittent TLS-handshake-EOF errors that
  some reverse-proxy setups produce when keep-alive pools churn.
- Distinguishes "server can't do this" (HTTP 400/404/422/501) from
  transient errors so the capability banner only fires when the server
  actually rejects the request shape; everything else gets a retry
  button.
- i18n in all 8 supported locales.

* fix(composer): address review feedback on detail page + role queries

- Re-fetch ComposerDetail when music-library scope changes; previously
  the album grid stayed stale until navigation while the list refreshed.
- Thread library_id through nd_list_artists_by_role and
  nd_list_albums_by_artist_role so role queries respect the active
  Navidrome library, matching the Subsonic musicFolderId already piped
  through libraryFilterParams().
- Fix CachedImage cache-key mismatch on ComposerDetail: a Last.fm header
  image was stored under the Subsonic cover-art key, aliasing cache
  entries and risking cross-source pollution.
- Consolidate the two contradictory composer-imagery comments in
  Composers.tsx into a single accurate one (the older one referenced an
  Images toggle that was never implemented).
- Align openLink toast duration with ArtistDetail (1500ms -> 2500ms).

* fix(composer): keep bio across scope changes, add share, degrade gracefully

Three remaining items from the latest review pass on the composer flow.

1. Bio survives a music-library scope change.
   The previous fix added musicLibraryFilterVersion to the load effect,
   but that effect also did setInfo(null) while the getArtistInfo effect
   still depended on [id] alone — so a scope bump on the open page
   wiped the bio without re-fetching it. Move the info reset into the
   bio effect (keyed on id) and out of the load effect: the album grid
   still refreshes on scope change; the Last.fm header image and
   biography survive untouched, since both are library-independent.

2. Composers join the share pipeline as a first-class entity kind.
   Extend EntityShareKind with 'composer' (and isEntityKind), branch
   applySharePastePayload to validate via getArtist (same id pool) and
   navigate to /composer/:id, and wire a Share button into
   ComposerDetail. A pasted composer link now opens the composer view
   instead of the artist view, matching what was copied. i18n added in
   all 8 locales (sharePaste.composerUnavailable, openedComposer;
   composerDetail.shareComposer, unknownComposer).

3. Partial server failure no longer hides the works.
   If getArtist rejects but ndListAlbumsByArtistRole succeeds, the page
   used to show full "not found" despite having data to display. Switch
   the not-found gate to require both empty (`!artist && !albums`) and
   render a degraded header (placeholder name, no Wikipedia / favourite
   / share / Last.fm image) when only metadata is missing.

* fix(composer): right-click share copies a composer link, not an artist link

The context menu opened from a composer card / row uses type='artist'
because every composer-action (radio, favourite, rating, add-to-playlist)
is identical to the artist counterpart — they share an id space and a
backend representation. Sharing was the one exception: the "Share Link"
entry produced a 'psysonic2-' string with k='artist', so a paste opened
/artist/:id even though the user came from /composers.

Add an optional shareKindOverride to openContextMenu (default: undefined,
preserves existing behaviour) and have the artist-typed branch consult
it when calling copyShareLink. Composers.tsx now passes 'composer' on
both right-click sites; nothing else changes downstream because the
override only affects the share kind.

* polish(composer): show Last.fm avatar even without server metadata

Two minor follow-ups from the latest review.

- ComposerDetail: drop the `&& artist` guard on the header-avatar render
  path. info?.largeImageUrl can resolve through getArtistInfo(id) without
  ever needing the SubsonicArtist record, so the previous gate hid a
  perfectly good Last.fm portrait whenever getArtist failed but the
  bio fetch succeeded. Replace artist.name with displayName so the
  alt / aria-label degrade to the localised "Composer" placeholder
  instead of empty strings.
- copyEntityShareLink: doc comment now mentions composer alongside
  track / album / artist.

* fix(composer): derive Last.fm cache key from route id, not from artist record

Follow-up to the previous polish: the avatar render path no longer
requires `artist` to be populated, but the cache-key gate still did. So
when getArtist failed but getArtistInfo returned a Last.fm portrait, the
key fell through to coverKey — which is empty without an artist record,
re-creating the very aliasing bug the earlier Subsonic-vs-Last.fm fix
was meant to close.

Switch the Last.fm branch to the route id (same id namespace as the
SubsonicArtist record), so the key stays stable whenever Last.fm art is
shown, independent of getArtist succeeding.

* docs: CHANGELOG + Contributors entry for composer browsing (PR #487)
2026-05-07 00:36:09 +02:00
cucadmuh c83447ebd2 fix(player): reset WaveformSeek interpolation anchor on resume (#486)
The interpolation effect unmounts while paused, so progressAnchorRef.atMs was
never refreshed. The first tick after play added the entire pause duration to
elapsedSec and overshot the playhead until the next transport heartbeat.
Re-anchor from getPlaybackProgressSnapshot() when the effect starts.
2026-05-06 21:48:06 +00:00
Frank Stellmacher 43d75e744b feat(selection): Shift+Click range selection on grid pages (#484)
* feat(selection): add useRangeSelection hook with Shift+Click range support

Reusable hook for multi-select state across pages that show grids of
items the user can pick. Tracks the selected ID set, a click anchor,
and exposes a `toggleSelect(id, { shiftKey })` callback:

- Plain click → toggles that item and moves the anchor to it.
- Shift-click on a second item → adds every item between the anchor
  and the click target (inclusive) to the selection. The anchor moves
  to the shift-clicked item so the next shift-click extends from
  there.

Range expansion follows the items array passed to the hook, so the
caller controls the user-visible order (filtered + sorted list, not
the raw upstream array).

Implementation note: the anchor ref is snapshotted *before* the state
updater runs and written *after* it. React 18 strict mode invokes
state updater functions twice in dev to surface side effects, so any
ref mutation inside the updater would taint the second invocation and
the replay would miss the range branch.

* feat(selection): adopt Shift+Click range selection on grid pages

Wires `useRangeSelection` into the four pages that ship a multi-select
mode on top of card grids:

- Albums (passes the filtered/sorted `visibleAlbums` so range follows
  the order the user actually sees)
- RandomAlbums
- NewReleases
- Playlists

`AlbumCard.onToggleSelect` is extended to forward `{ shiftKey }`, and
the card's onClick handler reads `e.shiftKey` from the React event
and threads it through. The Playlists grid uses an inline onClick on
the card div and was updated the same way.

User-visible behaviour: in selection mode, click an item then
shift-click a later item — every item between them gets selected.
Existing single-toggle behaviour is unchanged when no shift key is
held.

* docs: changelog entry for PR #484

Logs the Shift+Click range selection on grid pages under
v1.46.0 "## Changed".
2026-05-06 17:51:25 +02:00
Frank Stellmacher ebce53f8a7 fix(sidebar): centre Playlists icon and unify hover hitbox in collapsed mode (#481)
* fix(sidebar): centre Playlists icon and unify hover hitbox in collapsed mode

The Playlists nav entry had its own special render path with a wrapper
div, header-row and `flex: 1` main link to fit the expand-toggle
button. Those elements remained active in collapsed mode too, where
`padding-right` on the header-row and `flex: 1` on the link made the
icon sit off-centre and gave the row a wider hover hitbox than every
other collapsed sidebar item.

Hoist the `isCollapsed` check above the playlists special-case so that
in collapsed mode Playlists renders through the same plain `<NavLink>`
branch as Artists / Albums / Favorites / etc. The expanded-mode
treatment (wrapper, header-row, expand-toggle, nested playlist list)
is unchanged.

* docs: changelog entry for PR #481

Logs the collapsed-sidebar Playlists icon centring fix in v1.46.0
"## Fixed".
2026-05-06 16:12:50 +02:00
Frank Stellmacher 8692e50603 feat(settings): Open Source Licenses section in System tab (#477)
* feat(licenses): tooling and initial data generation

Adds the maintainer-only generator that produces src/data/licenses.json:
- src-tauri/about.toml + about.hbs: cargo-about config + handlebars template
  for the Rust-side license enumeration
- scripts/generate-licenses.mjs: orchestrator that runs cargo-about and
  license-checker-rseidelsohn (via npx, no devDep), merges the outputs into
  a single per-crate JSON with full license texts, and writes the result to
  src/data/licenses.json

The script is invoked directly with `node scripts/generate-licenses.mjs` —
no npm script wrapper on purpose, since adding one to package.json would
trigger the nix-npm-deps-hash-sync workflow on every push.

Initial generation covers 575 cargo crates + 71 npm packages (646 entries
total, all with full license text bundled, ~1.4 MB JSON).

* feat(licenses): Settings panel UI

Adds a new Open Source Licenses section under Settings → System,
sitting below Contributors. Components:

- LicensesPanel.tsx: search input, curated highlight block of ~10 key
  dependencies (Tauri, React, rodio, symphonia, etc.), TanStack-Virtual
  list of all 600+ entries
- LicenseTextModal.tsx: full-screen-ish modal showing the bundled license
  text plus name/version/license-id badges + repository link
- licensesData.ts: lazy dynamic-import loader (Vite emits the JSON as a
  separate chunk, so the heavy ~1.4 MB payload is only loaded when the
  user actually opens the panel — no runtime fetch, the data is fixed
  into the build artifact)

The panel registers itself in the Settings in-page search index under
the System tab.

* feat(licenses): i18n in 8 locales

Adds the `licenses` namespace (title, intro, highlights, search
placeholder, no-results, loading / load error, no-license-text,
view-source, total line, generated-at) across en, de, fr, nl, zh, nb,
ru, es. License names themselves (MIT, Apache-2.0, GPL-3.0, …) stay
universal and are rendered as-is.

* docs(release): document licenses regeneration step

Adds a Step A.3 to the release SOP describing the maintainer-only
`node scripts/generate-licenses.mjs` workflow, the cargo-about
prerequisite, and explicitly notes why no npm script wrapper exists
(would trigger the nix-npm-deps-hash-sync workflow).
2026-05-06 14:17:13 +02:00
cucadmuh d48ea819c1 fix: stabilize preview seekbar, post-sleep audio recovery, and card hover behavior (#476)
* fix(player): freeze main seekbar during track preview

Preview pauses the main sink in Rust while isPlaying stays true in the
store, so WaveformSeek's interpolation rAF must not advance progress.

* fix(audio): recover output after sleep and stalled streams

Add platform-specific post-sleep recovery hooks for Windows and Linux, and add a watchdog that reopens the output stream when playback is active but sample progress stalls, so audio can recover without restarting the app.

* fix(ui): remove card hover lift and smooth artwork zoom

Remove vertical hover translation from album and artist cards, and move image fade transition out of inline styles so cover zoom uses CSS timing consistently.

* fix(player): prevent seekbar jump after preview ends

Reset interpolation anchor timing when preview freeze state changes so the main seekbar does not momentarily jump forward before resyncing.

* fix(audio): reduce false watchdog recoveries and add diagnostics

Arm stalled-output recovery only after long poll gaps that suggest sleep/resume, and add detailed watcher logs for arm/clear/trigger paths to diagnose unintended stream reopens.

* chore(ui): drop card GPU hints and clarify macOS sleep scope

Remove translateZ and will-change hints from album and artist cover images to avoid per-card compositing overhead on software-composited Linux paths, and document why post-sleep recovery hooks currently target only Windows and Linux.

* docs(audio): document intentional Win32 callback pointer lifetime

Add inline rationale for the two Box::into_raw pointers in Windows suspend/resume registration so future maintenance does not treat the process-lifetime pointers as accidental leaks.

* docs(changelog): summarize playback stability updates for PR #476

Add a high-level changelog entry for preview seekbar fixes, sleep/wake audio recovery hooks and watchdog diagnostics, and card-hover stability adjustments from PR #476.

* docs(contributors): add cucadmuh entry for PR #476

Logs the post-sleep audio recovery, preview-seekbar fixes and card
hover stability work in the Settings → System → Contributors list.
2026-05-06 13:28:38 +03:00
cucadmuh 5abe18d5b8 Perf/UI performance (#473)
* perf(ui): defer off-screen Artist grid paint + rAF overlay scrollbar

Apply content-visibility to artist tiles in .album-grid-wrap (aligned with
album cards). Coalesce OverlayScrollArea thumb updates to one requestAnimationFrame per scroll burst.

* perf(css): content-visibility on horizontal artist rails

Apply the same off-screen deferral as album cards to `.album-grid .artist-card`
(ArtistRow, Favorites, etc.).
2026-05-06 10:45:13 +03:00
cucadmuh de3c0d9da1 Feat/performance probe fps overlay (#472)
* feat(perf-probe): add FPS overlay toggle and tidy probe modal

Add optional rAF-based FPS readout controlled by a persisted probe flag.
Remove the separate keyboard shortcut. Collapse all phase sections by default.

* perf(fps-overlay): subscribe only to showFpsOverlay flag

Add usePerfProbeFlag so the overlay does not re-render when other probe
toggles change. Track the animation frame id with a loop-local variable.
2026-05-06 02:13:59 +03:00
cucadmuh c3d37546cf Feat/search improvements (#470)
* feat(covers): race sibling downscale vs fetch, search thumb priorities

Run getCoverArt and client downscale in parallel when another size of the
same cover is cached; first successful result wins and aborts the other path.
Await both branches so inflight bookkeeping does not detach early.

Extend the cover cache size roster so provisional siblings resolve for sizes
used in the UI (e.g. 400/600/800, 48/96).

CachedImage: fetchQueueBias for live/mobile search (artist thumbnails ahead of
albums in fetch-slot ordering); configurable observeRootMargin with a wider
default to prepare priority slightly before elements enter view.

Mobile search adds round artist-thumb styling; add shared cover blob downscale
helper.

* perf(image-cache): batch sibling IDB reads and guard cover size registry

Use one read transaction when probing IndexedDB for sibling cover keys.
Extract COVER_ART_REGISTERED_SIZES and add Vitest coverage so every literal
coverArtCacheKey(_, size) in src stays aligned with sibling invalidation.
Honor AbortSignal during JPEG encode in downscaleCoverBlob.
2026-05-06 01:26:30 +03:00
Frank Stellmacher 072bef473f fix(queue): restore pre-#419 Play-icon prefix on the active row (#469)
PR #419 replaced the small (10px) Play icon next to the active queue
title with an animated 3-bar eq-bars block prefixed before the row.
Restore the original Play-icon-in-title behaviour and drop the unused
isStorePlaying selector.
2026-05-05 23:40:07 +02:00
cucadmuh 9d30285ff1 Perf/UI cover cache mainstage (#468)
* Enhance CachedImage and ArtistDetail components with improved image caching and priority handling

- Refactor CachedImage to utilize a priority system for image loading based on viewport visibility, improving performance during scrolling.
- Update useCachedUrl to accept an optional getPriority function for better cache management.
- Optimize ArtistDetail and Artists components by using useMemo for cover art URLs, reducing redundant calculations and improving rendering efficiency.
- Adjust image loading logic in CachedImage to ensure smoother transitions and avoid unnecessary fetch requests.

* perf(ui): unblock IDB cover art, stabilize mainstage rails and virtual lists

Let IndexedDB reads bypass the network concurrency slot so cached thumbnails
paint without queueing behind remote fetches; debounce disk eviction during
heavy scrolling.

Fix mainstage horizontal rails: dedupe album/song ids for React keys, widen
artwork budget overscan, avoid resetting the budget on list append, and raise
Home initial artwork budgets. CachedImage treats already-decoded images as
loaded; rail cards load cover images eagerly.

Refresh dynamic color extraction and extend virtual scrolling / scroll roots on
Albums, Artists, Playlists, and related surfaces.


Remove local agent-only commit instructions from the repository tree.

* perf(virtual): viewport-based overscan for main scroll lists

Drive TanStack Virtual overscan from measured scroll height so each list
renders about one screen of extra rows above and below the viewport for
snappier scrolling on Albums, Artists (list mode), and Tracks virtual song list.

Introduce useResizeClientHeight helpers (ID + ref) for ResizeObserver-based
clientHeight tracking.

* docs(changelog): note PR #468 UI cover cache, rails, and virtual lists

Add a coarse summary under 1.46.0 Changed for cover-art pipeline,
mainstage rails, viewport-based overscan, and library/chrome polish.
2026-05-06 00:15:58 +03:00
Frank Stellmacher d33abf565c feat(library): "favorites only" filter on Albums, Artists, AdvancedSearch (#466)
* feat(ui): StarFilterButton component + common i18n keys

Reusable toggle button for "favorites only" filtering. Three size
variants for different toolbar contexts:
- default: icon + label (Albums-style)
- compact: icon-only with 0.5rem padding (Artists view-mode buttons)
- small:   icon + label at 12px / 4×14 padding (AdvancedSearch tabs)

Adds common.favorites + favoritesTooltipOff/On in all 8 locales.

* feat(library): "favorites only" filter on Albums, Artists, AdvancedSearch

Client-side filter using the existing useMemo pipelines on each page.
Reads starred state from item.starred + playerStore.starredOverrides
(O(1) Map lookup, picks up live star toggles without refetch).

- Albums: toolbar button (default size) next to compilation filter.
- Artists: toolbar button (compact / icon-only) before the Images toggle.
- AdvancedSearch: toolbar button (small) next to the result-type tabs;
  filters all three result categories (artists / albums / songs) and
  updates the count badges accordingly.

Filter state is ephemeral per-page (not persisted) so users don't get
surprised by hidden items after a restart. Zero extra server calls.

* docs(contributors): credit + changelog entry for #466
2026-05-05 23:02:22 +02:00
Frank Stellmacher 0fab2849e5 feat(queue): preserve Play Next order toggle (#464)
* feat(queue): add preservePlayNextOrder setting + playNext store action

- New Track.playNextAdded flag (analogous to autoAdded / radioAdded).
  Stale flags behind queueIndex are harmless — only forward streak scan.
- New playerStore action playNext(tracks): tags incoming tracks and
  delegates to enqueueAt for unified undo + server sync.
- New authStore boolean preservePlayNextOrder (default false). When on,
  playNext appends behind the existing Play-Next streak (Spotify-style)
  instead of inserting directly after the current track.

* refactor(context-menu): centralise Play Next; add Settings toggle + i18n

- Replace 3 inline splice/enqueueAt call sites in ContextMenu with the
  new playNext action. Side-benefit: the single-song path now goes
  through enqueueAt and gets undo + queue sync (previously missing).
- Settings → Audio → Playback: new toggle below Gapless.
- 8 locales: preservePlayNextOrder + preservePlayNextOrderDesc.

* docs(contributors): credit + changelog entry for #464
2026-05-05 22:33:15 +02:00
cucadmuh 8d8c1aa8a3 Environment upgrade & hot-cache playback (#463)
* chore: upgrade dependencies and migrate playback to rodio 0.22

Bump npm and Rust crates; adapt symphonia decoding, ringbuf 0.5, lofty tags,
and discord-rich-presence usage. Use native rodio Player/MixerDeviceSink and
cpal device descriptions; drop the unused cpal patch. Align Vite 8 build
targets and chunking; remove redundant dynamic imports and fix hot-cache debug
logging imports.

* perf(build): lazy-load routes and restore default chunk warnings

Lazy-load all routed pages with React.lazy to shrink the main bundle; wrap root
Routes in Suspense for lazy Login. Drop chunkSizeWarningLimit override so Vite
uses the default 500 kB threshold.

* fix(windows): tray double-click without spurious menu; clean unused import

Disable tray menu on left mouse-up on Windows so a double-click to hide the
main window does not immediately reopen the context menu (tray-icon default
menu_on_left_click). Gate std::fs in app_api/core behind cfg(linux) for
/proc-only code so Windows builds stay warning-free.

* fix(sidebar): preserve new-releases read state under storage cap

When merging seen album ids, keep the current newest sample first so the
500-id localStorage limit does not truncate freshly marked reads and bring
back the unread badge.

* fix(audio): hot-cache replay, analysis no-op skips, playback source UI

Retain stream_completed_cache across audio_stop so end-of-queue replay can
use RAM promote or disk hot file instead of re-ranging HTTP.

Add cpu_seed_redundant_for_track gate before file/bytes seeds and local-file
spawn; emit analysis:waveform-updated only on Upserted. Ranged/legacy promote
checks generation after await before filling the slot.

Frontend: promote on same-track and cold resume; set currentPlaybackSource on
resume, queue undo restore, and gapless track switch so cache/stream icons stay
accurate. Import tauri::Manager for try_state in audio_play.

* fix(ts): narrow activeServerId for hot-cache promote calls

promoteCompletedStreamToHotCache expects a string; bind non-null server ids
in repeat-one, playTrack prev/same-track, and cold resume paths so tauri
production build (tsc) succeeds.

* fix(player): handle same-track hot-cache promote promise chain

Add .catch for promoteCompletedStreamToHotCache → runPlayTrackBody so sync
throws and unexpected rejections do not surface as unhandled in DevTools;
reset defer-hot-cache prefetch and isPlaying on failure.

* chore(nix): sync npmDepsHash with package-lock.json

* chore(release): finalize 1.46.0 CHANGELOG with PR #463 links

Document the release with full GitHub PR #463 on every subsection so
entries stay attributable if sections are reordered. Fix ContextMenu
lines where dynamic imports were accidentally merged onto one line.

* docs(contributors): credit cucadmuh for #463
2026-05-05 22:00:29 +03:00
Frank Stellmacher 5913d20cc2 fix(lyrics): keep highlight + auto-scroll alive after tab switch (#454)
The track-change reset wiped lineRefs/wordRefs in its effect body, which
ran *after* the commit phase that just populated them on remount. The
tracker effect then saw an empty refs array, no-op'd the DOM update, but
still advanced prevActive — so every following progress tick early-returned
on `prev.line === lineIdx` and the highlight froze.

Track the previous track id in a ref and skip the reset on initial mount,
so refs only get cleared on an actual track change.
2026-05-04 20:47:15 +02:00
cucadmuh a6cc2e2ad4 perf(linux): WebKit probe, throttled progress IPC, snapshot playback UI (#452)
* feat(linux): optional native GDK for Nix gdk-session

Introduce PSYSONIC_ALLOW_NATIVE_GDK so main skips the default GDK_BACKEND=x11
pin when the Nix gdk-session wrapper sets the flag. Remove GDK_BACKEND from
the npm tauri:dev script so it does not override nix develop defaults.

* fix(ui): portal server switch menu above sidebar

Main column stacks below the sidebar (layout z-index), so an in-tree dropdown
could never win over the left nav. Render the menu via createPortal to
document.body with fixed coordinates, matching the library scope picker.

* feat(perf): add mainstage probe controls and cut WebKit repaint load

Add a dedicated performance probe surface for mainstage/home toggles and wire Linux CPU diagnostics to isolate expensive UI paths. Tune waveform drawing and Home artwork clipping/windowing so visible content loads immediately while reducing WebKit compositor pressure during playback.

* fix(perf): stop hero rotation when section is off-screen

Gate hero auto-rotation and backdrop crossfade by real viewport visibility using the actual scrolling ancestor. This prevents periodic 10-second CPU spikes from hidden hero updates while preserving normal behavior when the hero is visible.

* fix(perf): isolate player progress updates from mainstage diagnostics

Add probe toggles for PlayerBar waveform and live progress UI updates to confirm playback progress churn as the main CPU driver.
Restore Home artwork quality defaults and keep visual-degradation modes opt-in via debug flags only.

* fix(hero): resume background and autoplay after viewport return

Re-check hero visibility on focus/visibility changes and add a short recovery poll while off-screen so missed scroll/RAF events cannot leave hero animation paused.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(perf): decouple playback progress from mainstage compositing pressure

Throttle audio progress delivery and route live seekbar timing through a lightweight progress channel to cut focus-time WebKit CPU spikes.
Add focused diagnostics in Performance Probe and restore hero/waveform behavior so visuals remain stable while profiling.

* fix(debug): open performance probe with Ctrl+Shift+D

Replace logo-triggered opening with a keyboard shortcut and keep logo purely decorative to avoid accidental probe activation.

* docs(changelog): document experiment/performance probe and playback work

Add an [Unreleased] section for the performance probe, throttled audio
progress IPC, snapshot-based live UI updates, WaveformSeek scheduling
over the same canvas bar renderer, Hero/Home rail fixes, and Linux/Nix
GDK dev ergonomics.

* perf(linux): add WebKit probe, throttle progress IPC, snapshot playback UI

Ship Performance Probe (Ctrl+Shift+D), Rust-throttled audio:progress, a
playback progress snapshot channel with coarse Zustand timeline commits,
Linux /proc CPU readout for the probe, Hero and Home rail artwork fixes,
Tracks SongRail windowing parity, MPRIS cleanup, gated perf counters, and
WaveformSeek paused-seek correctness. Documented in CHANGELOG for PR #452.

* docs(changelog): fold perf work into 1.45.0 and refresh date

Drop the separate 1.45.1 heading; keep PR #452 notes under 1.45.0 Added and
set the section date to 2026-05-04. Restore the safety preface before the
versioned sections.

* docs(changelog): order 1.45.0 Added entries by PR number

Sort the 1.45.0 release notes so subsections follow ascending PR id (390
through 452), with PR #452 last.

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-04 20:06:49 +03:00
cucadmuh dc7a785f94 feat(ui): bulk entity ratings, Random Albums multi-select, album New badge (#446)
Add star rating rows to multi-artist and multi-album context menus so the
selection shares one rating control (mixed ratings show empty until set;
keyboard navigation supported). Pass selectedAlbums into AlbumCard on Random
Albums so multi-select context menu works. Add i18n aria labels for bulk
rating controls. Move the New album badge to the top-right of the cover and
stack it with the offline badge to avoid overlap.
2026-05-04 01:56:26 +03:00
Frank Stellmacher 1799e90e04 feat(tracks): Highly Rated rail + per-card star display (#443)
* feat(tracks): Highly Rated rail + per-card star display

Adds a new SongRail above the Random Pick on the Tracks page that
surfaces the user's highly-rated tracks (sorted by rating DESC).
Auto-hides on non-Navidrome servers and when the library has no
rated tracks yet. Reuses the existing SongRail layout, with the
standard reroll button forcing a cache bypass.

Per-card stars: any SongCard whose `userRating > 0` now shows a
small five-star row (filled to the rating value) below the artist
line — visible everywhere SongCard is used, not only in the new
rail. Read-only display; rating is still done via the row's
context menu or the Now Playing star widget.

Cache layer in `ndListSongs`: opt-in `cacheMs` parameter (skipped
by VirtualSongList; used only by the Highly Rated rail with a 60 s
TTL). Cleared on `setRating` mutation so a freshly-rated track
shows up on the next page revisit, and on server switch alongside
the existing token cache. The reroll button explicitly invalidates
before refetching, so a manual refresh always hits the network.

* docs(changelog): add #443 Tracks Highly Rated rail entry

* chore(credits): add #443 to Psychotoxical contributions
2026-05-03 18:54:07 +02:00
Frank Stellmacher 98ff73d17a feat(perf): 3-state animation mode (Full / Reduced / Static) (#441)
* feat(perf): 3-state animation mode (Full / Reduced / Static)

Replaces the boolean `reducedAnimations` toggle with a three-way
`animationMode` setting, suggested by Viktor Petrovich after the
Windows audio fix (PR #426) shipped and confirmed a measurable
GPU drop:

- `full` (default): native frame rate, marquee scrolls normally
- `reduced`: 30 fps cap on the animated seekbar wave; player
  marquee runs at half speed
- `static`: rAF loop disabled; the seekbar repaints from the
  ~2 Hz audio:progress heartbeat. Player title/artist truncate
  with ellipsis instead of scrolling.

Migration in `onRehydrateStorage` maps legacy
`reducedAnimations: true` to `'reduced'`, anything else to
`'full'`. Static is opt-in only.

Settings UI follows the ReplayGain Auto/Track/Album pattern with
a contextual hint that explains what each mode does.

i18n: 5 new keys across 8 locales, 2 legacy keys removed.

* docs(changelog): add #441 3-state animation mode entry

* chore(credits): add #441 to Psychotoxical contributions
2026-05-03 14:12:27 +02:00
Frank Stellmacher 364b29ceee fix(queue): restore PR #420 QueuePanel.tsx parts lost in #419 squash (#440)
The drag-row-outside-queue-to-remove feature shipped in #420 (by
cucadmuh) had its QueuePanel.tsx wiring wiped when #419 was squash-
merged from a branch that pre-dated #420. The DragDropContext and
MiniPlayer parts of #420 survived; only QueuePanel.tsx was overwritten.

Restored to the state on commit 274ac5b3:
- Import `registerQueueDragHitTest` from DragDropContext
- Subscribe to `removeTrack` from playerStore
- Register the queue rect as a hit-test region on mount
- Document-level psy-drop listener that removes the dragged row when
  the drop coordinates fall outside the queue rect

Co-authored-by: cucadmuh <49571317+cucadmuh@users.noreply.github.com>
2026-05-03 14:03:07 +02:00
cucadmuh 1e05180418 feat(shortcuts): action registry + dynamic CLI help + new input targets (#435)
* feat(shortcuts): unify action-driven shortcut and CLI routing

Centralize shortcut action metadata in one TypeScript registry and route keyboard, global shortcut, mini-window, and CLI inputs through shared runtime handlers.
Keep CLI as an abstract transport layer by emitting player-command payloads without depending on shortcut definitions.

* feat(shortcuts): generate CLI action help from shortcut registry

Move no-arg player commands and their descriptions into the central action registry so CLI parsing and --player help are derived dynamically from one source of truth. Also route runtime action execution through the registry and remove duplicated shortcut runtime handling.

* feat(shortcuts): add new input actions and hidden F1 help binding

Add the requested input actions (search, advanced search, sidebar, mute, equalizer, repeat, now playing, lyrics, favorite current track) to the central shortcut action registry and wire runtime handlers for sidebar/equalizer toggles. Keep Help bound to F1 by default while hiding it from Settings input lists, and backfill persisted keybindings with new defaults so F1 works for existing users.

Requested by @zunoz (Discord community).
2026-05-03 00:37:43 +03:00
Frank Stellmacher fca8fc5318 fix(seekbar): blank canvas after update from legacy build (#432)
After PR #316 split the 'waveform' seekbar style into 'truewave' /
'pseudowave', users who carried any other unrecognised persisted value
(legacy variants, undefined, tampered strings) ended up with a blank
seekbar — the rehydrate migration matched only the literal 'waveform'
string, and the drawSeekbar dispatcher had no default branch, so an
unknown style drew nothing.

Two-layer fix:

* authStore migration now treats *any* value not in the current
  SeekbarStyle union as legacy and resets it to 'truewave'.
* drawSeekbar gains a default case that falls back to the truewave
  renderer, so even if a future style mismatch slips through the
  store-level guard the user still sees a usable seekbar.

Visible to affected users on next app start (rehydrate runs once per
session). Clicking a style in Settings has always worked around the
issue; this fix removes that workaround.
2026-05-02 22:50:22 +02:00
Frank Stellmacher e44e6dcdf4 fix: restore audio refactor + features lost in #419 squash-merge (#429)
The squash-merge of PR #419 was performed against an outdated PR base
that predated several main-side refactors and features. The resulting
squash inadvertently re-introduced files that had already been removed
(`src-tauri/src/audio.rs` monolith, `app-icon.png`) and reverted main's
content for ~20 files (`src-tauri/src/lib.rs` decompose, `src/App.tsx`
animation-pause, `src/components/AlbumRow.tsx` headerExtra, etc).

This commit:

* Restores all collateral-damage files to their pre-#419 main state
  (5662dafe), including the audio module split, lib decompose,
  Cargo.toml `windows` dep, animation-pause-on-blur logic, and
  `reducedAnimations` toggle plumbing in WaveformSeek/Settings.
* Keeps the genuine #419 work intact: tri-state duration toggle,
  position counter, persistent Now Playing collapse, animated EQ
  indicator (in QueuePanel.tsx, 8 locales, authStore, components.css).
* Merges authStore.ts so both `reducedAnimations` (from #426) and
  `queueNowPlayingCollapsed` (from #419) coexist.
* Adds the `.eq-bars.paused` CSS rule manually since components.css
  needed a 5662dafe base + the single #419 addition.
* Fixes a latent type error in OverlayScrollArea.tsx (`useRef<T>(null)`
  is `RefObject<T>` / read-only under current `@types/react`; widened to
  `useRef<T | null>(null)` so the existing `wrapRef.current = el`
  assignment compiles).

Verified locally with `cargo check` and `npm run build` — both green.
2026-05-02 22:01:51 +02:00
Kveld. 18b4a982ef feat: queue-ux-improvements (#419)
* feat(queue): add ETA display, equalizer indicator and collapsible now playing

* deleted endsAt and showDuration strings, changed eta update to 30s

* feat(queue): ETA tooltip, persistent Now Playing collapse, EQ bar pause, remove redundant Play icon

* feat(queue): fold ETA into existing total/remaining toggle as third mode

The standalone ETA span next to the track counter is removed; instead the
clickable duration label in the queue header now rotates through three
modes per click: total → remaining → eta → total. Counter (N/M) stays
where it was.

ETA mode keeps the live-feel treatment from the original PR (accent
colour while playing, muted at 50% opacity when paused). The other two
modes use plain accent.

i18n: queue.etaTooltip removed (no longer a separate descriptive label),
queue.showEta added as the action tooltip ('Show estimated end time')
in all 8 locales — matches the showRemaining / showTotal pattern.

* docs(changelog): add #419 queue UX improvements entry

Adds the [1.45.0] / Added entry for this PR's queue panel refinements
(position counter, tri-state duration toggle including ETA, collapsible
Now Playing section, animated EQ indicator).

---------

Co-authored-by: Psychotoxical <171614930+Psychotoxical@users.noreply.github.com>
2026-05-02 21:45:55 +02:00
Frank Stellmacher 2e9618cf54 fix(audio): Windows playback stutter under GPU load (#334) (#426)
* fix(audio): promote WASAPI render thread to MMCSS Pro Audio on Windows

Wraps the outermost audio source in a `PriorityBoostSource` that calls
`AvSetMmThreadCharacteristicsW("Pro Audio")` on its first sample. The
cpal output-stream callback runs `Source::next` on the WASAPI render
thread, which is otherwise normal-priority and gets preempted under
WebView2 / DWM / GPU pressure — producing the audible click/stutter
reported in issue #334. No-op on Linux/macOS (PipeWire/rtkit and
CoreAudio promote their audio threads externally).


* fix(build): repair Windows compile after audio split + lib decompose

Two pre-existing build breakers on Windows that surfaced after the
`use super::*;` cleanup (9cc74a7) and the lib-decompose refactor
(cfeec22):

- `audio/device_watcher.rs` lost the `output_enumeration_includes_pinned`
  import. Add it cfg-gated to `not(target_os = "linux")` since it's only
  called inside the non-Linux pinned-device fallback path.
- `lib_commands/sync/tray.rs::is_tiling_wm()` is `#[cfg(target_os = "linux")]`,
  but its Tauri command wrapper `is_tiling_wm_cmd()` is unconditional.
  Add a `#[cfg(not(target_os = "linux"))]` stub returning `false`.

No behavior change on Linux. Restores `cargo build` on Windows + macOS.

* perf(ui): pause cosmetic animations when window loses OS focus

Companion to the WASAPI MMCSS fix in this branch — issue #334
reported audible audio stutter on low-end laptops, partly traced
to WebView2 still compositing CSS animations + waveform canvas at
full rate while the user has alt-tabbed into another app.

Existing `data-app-hidden` / `data-psy-native-hidden` flags pause
animations only when the window is fully hidden (minimized or
behind a tray). When the window is visible-but-unfocused — the
common "music in the background while I work" case — every infinite
keyframe + 60fps rAF loop keeps running.

Add a parallel `data-app-blurred` flag driven by `window.focus`/
`window.blur`, plus matching `__psyBlurred` on the global window
object for imperative checks. The CSS rule that pauses every
`animation-play-state` on hidden gets a third selector for blurred.
WaveformSeek's two 60fps rAF loops (animated styles + settings demo)
extend their existing `document.hidden || __psyHidden` short-circuit
to also include the new blurred flag, falling back to the same
400ms polling path that already exists for hidden.

Verified visually on the dashboard: alt-tab → `<html
data-app-blurred="true">`, mesh blobs in the fullscreen player +
marquee + waveform 60fps loop all pause. Click back into the
window → blob/marquee/waveform resume immediately.

Cross-platform: focus/blur events fire reliably on all targets, so
the optimization applies to Windows, Linux (incl. WebKitGTK with
software compositing where the savings are largest), and macOS.

* chore: remove stale app-icon.png from repo root

* perf(ui): add 'Reduce animations' toggle that caps animated seekbar styles to 30 fps

Off by default. When on, animated seekbar styles (pulsewave, particletrail,
liquidfill, retrotape) skip every other rAF and advance the animation timer by
a doubled delta, so wave speed is unchanged while draws are halved. Aimed at
low-end Windows GPUs where the 60 fps shadow-blur loop drives ~40 % WebView2
GPU usage even when focused.

Toggle lives directly under the seekbar style picker in Settings → Appearance.
i18n in all 8 supported languages.
2026-05-02 21:10:32 +02:00
Frank Stellmacher 7064ca500e feat(stats): shareable Top-Albums card export (#425)
* feat(stats): add shareable Top-Albums card export

Adds a shareable PNG export of the user's most-played albums,
available from the Statistics page under the "Most Played Albums"
section header.

Layout:
* 3 formats — Story (1080×1920, 9:16), Square (1080×1080), Twitter
  Card (1200×675, 16:9). Story and Square stack a centered URL footer;
  Twitter packs logo + label + URL into a single header row.
* 3 grid sizes — 3×3 (default), 4×4, 5×5. Modal fetches up to 25
  frequent albums on open so larger grids work even when the entry
  surface only loaded a handful.
* Cover tiles render the rank + play count in a thin black info strip
  at the bottom of the cover (rank left, "N Plays" right).
* Header label is hardcoded English ("Top Albums") so a shared image
  remains legible to followers who don't share the user's UI language.
  Caller can override via `opts.meta`.

Source path is local-only — `getAlbumList(type='frequent')` from
Subsonic. Last.fm is intentionally not used.

Implementation:
* `src/utils/exportAlbumCard.ts` — pure Canvas-API renderer. Reads
  theme accent / bg from the document's CSS variables so the export
  matches the active theme. Cover art is loaded through the existing
  `getCachedBlob` IndexedDB cache, so repeated exports are cheap.
  Wordmark is rendered from the `PsysonicLogo` React component via
  `renderToStaticMarkup`, with both gradient stops baked to a single
  accent color for crisp single-tone branding.
* `src/components/StatsExportModal.tsx` — UI: format + grid pickers,
  live downscaled preview, native save dialog via
  `@tauri-apps/plugin-dialog` + `plugin-fs`.
* `src/components/AlbumRow.tsx` — adds optional `headerExtra` slot so
  the share button sits next to the existing scroll-nav arrows.
* i18n keys added under `statistics.*` (`en`, `de`) — other locales
  fall back to English.

* docs(changelog): add Top-Albums card export for PR #425
2026-05-02 16:44:29 +02:00
Frank Stellmacher 297c9f1125 fix(preview): sync audio start, ring animation, and download timeout (#423)
* fix(preview): sync audio start, ring animation, and download timeout

Three coupled fixes for the track-preview engine:

1. Audio sync. `Sink::try_seek` was running on a worker thread after
   `sink.append(source)`, so the sink began playing position 0 while
   the seek was still iterating to the mid-track target. With the
   30 s `take_duration` cap counting wall-clock from append, audio
   could only become audible ~25% into the preview window. The seek
   now runs on the bare source before append, then `take_duration`
   wraps it — playback starts at the seek position with the cap
   measured from there.

2. Ring animation gating. The CSS progress-ring animation was
   bound to `is-previewing` (set on click), so the ring sprinted
   ahead of any download/decode/seek warmup and didn't reset
   cleanly when switching from one preview to another. Added an
   `audioStarted` flag in `previewStore` that flips on
   `audio:preview-start` from the engine; CSS animation is now
   gated on `audio-started` instead. `is-previewing` still drives
   tooltip/icon for instant click feedback. Same SVG is reused for
   a 25%-arc rotating loading spinner while waiting for audio,
   with a 150 ms delay so cached/short previews don't flash.

3. Download timeout. The shared `audio_http_client` caps at 30 s,
   which aborts mid-download on multi-hundred-MB uncompressed
   files (e.g. 18-min Hi-Res WAV ~600 MB). The preview engine now
   builds a dedicated client with a 5 min timeout for the bytes
   fetch. Watchdog still bounds the playback window at 30 s once
   the audio actually starts.

Touches `audio/preview.rs`, `previewStore.ts`, `components.css`
plus the eight tracklist/player-bar components that render the
preview button.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(changelog): add preview audio sync fix for PR #423

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 14:44:49 +02:00
cucadmuh 274ac5b3b4 feat(queue): drag rows outside queue to remove; trash ghost outside bounds (#420)
* feat(queue): drag rows outside queue to remove; trash ghost outside bounds

- Document-level psy-drop when cursor leaves queue rect (main + mini)
- DragDropContext: drop coordinates on psy-detail; optional wrapRef on OverlayScrollArea
- Trash icon on drag ghost only outside registered queue hit-test regions

* docs(changelog): add queue drag-out removal for PR #420
2026-05-02 02:36:35 +03:00
cucadmuh 9ad0f8af6d feat(ui): UI refinements — sidebar indicators, adaptive header, and interaction polish (#397)
* feat(ui): unify queue toggle handle behavior

Show the queue toggle in the header when the queue is collapsed and use a seam-aligned drag handle when it is open. Hide the seam handle while the main content is actively scrolling to reduce accidental interactions.

* feat(ui): add adaptive header search collapse behavior

Collapse header search to a magnifier when top controls get crowded and expand it as an overlay only while active. Use measured header space with hysteresis to avoid flicker and keep neighboring controls stable.

* chore(ui): remove leftover search prototype artifacts

Drop an unused icon import from live search and remove an unused header container-type rule left from an earlier layout experiment.

* feat(ui): persist sidebar and queue visibility state

Save left sidebar collapse and right queue open/closed visibility in local storage helpers so both panel modes are restored after app restart.

* feat(ui): unify player overflow menu behavior

Use a single overflow menu for click and wheel interactions, with a volume-only mode that keeps the same layout and volume controls as the full menu.

* feat(ui): add wheel seek controls to waveform

Apply 10-second wheel seek steps with trailing 1-second debounce and keep the waveform preview stable so the playhead moves smoothly during rapid scroll input.

* fix(now-playing): stabilize narrow dashboard layout

Switch now-playing responsiveness to container-based breakpoints and prevent stacked widgets from overlapping when width is constrained.

* fix(search): reduce collapse jitter and avoid header overlap

Add a short collapse-state cooldown to prevent threshold flicker and hide conflicting header controls while collapsed search expands as an overlay.

* fix(i18n): localize player overflow controls across locales

Replace hardcoded player overflow labels with translation keys and add the missing keys for all shipped locale files.

* fix(search): keep advanced control clickable in collapsed mode

Prevent focus loss on the advanced search button in collapsed overlay mode so its click handler consistently runs.

* fix(i18n): restore queue translation in offline library

Use the existing queue.appendToQueue key for the offline enqueue button tooltip and label instead of a missing key and hardcoded English text.

* fix(ui): apply overlay scrollbar to right-panel text tabs

Switch now-playing content, lyrics, and info panes to OverlayScrollArea and harden tour-item layout so long concert metadata stays within panel bounds.

* fix(ui): add unread indicator for new releases and guard sidebar drag clicks

Track unread new-release IDs per server/library scope and clear the badge when opening the New Releases page. Also prevent click-through navigation after sidebar drag release and keep related i18n/responsive sidebar-adjacent refinements in this snapshot.

* fix(ui): stabilize live dropdown layering and unread reset flow

Render the topbar Live dropdown via a portal so it consistently overlays sidebar layers. Rework new-releases unread tracking to handle library scope baselines, ignore stale refresh races, and mark items as seen after a 5-second stay on the New Releases page.

* feat(ui): add localized New badges for recently added albums

Show a theme-consistent New badge on album cards and album detail for albums created within the last 48 hours. Localize the badge label across all supported locales and centralize recency logic in a shared utility to avoid duplication.

* fix(album): prevent tracklist jump when entering multiselect

Move bulk selection actions from the tracklist body into the album toolbar next to the track filter. Keep selection controls stable in the header area so enabling multiselect no longer shifts the tracklist content downward.

* fix(tray): add playback-state badge and finalize queue handle tooltip

Show play/pause/stop icons in the Linux tray now-playing entry and persist state safely in Tauri managed state.
Also switch the queue-resize handle tooltip to the dedicated localized key across all locales.

* fix(header): prioritize search collapse before Live/Orbit labels

Make topbar compression deterministic by collapsing search first and compacting Live/Orbit labels only in sustained low-space mode.
Add sticky hysteresis-based header compact state to prevent oscillation while resizing.

* fix(ui): stabilize header compaction and show tray state icons

Prevent topbar flicker in the narrow-width range by tightening compact-mode thresholds, gating on real overflow, and removing width transitions from live search.
Also include playback state icons in tray tooltip text across platforms while preserving tooltip length limits.

* fix(tray): keep tooltip iconization Windows-only

Revert Linux tray tooltip/title fallback attempts and keep state icons only in Windows tray tooltips, while Linux continues to show playback state in the now-playing menu entry.

* fix(ui): restore queue resize response after overlay scroll interactions

Hide the queue handle while scrolling on both the main route viewport and the now-playing viewport, and clear stale thumb-drag state before starting queue resize.
Also ignore inactive/faded overlay thumbs in resizer suppression so horizontal pointer transitions no longer leave the queue seam unresponsive.

* docs(changelog): summarize ui-refinements branch features

Document the branch-level feature additions in 1.45.0 as separate changelog sections and group remaining branch-local fixes under a single polish entry.

* docs(changelog): add PR #397 references for ui-refinements

Attach PR metadata to the new 1.45.0 ui-refinement sections and the polish entry so release notes map directly to the merged branch discussion.
2026-05-01 15:42:40 +03:00
Frank Stellmacher 20a083a9a6 feat(player): preview indicator in player bar + smart stop semantics (#394)
* feat(player): preview-active state on play button (ring + stop icon)

Checkpoint: play button mirrors the inline preview button from tracklists
during preview playback — hollow circle, accent ring depleting over the
preview duration, Square (stop) icon. Click still resumes main playback,
which the Rust audio engine cancels the preview for.

i18n key player.previewActive in all 8 locales for tooltip + aria-label.


* feat(player): show preview track in player bar + smart stop semantics

The player-bar info cell (cover, title, artist) now mirrors the previewing
track during preview playback, with a small accent "Preview" pill above
the title and an accent top-border on the bar. Rating, fullscreen hint
and album/artist link clicks are suppressed while previewing — they
target the queued track, not the preview.

Stop semantics for the two transport buttons during preview:
- Big play button (Square+ring visual): stops preview, main auto-resumes
  if it was playing before. Matches the tracklist preview-button behaviour.
- Small Stop button: new audio_preview_stop_silent Rust command — stops
  preview AND leaves main paused, so "Stop = silence" actually goes silent.

previewStore now stores the full PreviewingTrack (id, title, artist,
coverArt) — the seven startPreview call sites pass it through.
i18n key player.previewLabel in all 8 locales.
2026-05-01 12:57:34 +02:00
Frank Stellmacher a14dba8167 feat(audio): rust track preview engine + inline play/preview buttons (#392)
* feat(audio): rust preview engine with secondary sink

Adds a parallel rodio Sink on the existing OutputStream for 30s
mid-track previews. Two new Tauri commands (audio_preview_play,
audio_preview_stop) plus three events (audio:preview-start /
-progress / -end). The main sink is paused with Sink::pause() and
auto-resumed on preview end iff it was playing beforehand.


* feat(playlists): migrate suggestion preview to rust audio engine

Replaces the HTML5 <audio> path with the new rust preview engine.
previewStore mirrors the engine's start/progress/end events so any
tracklist row can render preview UI from a single source of truth.

Spacebar redirects to stopPreview while a preview plays, hardware
mediakeys are silently dropped (Q5), and tray clicks cancel the
preview before forwarding the original action.


* feat(albums): inline play + preview buttons in tracklist rows

Track number stays static on hover instead of swapping to a play
icon — the dedicated Play and Preview buttons in the title cell
take over click-to-play and click-to-preview. Active+playing rows
keep the eq-bars (also on hover), active+paused rows fall back to
the static accent-coloured number.

Pilot for the wider rollout to other tracklists.

* feat(tracklists): roll out inline play + preview buttons

Mirrors the AlbumTrackList pilot across the remaining track-row
based lists: PlaylistDetail main tracks, Favorites, ArtistDetail
top tracks, RandomMix (both genre-mix and filtered-songs lists).

Track number stays static, the dedicated Play + Preview buttons
in the title cell take over click-to-play and click-to-preview.

* feat(settings): track preview toggle + configurable position and duration

Adds an opt-out switch and two sliders to Settings → Audio:
start position (0-90 % of track length, default 33 %) and preview
duration (5-60 s, default 30 s). The progress-ring animation
follows the duration via a CSS variable so the visual matches the
engine's auto-stop. Disabling the feature hides every inline
preview button via a single root-level data attribute, no per-row
conditional rendering required.

i18n keys added in all 8 locales.

* fix(audio): cancel preview when main playback (re)starts

audio_play, audio_play_radio, audio_resume and audio_stop did not
know about the parallel preview sink, so clicking Play on a track
that was currently being previewed left the preview running on top
of the freshly started main playback. New helper clears the resume
flag, bumps the preview generation, drops the sink and emits an
'interrupted' end event before any of those commands touches the
main sink.


* feat(settings): per-location track preview toggles

Splits the single trackPreviewsEnabled toggle into a master + 6 per-location
sub-toggles (suggestions, albums, playlists, favorites, artist, randomMix).
Master remains the kill switch; sub-toggles are only honoured when master
is on. Each tracklist container is marked with `data-preview-loc="<id>"` and
hidden via scoped CSS when the matching root attribute is "off".
startPreview now takes a location argument so the store can guard logic too.
i18n added in all 8 locales.


* fix(contextmenu): use ChevronsRight for Play Next to distinguish from preview
2026-05-01 09:11:28 +02:00
Frank Stellmacher 225f7c1406 feat(themes): add Kanagawa, Atom One, 1984 palettes; regroup OSS Classics by family (#390)
* feat(themes): add Kanagawa, Atom One, 1984 palettes; group OSS Classics by family

Adds three upstream-faithful theme families to Open Source Classics and
restructures the picker so the section is no longer alphabetically wild.

New themes (9 total):
- Kanagawa (rebelot/kanagawa.nvim): Wave, Dragon, Lotus
- Atom One (Th3Whit3Wolf/one-nvim): Dark, Light
- 1984 (juanmnl/vs-1984): Default, Cyberpunk, Light, Orwell
  (Fancy + Unbolded skipped — identical palette to Default, style-only)

Each theme defines the full token set (--bg-*, --accent, --text-*, all
--ctp-*, --waveform-*, --positive/warning/danger, --select-arrow), so
login screen, queue sidebar tabs, and all subpages inherit the palette
without component-level overrides.

Picker restructure:
- ThemeDef gains optional `family?: string`
- Open Source Classics regrouped: 1984, Atom One, Catppuccin, Dracula,
  Gruvbox, Kanagawa, Nightfox, Nord
- Family headings rendered inline (grid-column: 1 / -1) when family
  changes; new .theme-family-header style in components.css
- Theme scheduler dropdown labels prefixed with family for context

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(release): sync Cargo.lock to 1.45.0-dev
2026-05-01 08:51:34 +02:00
Frank Stellmacher be4e214133 fix(equalizer): bail out when canvas has no laid-out dimensions (#384)
drawCurve read offsetWidth/offsetHeight unconditionally. When the canvas
sits inside a collapsed <details> (SettingsSubSection) on first paint,
both are 0, innerW becomes negative, the points-loop never runs, and
points[0][0] throws. Linux WebKitGTK happens to lay out the details
expanded on first render; macOS WebKit does not, so the same code path
unmounted the entire React tree because there is no ErrorBoundary above
Settings.

Bail early when innerW or innerH is non-positive — the ResizeObserver
already triggers a redraw once the canvas gets real dimensions.

Closes #382.
2026-04-30 20:19:10 +02:00
cucadmuh ef45b726ad fix(changelog): resolve embedded notes by semver core (#373)
Whats New and the post-update modal required an exact ## [version] header,
so prerelease package versions (rc, dev) often had no matching section.
Match on major.minor.patch first; if several sections share the core,
prefer a plain X.Y.Z heading when present.
2026-04-29 22:36:57 +00:00
cucadmuh 87373edb17 fix(loudness): target sync, effective pre-analysis trim, and queue/settings copy (#333)
* fix(loudness): target sync, -14 pre-analysis ref, queue UI, and reseed

- Front: coalesce loudness refresh by target LUFS; replay-gain IPC dedupe keys
  include norm target and effective pre-attenuation so TGT changes apply.
- Rust: placeholder gain before integrated LUFS uses pivot at -14 LUFS; UI gain
  from effective trim; reseed loudness after delete when waveform cache would skip.
- Pre-analysis: store attenuation relative to -14 LUFS; engine and UI use an
  offset for other targets; migrate legacy absolute values on rehydrate.
- Queue/Settings: Loudness/TGT labels vs value buttons; styles; i18n for help.

* fix(i18n): simplify loudness pre-analysis helper copy

Remove reference-target wording from loudness pre-analysis helper text and keep
only the effective adjustment shown for the current LUFS target in all locales.
2026-04-27 02:54:58 +03:00
Maxim Isaev 55a49a9fb0 feat(player): restore queue list scroll on queue undo/redo
Store the main queue panel viewport scrollTop in undo snapshots and
reapply it after undo/redo so Ctrl+Z / Ctrl+Shift+Z restores scroll
position alongside queue state.
2026-04-27 00:33:47 +03:00